美文网首页
Vue源码分析—组件化(二)

Vue源码分析—组件化(二)

作者: oWSQo | 来源:发表于2019-07-11 17:43 被阅读0次

    patch

    当我们通过createComponent创建了组件VNode,接下来会走到vm._update,执行vm.__patch__去把VNode转换成真正的DOM节点。但是针对一个普通的VNode节点,接下来我们来看看组件的VNode会有哪些不一样的地方。
    patch的过程会调用createElm创建元素节点,回顾一下createElm的实现,它的定义在src/core/vdom/patch.js中:

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

    createComponent

    我们删掉多余的代码,只保留关键的逻辑,这里会判断createComponent(vnode, insertedVnodeQueue, parentElm, 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钩子函数,我们在创建组件VNode的时候合并钩子函数中就包含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方法挂载子组件, 先来看一下createComponentInstanceForVnode的实现:

    export function createComponentInstanceForVnode (
      vnode: any, // we know it's MountedComponentVNode but flow doesn't
      parent: any, // activeInstance in lifecycle state
    ): 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.componentOptions.Ctor(options)。这里的vnode.componentOptions.Ctor对应的就是子组件的构造函数,它实际上是继承于Vue的一个构造器Sub,相当于new Sub(options)这里有几个关键参数要注意几个点,_isComponenttrue表示它是一个组件,parent表示当前激活的组件实例。

    所以子组件的实例化实际上就是在这个时机执行的,并且它会执行实例的_init方法,这个过程有一些和之前不同的地方需要挑出来说,代码在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)
      } 
    }
    

    这里首先是合并options的过程有变化,_isComponenttrue,所以走到了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
      }
    }
    

    这个过程我们重点记住以下几个点即可:opts.parent = options.parentopts._parentVnode = parentVnode,它们是把之前我们通过createComponentInstanceForVnode函数传入的几个参数合并到内部的选项$options里了。
    再来看一下_init函数最后执行的代码:

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

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

    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当前组件的渲染vnodevnodeparent指向了_parentVnode,也就是vm.$vnode,它们是一种父子的关系。

    我们知道在执行完vm._render生成VNode后,接下来就要执行vm._update去渲染VNode了。来看一下组件渲染的过程中有哪些需要注意的,vm._update的定义在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
      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)
      }
      activeInstance = prevActiveInstance
      // 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()返回的组件渲染VNodevm._vnodevm.$vnode的关系就是一种父子关系,用代码表达就是vm._vnode.parent === vm.$vnode。还有一段比较有意思的代码:

    export let activeInstance: any = null
    Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
        // ...
        const prevActiveInstance = activeInstance
        activeInstance = vm
        if (!prevVnode) {
          // initial render
          vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
        } else {
          // updates
          vm.$el = vm.__patch__(prevVnode, vnode)
        }
        activeInstance = prevActiveInstance
        // ...
    }
    

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

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

    那么回到_update,最后就是调用__patch__ 渲染VNode了。

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

    之前分析过负责渲染成DOM的函数是createElm,注意这里我们只传了2个参数,所以对应的parentElmundefined。我们再来看看它的定义:

    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的插入顺序是先子后父。

    总结

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

    相关文章

      网友评论

          本文标题:Vue源码分析—组件化(二)

          本文链接:https://www.haomeiwen.com/subject/hwtshctx.html