vue为什么要用虚拟dom机制_Vue 虚拟 dom

虚拟 dom 桥接着 view-model 模型和实际的 dom 节点树。首先 view-model 通常不只包含一个节点,而是一颗节点树,和实际的 dom 节点有一对多的关联。因此借助于虚拟 dom 节点,我们就可以与实际的 dom 节点产生一对一的关联。同时,view-model 保存着全量的数据,通过它不能直接对实际的 dom 节点树进行增量更新。有了同样是树结构的虚拟 dom 作为 view-model 和实际 dom 节点树的中继者,一方面我们就能将 view-model 中的数据、模板很好地反映到实际的 dom 节点树上;另一方面 view-model 增量数据更新可表现为虚拟 dom 节点树的前后差异,从而局部重绘实际的 dom 节点树。

1 整体流程

虚拟 dom 的核心操作分为三个步骤:对虚拟 dom 进行建模,并创建虚拟 dom;比较虚拟 dom 树的前后差异;将差异渲染到页面上。在 Vue 中,第二步和第三步在 patch 过程中合为一体。广义的虚拟 dom 还包含从模板解析出虚拟 dom 树的过程,本文针对的是 Vue 中虚拟 dom 的实现,这里仅指明由模板生成的渲染函数。至于解析模板的具体逻辑,笔者将在后续的文章加以分析。下面是 Vue 通过模板解析出的渲染函数。

// 由 src/compiler/codegen/index.js 中 generate 函数输出
// _s 函数:toString 方法
// _v 函数:创建文本节点
// _c 函数:创建标签节点
{
  render: `with (this) {
   return _c(
     'div', { attrs: { "id": "app" } },
     [
       _c(
         'h1', { staticStyle: { "color": "red" }, attrs: { "data-id": "1" } },
         [_v(_s(message))]
       )
     ]
   )
 }`
}

在上述代码中,有必要指明的是,_c 即 Vue 生命周期 - 创建实例 中的 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) 以及 vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)。参数 a 为 tag 节点的标签名,b 为 data 节点的数据,c 为 children 子节点,d 为 normalizationType;而 createElement 函数则用于创建 VNode。实际上,上述代码中的 render 渲染函数会被包裹成 vm.$options.render 高阶函数,最终在 vm._render 方法中以 vm.$options.render.call(vm._renderProxy, vm.$createElement) 的调用形式创建 VNode,即通过高阶函数传入 _c, _s, _v 等工具函数。

当通过 vm._render 获得 vnode (该 vnode 将包含从模板中解析到的父 vnode 节点信息)以后,Vue 会调用 vm._update 方法将 vnode 填入文档或进行重绘。vm._update 方法实际基于 vm.patch 方法完成组件的挂载或重绘,参考 Vue 生命周期 - 组件挂载及更新。下图是单个 Vue 组件在父模板中作为模板节点的解析过程。

206e562600d87af70825c17d9c7335f2.png

对于用户手动 new 出的 Vue 实例,完成挂载需要手动调用 $mount 方法,然后将该 Vue 实例关联的模板解析为渲染函数并完成渲染。这时 template 和 _render 方法一样都是 Vue 实例的一部分。当该组件模板 template 包含其他组件节点或原生节点等时,在渲染函数执行期间,注入的 vm._c 方法将会创建与该组件节点或原生节点相对应的 vnode。对于组件节点,在 patch 过程中通过 init 钩子创建关联的 Vue 实例。处理过程参见下文的 patch 一小节。

2 vnode 模型

c80735e0a85f619da5a5d1a35577d8ab.png

vnode 大致可以分为以下几类:

  • EmptyVNode:注释节点,对应原生的注释节点,text 属性有值 且 isComment 属性为真值。
  • TextVNode:文本节点,对应原生的文本节点,text 属性有值。
  • ElementVNode:元素节点,对应原生的元素节点,elm 属性在渲染或重绘后有值。
  • ComponentVNode:组件节点,elm 属性在渲染或重绘后有值,包含类组件、函数式组件、异步组件等。
  • CloneVNode:拷贝节点,isCloned 属性为真值,以便于复用。

3 创建 vnode

a9c849d217883768fa4ece39d84def3a.png

