vue2-虚拟节点实现

简介: 前面在vue2-render函数中梳理了组件挂载的过程,其中在分析 $createElement 的时候有遇到虚拟节点的创建及使用。今天就来好好分析下虚拟节点在节点创建及渲染中所扮演个什么样的角色以及实现。

前面在vue2-render函数中梳理了组件挂载的过程,其中在分析 $createElement 的时候有遇到虚拟节点的创建及使用。今天就来好好分析下虚拟节点在节点创建及渲染中所扮演个什么样的角色以及实现。


为什么需要虚拟节点


网上关于虚拟节点的文章很多,关于为什么需要使用虚拟节点的原因也众说纷纭,下面我说说自己的理解


1.跨平台


我们知道 vue 其实是支持 weex 开发的,weex 我们可以理解为原生渲染引擎,它可以将按照协定的格式如 虚拟节点 将h5解析并渲染为原生APP。所以为了更好地兼容跨平台开发,有必要引入 DOM 管理机制,也就是由我们的虚拟节点实现。

可以看到,引入的虚拟节点可以作为一个中间桥梁,我们的数据由VUE先转化为虚拟节点,再根据不同的平台调用不同的API去渲染节点


2.提升效率


我们知道使用vue开发是不需要也不建议我们手动修改DOM的,我们通常只是建立模板,再操作数据来更改DOM。考虑到有两个相邻互斥标签,它们除了里面的文本不同其它都相同。如果仅仅根据数据渲染DOM的做法,虽然页面同时只会挂载一个节点,但对于VUE来说可能需要管理两个DOM节点。


但是引入虚拟节点后,VUE就可以很轻松地知道,页面当前实际只有一个节点,当我们切换数据挂载不同节点时,并不需要个新建删除的过程,而是能很好的复用节点。

所以引入虚拟节点其实是为了更好地管理当前页面的节点信息,通过虚拟节点的对比来达到复用及其它高效更新的目的。


虚拟节点的实现


我们先来看看vue中虚拟节点 VNode 的实现代码

export default class VNode {
  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag // 标签如div
    this.data = data // 数据包括style attrs class normalizedStyle staticClass staticStyle style等
    this.children = children // 子节点[VNode]
    this.text = text // 文本节点文本
    this.elm = elm // 关联的真实节点
    this.ns = undefined
    this.context = context // context vm实例
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key // key
    this.componentOptions = componentOptions // 组件配置
    this.componentInstance = undefined // 组件实例
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }
  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}
复制代码

可以看到 VNode 的代码看起来比较简单,就是定义了一堆属性,通过那些属性来描述一个节点。当然,我们也可以说其很复杂,因为定义的属性实在太多了,我也没有逐一弄去清楚每个属性的作用。


值得注意的是 componentOptions 选项,组件其实也会被创建为一个 VNode 实例,这个我们在以后的文章再去分析。


虚拟节点的创建到渲染


我们结合之前的 render 来分析下 VNode 的使用,主要包括虚拟节点创建时机,虚拟节点如何渲染为真实节点等。


1. 虚拟节点的创建


从组件挂载开始,我们在前面有分析过,组件实例化的最后一步是挂载节点

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}
复制代码


实际将调用 mountComponent

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
复制代码


vm.render() 中将调用

const { render, _parentVnode } = vm.$options
vnode = render.call(vm._renderProxy, vm.$createElement)
vnode.parent = _parentVnode
复制代码


此处的 render 则是我们在初始化时配置的 render 函数,如果我们使用的是 template 模板的话,在编译阶段会将 template 编译为 render


我们再来看看 $createElement 的逻辑 vm.$createElement 实际调用的是 createElement

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}
复制代码

我们再来看看 _createElement

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // ...
  // 扁平化子节点
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 保留标签如div span
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn) && data.tag !== 'component') {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      // 在这创建VNode
      // 关于节点的主要数据其实都保存在data中
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
        )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 组件类型暂不分析
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}
复制代码


到这,我们其实已经梳理完了 VNode 的创建过程,主要创建逻辑在 _createElement

2. 虚拟节点渲染为DOM


