手撕Vue源码之组件化中的patch函数

patch

我们之前有分析得出createComponent创建了组件VNode,接下来我们来看一下vm._ update,执行vm._ patch_ 去吧VNode转换成真正的DOM节点。这个过程在之前的virtualDOM也有说过,但是针对一个普通的VNode节点,我们来看看VNode会有哪些不一样的地方。

patch的过程会调用createElm创建元素节点,回顾一下createElm的实现,他定义在src/core/vdom/patch.js中:

  function  createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(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)) {
      if (process.env.NODE_ENV !== 'production') {
        if (data && data.pre) {
          creatingElmInVPre++
        }
        if (isUnknownElement(vnode, creatingElmInVPre)) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          )
        }
      }

      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)

      /* istanbul ignore if */
      if (__WEEX__) {
        // in Weex, the default insertion order is parent-first.
        // List items can be optimized to use children-first insertion
        // with append="tree".
        const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
        if (!appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
        createChildren(vnode, children, insertedVnodeQueue)
        if (appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
      } else {
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        creatingElmInVPre--
      }
    } 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)
    }
  }
复制代码

createComponent

删掉多余的代码,保留关键逻辑,这里会判断createComponent(vnode,insertedVnodeQueue,parenElm,refElm)的返回值如果为true则世界结束,那么接下来看一下createComponent方法的实现:

  function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (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
      }
    }
  }
复制代码

createComponent函数中,首先对vnode.data做了一些判断:

    let i = vnode.data
    if (isDef(i)) {
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false /* hydrating */)
      }
    }
复制代码

如果vnode是一个组件VNode,那么条件会满足,并且得到i就是init钩子函数,之前看的init函数定义在src/core/vdom/create-component.js中

  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 {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
复制代码

init钩子函数执行很简单,暂时先不考虑keepAlive的情况,他是通过createComponentInstanceForVnode创建一个Vue实例,然后调用$mount方法挂在子组件,先来看一下createComponentInstanceFowVnode的实现:

export function createComponentInstanceForVnode (
  // we know it's MountedComponentVNode but flow doesn't
  vnode: any,
  // activeInstance in lifecycle state
  parent: any
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}
复制代码

createComponentInstanceForVnode函数构造的一个内部组件的参数,然后执行new vnode.componentOption.Ctor(options)。这列的vnode.componentOptions.Ctor(options)对应的就是子组件的构造函数,我们之前分析了Sub,相当于newSub(options),这里有几个关键要注意几个点,_isComponent为true表示它是一个组件,parent表示当前激活的组件实例(注意,这里比较有意思的是如何拿到组件实例,后面会介绍)

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

  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // 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
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    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')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

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

这里首先是合并options的过程有变化,_isComponent为true,所以走到了initInternalComponent过程,这个函数简单看一下

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
  }
}
复制代码

这个过程我们重点记住协以下几点就可以:ops.parent = options.parent 、opts._parentVnode = parentVnode ,他们是吧之前我们通过createComponentInstanceForVNode函数传入的几个参数合并到$options里了。

再看一下_ init函数最后执行的代码:

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

由于组件初始化的时候是不传el的,因此组件是自己接管了mount的过程,这个过程的主要流程在之前有说过,回到组件init的过程,componentVNodeHooksinit钩子函数,在完成实例init后,接着执行child.mount的过程,这个过程的主要流程在之前有说过,回到组件init的过程,componentVNodeHooks的init钩子函数,在完成实例化_ init后,接着执行child.mount(hydrating ? vnode.elm : undefined,hydrating)。这里hydrating为true一般是服务端渲染的情况,我们只考虑客户端渲染,所以这里mount相当于child.mount相当于child.mount(undefined,false),他最终会调用mountComponent方法,进而执行vm._ render()方法:

Vue.prototype._render = function ():Node{
    const vm:Component = this
    const { render, _parentVnode } = vm.$options
    
    vm.$vnode = _parentVnode
    let vnode 
    try {
        vnode = render.call(vm._renderProxy, vm.$createElement)
    }catch(e) {
        
    }
    vnode.parent = _parentNode
    return vnode 
}
复制代码

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