由上文可以看出,在创建 vnode 的过程中,将调用 createElement 函数生成 vnode 的一般属性,包含 tag, data, children, text, ns, context, key。创建 vnode 有三种方式:对于内置的 html 节点,直接创建 VNode 实例;对于 Vue 组件构成的模板节点,通过 createComponent 创建 VNode 实例;对于 tag 不可识别或为对象的其他节点,同样通过 createComponent 创建 VNode 实例。

3.1 createElement

在 Vue 中,createElement 函数的功能主要由 _createElement 实现。createElement 的特殊意义是对参数进行处理,余下的过程都由 _createElement 完成。createElement 通过 vm._c 注入到模板渲染函数中;当渲染函数执行过程中,可用于创建 vnode 节点树。createElement 本身的实现也较为简单:针对模板中的原生节点,直接使用 new VNode 创建 vnode 实例;针对模板中的组件节点或其他,调用 createComponent 创建 vnode 实例。以下是 _createElement 函数的扼要实现:

// createElement 函数处理参数,并调用 _createElement 创建 vnode
function _createElement (
  context: Component,// 模板对应的 vm 实例
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // v-bind:is 或 is 属性的存在,意为动态组件
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }

  // 插槽支持单个函数作为子组件
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }

  if (normalizationType === ALWAYS_NORMALIZE) {
    // normalizeChildren 函数意义为:
    // 如果 children 是原始类型,字符串、数值、symbol或布尔值,使用 createTextVNode 创建文本节点
    // 如果 children 是数组,递归处理,文本节点聚合,参考 normalizeArrayChildren 函数
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // simpleNormalizeChildren 如遇到数组,不作递归处理,只是简单地拼接
    children = simpleNormalizeChildren(children)
  }

  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    // config 根据不同环境有不同实现
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 针对环境支持的 html 节点标签
    if (config.isReservedTag(tag)) {
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    // resolveAsset 从 context.$options.components 获取指定的组件构造器
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    vnode = createComponent(tag, data, context, children)
  }

  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)// 将 ns 写入 vnode
    // registerDeepBindings 使用 observer 包提供的 traverse 函数递归地调用响应式数据 data 的 getter,绑定依赖
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

3.2 createComponent

createComponent 函数意义在于创建不同类型的组件:异步组件、函数式组件、类组件等。

function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  const baseCtor = context.$options._base// Vue 构造函数

  if (isObject(Ctor)) {// 当 Ctor 为对象,构造 Vue 的子类
    Ctor = baseCtor.extend(Ctor)
  }

  // 处理异步组件
  let asyncFactory
  if (isUndef(Ctor.cid)) {// Ctor 异步组件形式
    asyncFactory = Ctor
    // 异步组件加载过程中,Ctor 为 undefined;加载完成,Ctor 为待渲染的组件
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    // 异步组件加载过程中,创建异步占位符
    if (Ctor === undefined) {
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // 更新类所携带的选项
  resolveConstructorOptions(Ctor)

  // 根据 Ctor.options.model 更新 data 中的指定数据以及绑定事件
  // model 选项的意义就在于双向绑定,可包含的属性有 prop, event, callback
  // prop 指定模板和响应式数据交互的 key;event, callback 指定事件和绑定函数
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // 根据 Ctor.options.props 解析出 data.attrs, data.props(父组件传入的 props)
  // options.props 指定子组件接受数据的 key 键
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // 处理函数式组件
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  const listeners = data.on// dom 事件
  data.on = data.nativeOn

  // 抽象组件,data 中仅保留 slot
  if (isTrue(Ctor.options.abstract)) {
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // 将 componentVNodeHooks 钩子灌入到 data.hook 中
  installComponentHooks(data)

  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },// 作为 componentOptions
    asyncFactory
  )

  return vnode
}

3.2.1 函数式组件

函数式组件 对应 React 中的无状态组件。不同于类组件以 vm 实例作为上下文,函数式组件在生成过程中将构建新的上下文对象,因此没有对应的 vm 实例,也就没有响应的生命周期和响应式数据。

函数式组件所对应的 vnode 实例包含特殊属性如 fnContext, fnOptions, fnScopeId。fnContext 即模板所对应的 vm 实例;fnOptions 即 vm 构造函数所划定的选项;fnScopeId 即选项中包含的作用域 id。