前面我们分析了虚拟节点的创建过程,现在我们再来看看虚拟节点是如何被渲染为真实节点的


回到前面,我们在创建过程分析了 vm._render() 的实现,最后将返回 VNode 节点

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
复制代码

接下来我们再看看 _update 是如何将 VNode 渲染为DOM的

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const restoreActiveInstance = setActiveInstance(vm)
  vm._vnode = vnode
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  restoreActiveInstance()
  // ...
}
复制代码


可以看到 _update 的主要逻辑就是调用 vm.__patch__,我们先来分析下首次调用时执行 vm.__patch__(vm.$el, vnode)

__patch 的定义来自 platform/web/runtime/patch.js

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
复制代码


这边最终调用了 createPatchFunction 同时传入两个参数


  • nodeOps DOM操作的相关API

  • modules DOM属性更新的相关函数


可以发现 nodeOpsmodules 都是和平台相关的参数,例如浏览器的DOM API,所以这边通过以参数的方式传递下去,进一步解耦,在 createPatchFunction 就可以专心处理 patch 相关事务了。


const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
  let i, j
  const cbs = {}
  // hooks定义了一个节点的生命周期如create创建update更新
  // modules定义了属性创建更新函数包括attr class event style props等
  const { modules, nodeOps } = backend
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  let creatingElmInVPre = 0
  let hydrationBailed = false
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
    let isInitialPatch = false
    const insertedVnodeQueue = []
    // 没有老节点 直接创建新节点
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      // 通过nodeType判断真实节点
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // 创建空节点先替代旧节点
          oldVnode = emptyNodeAt(oldVnode)
        }
        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)
        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}
复制代码

createPatchFunction 我们分析的比较粗放一些,主要了解其定义了 cbs 并将DOM更新相关的函数添加进里面。还有就是调用了 createElm 来创建真实节点。


我们再来简单看看 createElm

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // vnode是组件的情况
  vnode.isRootInsert = !nested // for transition enter check
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      // 调用nodeOps.createElement创建节点
      : nodeOps.createElement(tag, vnode)
    // scoped 相关实现
    setScope(vnode)
    if (__WEEX__) {
      // ...
    } else {
      // 这边会遍历子vnode然后调用createElm形成递归
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        // 在存在data的情况则调用invokeCreateHooks
        // invokeCreateHooks实际就会调用我们之前定义的cbs中的create数组
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      // 插入节点到DOM中完成渲染
      insert(parentElm, vnode.elm, refElm)
    }
  }
}
复制代码


createElm 中我们可以很清楚地梳理出节点的创建,样式更新及插入了

nodeOps.createElement 就是直接调用了 DOM API,可以看看


export function createElement (tagName: string, vnode: VNode): Element {
  const elm = document.createElement(tagName)
  if (tagName !== 'select') {
    return elm
  }
  // false or null will remove the attribute but undefined will not
  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
    elm.setAttribute('multiple', 'multiple')
  }
  return elm
}
复制代码


同理,插入就是调用了 parentNode.insertBefore,可能稍微需要说明一下的就是 invokeCreateHooks

function invokeCreateHooks (vnode, insertedVnodeQueue) {
  // 如前面所说就是调用create数组中的函数
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode)
  }
  // 组件生命周期钩子相关
  i = vnode.data.hook
  if (isDef(i)) {
    if (isDef(i.create)) i.create(emptyNode, vnode)
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  }
}
复制代码


我们前面有说过 cbs.create 其实来自于 modules,所以我们看看 modulessrc/core/vdom/modules/index.js

import attrs from './attrs'
import klass from './class'
import events from './events'
import domProps from './dom-props'
import style from './style'
import transition from './transition'
export default [
  attrs,
  klass,
  events,
  domProps,
  style,
  transition
]
复制代码


可以很清楚地看到和节点属性样式相关的函数名,以 style 为例,我们看看部分代码