我们知道在执行完vm._ render生成VNode后,接下来就要执行vm._ update去渲染VNode了。来看一下组件渲染过程中有哪些需要注意的,vm. _update的定义在

src/core/instance/lifecycle.js中:

export function lifecycleMixin (Vue: Class<Component>) {
  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
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }
复制代码

_update过程中有几个关键的代码,首先vm. _ vnode = vnode 的逻辑,这个vnode是通过 vm. _render()返回的组件渲染VNode,vm.vnode和vm,vnode的关系就是一种父子关系,用代码表示就是vm.vnode.parent===vm.vnode的关系就是一种父子关系,用代码表示就是 vm. _vnode.parent === vm.vnode。

export let activeInstance: any = null
Vue.prototype._ update = function(vnode: VNode, hydrating?: boolean){
    //...
    const preActiveInstance = activeInstance
    activeInstance = vm
    if (!preVnode) {
    // initial render
     vm.$el = vm._ patch _ (vm.$el, vnode, hydrating, false/*removeOnly*/)
    }else{
        vm.$el = vm._ patch _ (preVnode, vnode)
    }
    activeInstance = prevActiveInstance
}
复制代码

这个activeInstance作用就是保持当前上下文的Vue实例,他是在lifecycle模块的全局变量,定义是export let activeInstance : any = null ,并在之前我们调用createComponentInstanceForVnode 方法的时候从lifecyle模块获取,并且作为参数传入的。因为实际上Javascript是一个单线程,Vue整个初始化是一个深度遍历的过程,在实例化子组件的过程中,他需要知道当前上下文的Vue的实例是什么,并把他作为子组件的父Vue实例。之前我们提到过子组件的实例化过程先调用initInternalComponent(vm, options)合并options,吧parent存储在vm.options中,在options中,在mount之前回调用initLifecycle(vm)方法:

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.parent是用来保留当前vm的父实例,并且通过parent.children.push(vm)

来吧当前的vm存储到父实例的$children中。

在vm_ update 的过程中,通过vm.$parent吧这个父子关系保留。

那么回到_ update,最后就是调用 _ patch __渲染VNode了。

vm.$el = vm.__patch__ (vm.$el,vnode,hydrating,false/*removeonly*/)
function patch (oldVnode,vnode,hydrating,removeOnly) {
    // ...
    let isInitialPath = false
    const insertedVnodeQueue = []
    
    if (isUndef(oldVnode)){
        //empty mount (likely as component),create new root element
        isInitialPath = true
        createElm(vnode, insertedVnodeQueue)
    }else{
        //...
    }
    //...
}
复制代码

这里又回到了我们开始的内容,之前分析过负责渲染成DOM的函数时createElm,注意这里我们只传递两个参数,所以对应的parentElm时undefined。我们再来看他的定义:

function createElm (
	vnode,
     insertedVnodeQueue,
     parentElm,
     refElm,
     nested,
     ownerArrary,
     index
    ){
        // ...
        if(createComponent(vnode, insertedVnodeQueue, parentElm, refElm)){
            return 
        }
        const  data = vnode.data
        const children = vnode.children
        const tag = vnode.tag
        if(isDeftag){
           //...
            vnode.elm = vnode.ns? node.createElementNS(vnode.ns, tag):
            nodeOps.createElement(tag, vnode)
           setScope(vnode)
            
            /* istanbul ignore if*/
            if(__week__){
               // ... 
            }else{
                createChildren(vnode, children, insertedVnodeQueue)
                if(isDef(data)){
                   insert(parentElm, vnode.elm, refElm)
                }
                // ...
            }else if (isTrue(vnode.isComment)) {
                vnode.elm = nodeOps.createComment(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(isTure(isReactivated)){
               reactivateComponent(vnode, insertedVnodeQueue, parentElm, refELm)
            }
       return true
        }
    }
}
复制代码

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

总结

那么到此,一个组件的VNode是如何创建、初始化、渲染的过程也就介绍完毕了,对组建的实现有一个大概了解后,接下里我们介绍一下其中的部分细节,我们知道编写一个组件实际上是编写一个JavaScript对象,对象的描述就是各种配置,之前我们提到在_init 的最初阶段的就是merge options 的逻辑,接下来我们来从源码的角度来分析合并配置的过程。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享