// 创建函数式-无状态组件
function createFunctionalComponent (
  Ctor: Class<Component>,
  propsData: ?Object,
  data: VNodeData,
  contextVm: Component,
  children: ?Array<VNode>
): VNode | Array<VNode> | void {
  const options = Ctor.options
  const props = {}
  const propOptions = options.props
  if (isDef(propOptions)) {
    for (const key in propOptions) {
      props[key] = validateProp(key, propOptions, propsData || emptyObject)
    }
  } else {
    if (isDef(data.attrs)) mergeProps(props, data.attrs)
    if (isDef(data.props)) mergeProps(props, data.props)
  }

  // 构建新的上下文对象,对应类组件的 vm 实例
  const renderContext = new FunctionalRenderContext(
    data,
    props,
    children,
    contextVm,
    Ctor
  )

  // 没有使用 vm 实例作为上下文,因此没有生命周期和响应式数据
  // renderContext._c 即上文中的 createElement
  const vnode = options.render.call(null, renderContext._c, renderContext)

  if (vnode instanceof VNode) {
    return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext)
  } else if (Array.isArray(vnode)) {
    const vnodes = normalizeChildren(vnode) || []
    const res = new Array(vnodes.length)
    for (let i = 0; i < vnodes.length; i++) {
      res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext)
    }
    return res
  }
}

function FunctionalRenderContext (
  data: VNodeData,
  props: Object,
  children: ?Array<VNode>,
  parent: Component,
  Ctor: Class<Component>
) {
  const options = Ctor.options
  let contextVm
  if (hasOwn(parent, '_uid')) {
    contextVm = Object.create(parent)
    contextVm._original = parent
  } else {
    contextVm = parent
    parent = parent._original
  }
  const isCompiled = isTrue(options._compiled)
  const needNormalization = !isCompiled

  this.data = data
  this.props = props
  this.children = children
  this.parent = parent
  this.listeners = data.on || emptyObject
  this.injections = resolveInject(options.inject, parent)
  this.slots = () => {
    if (!this.$slots) {
      normalizeScopedSlots(
        data.scopedSlots,
        this.$slots = resolveSlots(children, parent)
      )
    }
    return this.$slots
  }

  Object.defineProperty(this, 'scopedSlots', ({
    enumerable: true,
    get () {
      return normalizeScopedSlots(data.scopedSlots, this.slots())
    }
  }: any))

  if (isCompiled) {
    this.$options = options
    this.$slots = this.slots()
    this.$scopedSlots = normalizeScopedSlots(data.scopedSlots, this.$slots)
  }

  if (options._scopeId) {
    this._c = (a, b, c, d) => {
      const vnode = createElement(contextVm, a, b, c, d, needNormalization)
      if (vnode && !Array.isArray(vnode)) {
        vnode.fnScopeId = options._scopeId
        vnode.fnContext = parent
      }
      return vnode
    }
  } else {
    this._c = (a, b, c, d) => createElement(contextVm, a, b, c, d, needNormalization)
  }
}

installRenderHelpers(FunctionalRenderContext.prototype)

// 拷贝 vnode,以便于根据 isCloned 属性判断是否拷贝节点,如是,可复用
function cloneAndMarkFunctionalResult (vnode, data, contextVm, options, renderContext) {
  const clone = cloneVNode(vnode)
  clone.fnContext = contextVm
  clone.fnOptions = options
  if (process.env.NODE_ENV !== 'production') {
    (clone.devtoolsMeta = clone.devtoolsMeta || {}).renderContext = renderContext
  }
  if (data.slot) {
    (clone.data || (clone.data = {})).slot = data.slot
  }
  return clone
}

3.2.2 异步组件

异步组件 通常是 asyncFactory = (resolve, reject) => {} 或 () => ({ component }) 函数注册的组件。异步组件主要由 resolveAsyncComponent 函数处理其加载逻辑。当异步组件在加载过程中,Vue 会使用 createAsyncPlaceholder 创建占位节点。当异步组件加载完成后,Vue 会通过强制重绘父组件的方式,启动该异步组件的渲染过程。实际上,在加载完成后,Vue 会将该异步组件填充到 asyncFactory.resolved 属性中。因此,若有其他组件需要渲染该异步组件时,直接取出 asyncFactory.resolved 作为 vm 实例的构造器并完成渲染即可。

回溯上文的 vnode 模型,其中的 asyncFactory, asyncMeta 属性实际只存在于占位节点中。