function updateStyle (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  const data = vnode.data
  const oldData = oldVnode.data
  if (isUndef(data.staticStyle) && isUndef(data.style) &&
    isUndef(oldData.staticStyle) && isUndef(oldData.style)
  ) {
    return
  }
  let cur, name
  const el: any = vnode.elm
  const oldStaticStyle: any = oldData.staticStyle
  const oldStyleBinding: any = oldData.normalizedStyle || oldData.style || {}
  // if static style exists, stylebinding already merged into it when doing normalizeStyleData
  const oldStyle = oldStaticStyle || oldStyleBinding
  const style = normalizeStyleBinding(vnode.data.style) || {}
  // store normalized style under a different key for next diff
  // make sure to clone it if it's reactive, since the user likely wants
  // to mutate it.
  vnode.data.normalizedStyle = isDef(style.__ob__)
    ? extend({}, style)
    : style
  const newStyle = getStyle(vnode, true)
  for (name in oldStyle) {
    if (isUndef(newStyle[name])) {
      setProp(el, name, '')
    }
  }
  for (name in newStyle) {
    cur = newStyle[name]
    if (cur !== oldStyle[name]) {
      // ie9 setting to null has no effect, must use empty string
      setProp(el, name, cur == null ? '' : cur)
    }
  }
}
export default {
  create: updateStyle,
  update: updateStyle
}
复制代码


可以发现 style.js 暴露了含有 create 属性的对象,其值为 updateStyle,而 updateStyle 最终将 pushcbs.create 中,所以当我们遍历 cbs.create 中函数时就会调用 updateStyle 并传入节点数据 vnodeupdateStyle 则会取到 vnode.data 进行节点样式更新。


3. vnode创建到渲染流程梳理

到此,vnode 如何创建并渲染为真实节点的过程我们就梳理完了。过程比较复杂,所以我们来梳理一下。


  1. 组件创建或更新调用 updateComponent
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
复制代码
  1. vm._render() 生成组件的虚拟节点 vnode,其中 vnodechildren 属性包含了整个组件的虚拟节点。render 中实际调用了 createElement 创建 vnode
  2. 调用 vm._update 进行 __patch____patch__ 中的 vnode 是以整个组件为单元的,并非单个节点的 vnode
if (!prevVnode) {
  // initial render
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
  // updates
  vm.$el = vm.__patch__(prevVnode, vnode)
}
复制代码
  1. __patch__ 的实际定义来自 createPatchFunction,并传入了节点操作API nodeOps 及属性样式包括事件的更新处理函数 modules

export const patch: Function = createPatchFunction({ nodeOps, modules })
复制代码
  1. createPatchFunction 中会先将 modules 中的更新函数推入 cbs 再返回真正执行的 patch
let i, j
const cbs = {}
const { modules, nodeOps } = backend
for (i = 0; i < hooks.length; ++i) {
  cbs[hooks[i]] = []
  for (j = 0; j < modules.length; ++j) {
    if (isDef(modules[j][hooks[i]])) {
      cbs[hooks[i]].push(modules[j][hooks[i]])
    }
  }
}
return function patch() {}
复制代码
  1. 所以我们在 __patch__ 中实际调用 patch,在 patch 函数中调用 createElm 进行创建节点
// create new node
createElm(
  vnode,
  insertedVnodeQueue,
  // extremely rare edge case: do not insert if old element is in a
  // leaving transition. Only happens when combining transition +
  // keep-alive + HOCs. (#4590)
  oldElm._leaveCb ? null : parentElm,
  nodeOps.nextSibling(oldElm)
)
复制代码

  1. createElm 中调用 nodeOps.createElement 创建真实节点,并通过 createChildren 遍历虚拟子节点递归调用 createElm 进行创建真实子节点。之后再调用 invokeCreateHooks 更新节点属性及样式。注意此时节点还未插入页面中,但是真实节点已经创建好了,其中包括整个组件的根节点及子节点。所以我们最后再去调用 insert 将其插入页面中。

vnode.elm = vnode.ns
  ? nodeOps.createElementNS(vnode.ns, tag)
  : nodeOps.createElement(tag, vnode)
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
  invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
复制代码
  1. createElm 中再调用 invokeCreateHooks 更新节点属性及样式。invokeCreateHooks 实际将调用到 modules 中的各类型属性及样式的更新函数
