欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

vue2.x源码解析六——组件化--3.patch(将虚拟DOM映射为真实DOM)

程序员文章站 2022-05-13 20:59:49
...

1.patch

通过vue2.x源码解析六——数据驱动,当我们通过 createComponent 创建了组件 VNode,接下来会走到

vm._update —> vm.patch –> patch 方法,

去把 VNode 转换成真正的 DOM 节点。

这个过程我们在前一章已经分析过一个普通的 VNode 节点的path过程,但是针对的是一个普通的 VNode 节点,接下来我们来看看组件的 VNode 会有哪些不一样的地方。

patch 的过程会调用 createElm 创建元素节点实现虚拟DOM映射为真实DOM(要注意区分,render也就是参数虚拟DOM用的是createElement,映射为真实DOM createElm),回顾一下 createElm 的实现,它的定义在 src/core/vdom/patch.js 中:

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // ...
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  // ...
}

2.createComponent

我们删掉多余的代码,只保留关键的逻辑,上面的代码会判断 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值,如果为 true 则直接结束。

createComponent其实调用的就是给逐渐VNode添加的init方法

那么接下来看一下 createComponent 方法的实现:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    // 判断vnode.data中是否有hook,并且有init方法(因为上一节讲了会给组件merage一些钩子,其中就有init,所以这里是trueif (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
    // after calling the init hook, if the vnode is a child component
    // it should've created a child instance and mounted it. the child
    // component also has set the placeholder vnode's elm.
    // in that case we can just return the element and be done.
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

2.1 对vnode.data 做了一些判断

let i = vnode.data
if (isDef(i)) {
  // ...
  if (isDef(i = i.hook) && isDef(i = i.init)) {
    i(vnode, false /* hydrating */)
    // ...
  }
  // ..
}
  1. vnode 是一个组件 VNode,那么条件会满足,并且得到 i 就是 init 钩子函数

  2. 判断vnode.data中是否有hook,并且有init方法(因为上一节讲了会给组件VNode merage一些钩子,其中就有init,所以这里是true),就会调用init方法

2.2 init方法

init方法定义在 src/core/vdom/create-component.js 中:

其实就是和组件的data.hook钩子合并的 componentVNodeHooks 钩子对象的init方法

const componentVNodeHooks = {
       init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
        if (
          vnode.componentInstance &&
          !vnode.componentInstance._isDestroyed &&
          vnode.data.keepAlive
        ) {
          // kept-alive components, treat as a patch
          const mountedNode: any = vnode // work around flow
          componentVNodeHooks.prepatch(mountedNode, mountedNode)
        } else {
            //child是一个vnode实例
          const child = vnode.componentInstance = createComponentInstanceForVnode(
            vnode,
            activeInstance
          )
          //调用 $mount 方法挂载子组件
          child.$mount(hydrating ? vnode.elm : undefined, hydrating)
        }
      },

     prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
     },

     insert (vnode: MountedComponentVNode) {
     },

     destroy (vnode: MountedComponentVNode) {
     }
}

init 钩子函数执行也很简单,我们先不考虑 keepAlive 的情况,它是通过 createComponentInstanceForVnode 创建一个 vnode 的实例,然后调用 $mount 方法挂载子组件

2.3 createComponentInstanceForVnode方法

创建一个 vnode 的实例
src/core/vdom/create-component.js

export function createComponentInstanceForVnode (
  vnode: any, // 组件VNode
  parent: any, // 当前vue实例vm
): Component { 
    // 定义参数
  const options: InternalComponentOptions = {
    _isComponent: true,
    // 父VNode,是一个占位节点,Vue实例A调用B组件,B调用C组件,_parentVnode就是C组件占位符
    _parentVnode: vnode, 
   //表示当前**的子组件的父级实例,例如 app =new Vue,并调用子组件,parent就是app
    parent 
  }
     ...

  // 由上一节我们知道组件会生成子构造器,vnode.componentOptions.Ctor 对应的就是子组件的构造函数
  return new vnode.componentOptions.Ctor(options)
}
  1. createComponentInstanceForVnode 函数构造的一个内部组件的参数,然后执行 new
    vnode.componentOptions.Ctor(options)。

  2. 上一节我们知道组件会生成子构造器,vnode.componentOptions.Ctor 对应的就是子组件的构造函数,

  3. 我们上一节分析了子构造器实际上是继承于 Vue 的一个构造器 Sub,相当于 new Sub(options)

  4. 这里有几个关键参数要注意几个点,_isComponent 为 true 表示它是一个组件,parent 表示当前**的组件实例。

因为上面执行了 new Sub(options),而Sub定义在
src/core/global-api/exten.js

 const Sub = function VueComponent (options) {
    this._init(options)cd
   }

所以子组件的实例化实际上就是在这个时机执行的,并且它会执行实例的 _init 方法,这个过程有一些和之前不同的地方需要挑出来说,代码在 src/core/instance/init.js 中:

2.4普通VNode 节点和组件的 VNode 初始化的不同。

他们都是在src/core/instance/init.js中初始化的,只是有一些不同,我们主要来看不同的

代码在 src/core/instance/init.js 中:

Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  // merge options
  if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  // ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  } 
}

1. 合并 options 的过程有变化

合并 options 的过程有变化,_isComponent 为 true,所以走到了 initInternalComponent 过程,,而上一章普通的 VNode 节点则会走else。

的这个函数的实现也在当前页面 src/core/instance/init.js

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

将2.3节中 createComponentInstanceForVnode 函数定义的参数 InternalComponentOptions 传入

 const options: InternalComponentOptions = {
    _isComponent: true,
    // 父VNode,是一个占位节点,Vue实例A调用B组件,B调用C组件,_parentVnode就是C组件占位符
    _parentVnode: vnode, 
   //表示当前**的子组件的父级实例,例如 app =new Vue,并调用子组件,parent就是app
    parent

  }

并且赋值个给opts.parent 和 opts._parentVnode ,它们是把之前我们通过 createComponentInstanceForVnode 函数传入的几个参数合并到内部的选项 $options 里了。

2. 初始化生命周期的不同

在init.js中,普通VNode 节点和组件的 VNode 都会初始化一些函数

    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

我们来看组件的 VNode的initLifecycle
步骤1.
首先定义了全局变量activeInstance
activeInstance 作用就是保持当前上下文的 Vue 实例,并且在之前我们调用 createComponentInstanceForVnode 方法的时候从 lifecycle 模块获取,并且作为参数传入的

export let activeInstance: any = null

步骤2. activeInstance的赋值
但我们调用update的时候,也就是我们前面讲过的渲染观察者watcher检测发现变化时调用的update去渲染VNode。具体下面讲挂载的时候会将

 Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    // 用prevActiveInstance去记录上一个activeInstance,也就是上一个vm实例
    const prevActiveInstance = activeInstance
    // 将当前的vm实例赋值给activeInstance
    activeInstance = vm
    vm._vnode = vnode
    。。。
  }

将当前的vm实例赋值给
会在当前这个vm的vnode的patch过程中,将当前这个vue实例vm作为父vue实例传给子组件
真个patch过程其实是一个深度遍历的过程

步骤3.调用initLifecycle

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  // ...
}

可以看到 vm.$parent 就是用来保留当前 vm 的父实例。

parent.$children.push(vm) 来把当前的 vm 存储到父实例的 children 中。

3.挂载

普通VNode 节点和组件的 VNode 初始化的类似,显示合并options,然后初始化一些函数,最后挂载到DOM。
在挂载的时候普通VNode 节点和组件的 VNode是不同的。
在初始化函数init.js的最后

if (vm.$options.el) {
   vm.$mount(vm.$options.el)
}

普通VNode 节点会走这里,而组件的 VNode并不会走这里。而是会结束这个 init方法,回到钩子中定义的init钩子中。

init钩子定义在 src/core/vdom/create-component.js 中:

其实就是和组件的data.hook钩子合并的 componentVNodeHooks 钩子对象的init钩子

const componentVNodeHooks = {
       init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
        if (
          vnode.componentInstance &&
          !vnode.componentInstance._isDestroyed &&
          vnode.data.keepAlive
        ) {
          // kept-alive components, treat as a patch
          const mountedNode: any = vnode // work around flow
          componentVNodeHooks.prepatch(mountedNode, mountedNode)
        } else {
            //child是一个vnode实例
          const child = vnode.componentInstance = createComponentInstanceForVnode(
            vnode,
            activeInstance
          )
          //调用 $mount 方法挂载子组件
          child.$mount(hydrating ? vnode.elm : undefined, hydrating)
        }
      },

     prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
     },

     insert (vnode: MountedComponentVNode) {
     },

     destroy (vnode: MountedComponentVNode) {
     }
}

createComponentInstanceForVnode 创建一个 vnode 的实例,然后调用 $mount 方法挂载子组件,其实就是直接调用Vue.prototype下的方法去挂载。

3.1 Vue.prototype.$mount

该方法定义在src/platform/web/runtime/index.js :
介绍vue实例挂载的时候讲过

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

又去调用的mountComponent方法

3.1 mountComponent

$mount 方法实际上会去调用 mountComponent 方法,该方法定义在:
src/core/instance/lifecycle.js :

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
   if (!vm.$options.render) {
        vm.$options.render = createEmptyVNode
        。。。
   }
  ...
  callHook(vm, 'beforeMount')

  let updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  vm._watcher = new Watcher(vm, updateComponent, noop)
  hydrating = false

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

mountComponent 核心就是先调用 vm._render 方法先生成虚拟 Node,再实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,最终调用 vm._update 更新 DOM。