// 异步组件的渲染机制
// 初次加载异步组件,二次以后直接消费已加载的异步组件
function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>
): Class<Component> | void {
  // 当异步组件加载完成再次被使用时,factory.errorComp, factory.resolved 等属性可能填满了值
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }

  if (isDef(factory.resolved)) {
    return factory.resolved
  }

  const owner = currentRenderingInstance// 当前渲染的组件实例,作为异步组件的父组件
  // factory.owners 记录异步组件的所有父组件,通过强制更新父组件渲染异步组件
  if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
    factory.owners.push(owner)
  }

  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }

  if (owner && !isDef(factory.owners)) {
    const owners = factory.owners = [owner]
    let sync = true
    let timerLoading = null
    let timerTimeout = null

    ;(owner: any).$on('hook:destroyed', () => remove(owners, owner))

    // 通过强制重绘父组件完成异步组件的渲染
    const forceRender = (renderCompleted: boolean) => {
      for (let i = 0, l = owners.length; i < l; i++) {
        (owners[i]: any).$forceUpdate()
      }

      if (renderCompleted) {
        owners.length = 0
        if (timerLoading !== null) {
          clearTimeout(timerLoading)
          timerLoading = null
        }
        if (timerTimeout !== null) {
          clearTimeout(timerTimeout)
          timerTimeout = null
        }
      }
    }

    const resolve = once((res: Object | Class<Component>) => {
      // factory.resolved 即 import 加载的组件,或者通过后端返回数据构建的 Vue 子类
      factory.resolved = ensureCtor(res, baseCtor)
      if (!sync) {
        forceRender(true)
      } else {
        owners.length = 0
      }
    })

    const reject = once(reason => {
      process.env.NODE_ENV !== 'production' && warn(
        `Failed to resolve async component: ${String(factory)}` +
        (reason ? `nReason: ${reason}` : '')
      )
      if (isDef(factory.errorComp)) {
        factory.error = true// 置为真值,以便在重绘时渲染 factory.errorComp
        forceRender(true)
      }
    })

    const res = factory(resolve, reject)// 加载异步组件

    if (isObject(res)) {
      if (isPromise(res)) {
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject)
        }
      } else if (isPromise(res.component)) {
        res.component.then(resolve, reject)

        if (isDef(res.error)) {
          // 构建 factory.errorComp,以便二次渲染
          factory.errorComp = ensureCtor(res.error, baseCtor)
        }

        if (isDef(res.loading)) {
          factory.loadingComp = ensureCtor(res.loading, baseCtor)
          if (res.delay === 0) {
            factory.loading = true
          } else {
            timerLoading = setTimeout(() => {
              timerLoading = null
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                factory.loading = true
                forceRender(false)
              }
            }, res.delay || 200)
          }
        }

        if (isDef(res.timeout)) {
          timerTimeout = setTimeout(() => {
            timerTimeout = null
            if (isUndef(factory.resolved)) {
              reject(
                process.env.NODE_ENV !== 'production'
                  ? `timeout (${res.timeout}ms)`
                  : null
              )
            }
          }, res.timeout)
        }
      }
    }

    sync = false
    // 异步组件已加载完成,返回待渲染的组件
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}

// 创建占位节点
createAsyncPlaceholder (
  factory: Function,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag: ?string
): VNode {
  const node = createEmptyVNode()
  node.asyncFactory = factory
  node.asyncMeta = { data, context, children, tag }
  return node
}

4 渲染 vnode

依据上文,vnode 需要通过 vm.__patch__ 方法完成渲染或重绘出真实 dom。vm.__patch__ 方法的创建过程是基于高阶函数 createPatchFunction,首先将不同平台的 dom 操作作为参数传入 createPatchFunction,然后生成实际针对不同平台的 patch 函数。在 createPatchFunction(backend) 函数中,backend 即不同平台中以钩子形式提供的 dom 操作函数集。

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

// 将平台钩子注入到 createPatchFunction 函数中,以便调用
export function createPatchFunction (backend) {
  const cbs = {}
  const { modules, nodeOps } = backend// 不同环境的节点、样式、事件等操作

  // 从 modules 中读取平台钩子(可能是浏览器钩子),以钩子形式更新节点的属性、事件
  // modules 即包含 class, style, attrs, events, domProps, transition
  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 (oldVnode, vnode, hydrating, removeOnly) {
    // ...
  }
}

因此,下面以浏览器钩子展开说明。

4.1 浏览器钩子