if (isDef(data)) {
  invokeCreateHooks(vnode, insertedVnodeQueue)
}
复制代码

  1. 至此我们已经将虚拟节点创建了真实的节点,并且通过更新函数更新了所以样式属性及事件,这时再通过 insert 将其插入到页面中,完成渲染流程。

insert(parentElm, vnode.elm, refElm)
复制代码

我们再次强调下,patch 函数是针对整个组件级别的。我们在组件的 render 函数中会生成整个组件的虚拟节点对象 vnode。在 patch 中又会在 createElm 递归子节点实现子节点,孙子节点的创建及样式属性更新。所以最后再插入页面的真实节点是组件的根节点,但是我们已经为其创建好了其所有子节点及孙子节点。这时候我们其实就完成了一整个组件的渲染。

总结


本篇文章分析了 vnodevue 中的运用,包括 vnode创建 -> 映射为真实节点 -> 渲染 的完整流程。当然篇幅有限,之前在 render 中分析的 createElementpatch 的细节及 modules 中的更新函数我们没有逐个分析。所以我们后面会将 patchmodules 放到后面单独分析梳理。goods goods staduy day day up


相关文章
|
缓存 JavaScript 前端开发
【Vue2.0源码学习】虚拟DOM篇-Vue中的虚拟DOM
【Vue2.0源码学习】虚拟DOM篇-Vue中的虚拟DOM
55 0
|
3月前
|
JavaScript 前端开发
react学习(3)创建虚拟dom的两种方式
react学习(3)创建虚拟dom的两种方式
170 67
|
4月前
|
JavaScript 前端开发
React——虚拟DOM创建的两种方式【二】
React——虚拟DOM创建的两种方式【二】
34 0
|
7月前
|
JavaScript 算法 前端开发
Vue的虚拟DOM:Vue虚拟DOM的工作原理
【4月更文挑战第24天】Vue的虚拟DOM提升渲染性能,通过创建JavaScript对象树(虚拟DOM树)来跟踪DOM变化。当状态改变,Vue用新的虚拟DOM树与旧树对比(diff算法),找到最小DOM操作集合来更新真实DOM。优化策略包括减少状态变化、使用key属性和简化组件结构。理解虚拟DOM工作原理有助于Vue的性能优化。
|
JavaScript 算法 前端开发
vue响应式原理与虚拟DOM实现
在Vue中,我们可以使用data属性来定义组件的数据。这些数据可以在模板中使用,并且当这些数据发生变化时,相关的DOM元素也会自动更新。这个过程就是响应式系统的核心。data() {return {这个过程是如何实现的呢?接下来我们就来探讨Vue响应式系统的实现原理。Vue是一个非常强大、灵活的前端框架,其响应式系统和虚拟DOM实现是其核心功能之一。本文探讨了Vue响应式系统和虚拟DOM实现的原理及其底层实现。希望本文能对大家理解Vue的原理有所帮助。
110 0
|
7月前
|
JavaScript 算法
vue如何通过VNode渲染节点
vue如何通过VNode渲染节点
120 0
|
7月前
|
JavaScript 算法
Vue的虚拟DOM是什么?它的作用是什么?
Vue的虚拟DOM是什么?它的作用是什么?
198 1
|
7月前
|
JavaScript 前端开发 算法
【Vue原理解析】之虚拟DOM
Vue.js是一款流行的JavaScript框架,它采用了虚拟DOM(Virtual DOM)的概念来提高性能和开发效率。虚拟DOM是Vue.js的核心之一,它通过在内存中构建一个轻量级的DOM树来代替直接操作真实的DOM,从而减少了对真实DOM的操作次数,提高了页面渲染效率。本文将深入探讨Vue.js中虚拟DOM的作用、核心源码分析。
124 1
|
7月前
|
JavaScript 前端开发
【React学习】—虚拟DOM两种创建方式(二)
【React学习】—虚拟DOM两种创建方式(二)
|
JavaScript 算法
【Vue2.0源码学习】虚拟DOM篇-Vue中的DOM-更新子节点
【Vue2.0源码学习】虚拟DOM篇-Vue中的DOM-更新子节点
60 0