3.2 vm._render() 方法

Vue 的 _render 方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node。 src/core/instance/render.js :

Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options


  // set parent vnode. this allows render functions to have access
  // to the data on the placeholder node.
  vm.$vnode = _parentVnode
  // render self
  let vnode
  try {
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    // ...
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}

我们只保留关键部分的代码,这里的 _parentVnode 就是当前组件的父 VNode,而 render 函数生成的 vnode 当前组件的渲染 vnode,vnode 的 parent 指向了 _parentVnode,也就是 vm.$vnode,它们是一种父子的关系。

执行完 vm._render 生成 VNode 后,接下来就要执行 vm._update 去渲染 VNode 了

3.4 vm._update

渲染 VNode
src/core/instance/lifecycle.js

export let activeInstance: any = null
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const prevActiveInstance = activeInstance
  activeInstance = vm
  // vnode 是通过 vm._render() 返回的组件渲染 VNode
  vm._vnode = vnode
  if (!prevVnode) {
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  activeInstance = prevActiveInstance
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
}

_update 过程中有几个关键的代码,

  • vm._vnode = vnode 的逻辑,这个 vnode 是通过 vm._render() 返回的组件渲染 VNode,

  • vm._vnode 和 vm.$vnode 的关系就是一种父子关系,

  • 父子关系用代码表达就是 vm._vnode.parent === vm.$vnode

我们在 3.2 节讲了深度遍历的过程,我们在这里再说一下。

1.activeInstance 作用就是保持当前上下文的 Vue 实例,它是在 lifecycle 模块的全局变量,在之前我们调用 createComponentInstanceForVnode 方法的时候从 lifecycle 模块获取,并且作为参数传入的。

2.因为实际上 JavaScript 是一个单线程,Vue 整个初始化是一个深度遍历的过程,在实例化子组件的过程中,它需要知道当前上下文的 Vue 实例是什么,并把它作为子组件的父 Vue 实例。

3.对子组件的实例化过程(以为就是子组件调用init方法),先会调用 initInternalComponent(vm, options) 合并 options,把 parent 存储在 vm.$options 中,再调用 initLifecycle(vm) 方法

当前js的initLifecycle方法

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  // ...
}
  1. 可以看到 vm.$parent 就是用来保留当前 vm 的父实例。
  2. 通过 parent.children.push(vm)vmchildren 中。

总结:

在 vm._update 的过程中,把当前的 vm 赋值给 activeInstance,同时通过 const prevActiveInstance = activeInstance 用 prevActiveInstance 保留上一次的 activeInstance。实际上,prevActiveInstance 和当前的 vm 是一个父子关系,当一个 vm 实例完成它的所有子树的 patch 或者 update 过程后,activeInstance 会回到它的父实例,这样就完美地保证了 createComponentInstanceForVnode 整个深度遍历过程中,我们在实例化子组件的时候能传入当前子组件的父 Vue 实例,并在 _init 的过程中,通过 vm.$parent 把这个父子关系保留。

_update,最后就是调用 patch 渲染 VNode 了。

3.5 vm.patch

在src/core/vdom/patch.js

vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

function patch (oldVnode, vnode, hydrating, removeOnly) {
  // ...
  let isInitialPatch = false
  const insertedVnodeQueue = []

    // 组件渲染会走到这里
  if (isUndef(oldVnode)) {
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    // ...
  }
  // ...
}

此时,因为我么你是子组件的的patch,所以vm.$el为空,那么patch方法的oldVnode就是空。
这是就会走负责渲染成 DOM 的函数是 createElm,注意这里我们只传了 2 个参数,所以对应的 parentElm 是 undefined

3.5 vm. _ patch _

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // ...
  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(tag, vnode)
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
      // ...
    } else {
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      insert(parentElm, vnode.elm, refElm)
    }

    // ...
  } else if (isTrue(vnode.isComment)) {
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

这里我们传入的 vnode 是组件渲染的 vnode,也就是我们之前说的 vm._vnode,也就是子组件,如果组件的根节点是个普通元素,那么 vm._vnode 也是普通的 vnode,这里 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值是 false。
接下来的过程就和我们上一章一样了,先创建一个父节点占位符,然后再遍历所有子 VNode 递归调用 createElm,在遍历的过程中,如果遇到子 VNode 是一个组件的 VNode,则重复本节开始的过程,这样通过一个递归的方式就可以完整地构建了整个组件树。

由于我们这个时候传入的 parentElm 是空,所以对组件的插入,在 createComponent 有这么一段逻辑:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    // ....
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
    // ...
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

在完成组件的整个 patch 过程后,最后执行 insert(parentElm, vnode.elm, refElm) 完成组件的 DOM 插入,如果组件 patch 过程中又创建了子组件,那么DOM 的插入顺序是先子后父。