在浏览器环境中,钩子函数集合包含 class 样式类、style 样式、attrs 节点属性、events 事件、domProps 节点内容等、transition 动效六类操作。

  • class.js: 提供 create, update 钩子,使用 el.setAttribute('class', cls) 方法设置或更新 vnode.elm 元素的样式类。
  • style.js: 提供 create, update 钩子,使用 el.style.setProperty(name, val) 或 el.style[name] = val 形式设置或更新 vnode.elm 元素的样式。
  • attrs.js: 提供 create, update 钩子,使用 el.setAttribute(key, value) 或 el.removeAttribute(key) 方法设置或更新 vnode.elm 元素的 html 属性。
  • events.js: 提供 create, update 钩子,使用 el.addEventListener, el.removeEventListener 方法设置或更新 vnode.elm 元素的绑定函数。
  • domProps.js: 提供 create, update 钩子,包含:当 vnode.data.domProps 属性包含 textContent, innerHTML 时,使用 el.removeChild 移除子节点,并将值写入 el 属性中等。
  • transition.js: 提供 create, activate, remove 钩子,用于实现 css transition,这里不作解读。

钩子的种类以及执行时机为:

  • create: 创建真实 dom 节点时。
  • activate: transition 过程中动效执行时。
  • update: 更新真实 dom 节点时。
  • remove: transition 过程中移除节点时。
  • destroy: 移除真实 dom 节点时,即组件卸载或 keep-alive 子组件置为非激活状态时。

4.2 vnode 钩子

除了平台钩子以外,patch 执行过程中还包含 vnode 钩子。通过上文也可以发现,Vue 在执行 createComponent 函数时,会通过调用 installComponentHooks 函数将 vnode 钩子注入到 vnode.data.hook 中。

  • init 钩子: 如果 vnode 对应未销毁状态的 keep-alive 组件,更新该 keep-alive 组件;如果 vnode 对应组件节点,实例化该组件,使用该组件的内置模板完成渲染,生成 vnode.elm。执行时机为更新组件时。
  • prepatch 钩子: 更新组件。执行时机为更新组件时。
  • insert 钩子: 将完成挂载或更新的 vnode 节点插入到父节点中,且调用组件的 mounted 生命周期。执行时机为组件所对应的节点树都渲染到文档中时。
  • destroy 钩子: 卸载组件,或者将 keep-alive 子组件置为非激活状态。执行时机为 vnode 节点移除时。
const componentVNodeHooks = {
  // 更新 keep-alive 组件,或实例化模板中的子节点组件并完成挂载
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    // keep-alive 组件,复用 vnode,并对其追加补丁
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      const mountedNode: any = vnode 
      componentVNodeHooks.prepatch(mountedNode, mountedNode)

    // 如模板节点为组件节点,实例化该组件,并予以挂载
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      // vm.$mount 方法通过 vm._update 挂载组件
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  // 更新组件
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance
    // 更新组件 vm 实例的属性,并酌情重绘
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

  // 当组件所对应的 vnode 树都渲染到文档中时,调用 mounted 生命周期
  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    // 调用组件的 mounted 生命周期
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // 缓存 vm 实例,直到 patch 过程结束才予以激活
        queueActivatedComponent(componentInstance)
      } else {
        // 直接激活 vm 实例
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },

  // 启动卸载、或将 keep-alive 组件置为未激活状态
  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}

4.3 钩子触发器

Vue 封装了如下的钩子触发器:

// 触发 create 平台钩子以及 vnode 钩子
function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode)
  }
  i = vnode.data.hook // Reuse variable
  if (isDef(i)) {
    if (isDef(i.create)) i.create(emptyNode, vnode)
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  }
}

// vnode 树中的子节点须等待组件内容都渲染到文档中时,才执行 insert 钩子
function invokeInsertHook (vnode, queue, initial) {
  // 根组件中的子节点须等待跟组件实际渲染到文档中
  if (isTrue(initial) && isDef(vnode.parent)) {
    vnode.parent.data.pendingInsert = queue
  } else {
    for (let i = 0; i < queue.length; ++i) {
      queue[i].data.hook.insert(queue[i])
    }
  }
}

// 递归销毁组件、移除 vnode 节点属性
function invokeDestroyHook (vnode) {
  let i, j
  const data = vnode.data
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
    for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
  }
  if (isDef(i = vnode.children)) {
    for (j = 0; j < vnode.children.length; ++j) {
      invokeDestroyHook(vnode.children[j])
    }
  }
}

4.4 patch

vnode 的 patch 过程就是完成挂载、重绘或卸载。patch 过程中渲染的直接表现是生成 vnode.elm 或 vnode.text。

fc4e08f85e0c9f3d8edd271ea8e6c752.png
function patch (oldVnode, vnode, hydrating, removeOnly) {
  // 组件卸载
  if (isUndef(vnode)) {
    // 深度优先遍历 oldVnode 树中子节点,执行 cbs.destory 以及 vnode.data.hook.destory 钩子
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }

  let isInitialPatch = false
  const insertedVnodeQueue = []

  // 组件挂载
  if (isUndef(oldVnode)) {
    isInitialPatch = true
    // createElm 函数将 vnode 及 children 解析成节点树 vnode.elm
    createElm(vnode, insertedVnodeQueue)

  // 组件更新
  } else {
    const isRealElement = isDef(oldVnode.nodeType)// 原生节点

    // 组件节点的数据发生变更,更新组件
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patchVnode 函数更新组件节点或原生节点
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      if (isRealElement) {
        // 服务端渲染的是元素节点,采用 hydrate 函数进行渲染,oldVnode 即真实的 dom 元素
        if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
          oldVnode.removeAttribute(SSR_ATTR)
          hydrating = true
        }
        if (isTrue(hydrating)) {
          // hydrate 函数将 oldVnode 作为 vnode 的 elm 属性,并递归处理子节点等
          // 当为 component 模板节点或原生节点、文本节点、注释节点时返回真值,否则返回否值
          if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
            // invokeInsertHook 函数对于根节点,将 insertedVnodeQueue 存入 vnode.parent.data.pendingInsert 中,等待调用 initComponent 时执行
            // 其他,触发 insertedVnodeQueue 数组项中的 data.hook.insert 钩子
            invokeInsertHook(vnode, insertedVnodeQueue, true)
            return oldVnode// 真实的 dom 节点
          } else if (process.env.NODE_ENV !== 'production') {
            warn(
              'The client-side rendered virtual DOM tree is not matching ' +
              'server-rendered content. This is likely caused by incorrect ' +
              'HTML markup, for example nesting block-level elements inside ' +
              '<p>, or missing <tbody>. Bailing hydration and performing ' +
              'full client-side render.'
            )
          }
        }
        // emptyNodeAt 函数构建一个 VNode 实例,该实例使用 oldVnode 作为 elm 属性
        oldVnode = emptyNodeAt(oldVnode)
      }

      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)

      createElm(
        vnode,
        insertedVnodeQueue,
        oldElm._leaveCb ? null : parentElm,// 在执行 leave 动效时不将 vnode 插入到 parentElm 中
        nodeOps.nextSibling(oldElm)// 使用兄弟节点锁定插入 parentElm 时的位置
      )

      // vnode.parent 查找模板节点
      if (isDef(vnode.parent)) {
        let ancestor = vnode.parent
        // isPatchable 函数向上寻找非 keep-alive 组件,且须判断该组件对应的 vnode.tag 为已定义
        // patchable 意味着 keep-alive 组件下复用已缓存的某子组件
        const patchable = isPatchable(vnode)
        while (ancestor) {
          for (let i = 0; i < cbs.destroy.length; ++i) {
            cbs.destroy[i](ancestor)
          }
          ancestor.elm = vnode.elm

          // 渲染已缓存的某子组件
          if (patchable) {
            for (let i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, ancestor)
            }

            const insert = ancestor.data.hook.insert
            if (insert.merged) {
              for (let i = 1; i < insert.fns.length; i++) {// i 从 1 起始,避免执行组件的 mounted 钩子
                insert.fns[i]()
              }
            }
          } else {
            // registerRef 函数为外层组件实例 ancestor.context 设置 ref 引用
            registerRef(ancestor)
          }
          ancestor = ancestor.parent
        }
      }

      if (isDef(parentElm)) {
        // 移除原始节点
        removeVnodes(parentElm, [oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        // 执行 destroy 平台钩子以及 vnode 钩子
        invokeDestroyHook(oldVnode)
      }
    }
  }

  // 只针对 createElm, hybrate 收集到插入情形
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

5 参考

Vue原理解析之Virtual Dom

patch

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值