Skip to content

Latest commit

 

History

History
402 lines (338 loc) · 14.6 KB

patch——自定义组件的处理流程.md

File metadata and controls

402 lines (338 loc) · 14.6 KB

原本想的Vue中对于自定义组件的处理,无非就是通过调用几个钩子,然后来创建原生的dom结构。当自己准备写这一部分的分析时,才发现给自己留了一个大坑。自定义组件的创建处理,涉及到整个Vue的各个阶段,本文可以说是从一个小栗子查看Vue的生命周期在生命周期处理阶段的一个补充。

vdom——VNode一文的最后,讲解createComponent方法时,我们提到在这儿会给data.hook上添加四个钩子函数——initprepatchinsertdestroy。我们一起来看一下每个时期,都分别作了哪些处理。

init

回到patch方法中,我们在创建dom元素时,首先会调用一个createComponent方法来判断当前的vnode是不是一个自定义组件。

  function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    ...
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    ...
  }
  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 */, parentElm, refElm)
      }
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

上面的代码中,我会调用init方法,并传入四个参数。从之前patch相关内容的讲解中,我们知道,第一个参数vnode就是当前自定义组件的vnode,第二个参数直接就是false,第三个参数parentElm是当前元素的父元素,第四个参数是值要把当前元素插入到refElm之前。

接着我们来看init方法里面都进行了哪些操作。

  init (
    vnode: VNodeWithData,
    hydrating: boolean,
    parentElm: ?Node,
    refElm: ?Node
  ): ?boolean {
    if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance,
        parentElm,
        refElm
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    } else if (vnode.data.keepAlive) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    }
  }

对于keep-alive组件,我们暂且不管。如果vnode.componentInstance不存在或已经销毁,则通过createComponentInstanceForVnode方法来创建新的Vue实例。

export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
  parentElm?: ?Node,
  refElm?: ?Node
): Component {
  const vnodeComponentOptions = vnode.componentOptions
  const options: InternalComponentOptions = {
    _isComponent: true,
    parent,
    propsData: vnodeComponentOptions.propsData,
    _componentTag: vnodeComponentOptions.tag,
    _parentVnode: vnode,
    _parentListeners: vnodeComponentOptions.listeners,
    _renderChildren: vnodeComponentOptions.children,
    _parentElm: parentElm || null,
    _refElm: refElm || null
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (inlineTemplate) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnodeComponentOptions.Ctor(options)
}

componentOptions中包括五项数据,Ctor是自定义组件的构造函数,propsData是父组件通过props传递的数据,listeners是添加在当前组件上的事件,tag是自定义的标签名,children即当前自定义组件的子元素。

createComponentInstanceForVnode接收了四个参数,第一个就是当前组件的vnode,第二个是父Vue实例,第三个是父元素,第四个是后面的兄弟元素。最终我们会调用new vnodeComponentOptions.Ctor(options)来创建一个新的Vue实例。

所以,我们又回到了创建Vue实例的生命周期。

回到src/core/instance/init.js中的Vue.prototype._init方法。

    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
      )
    }

这一次对于options的处理我们走进了if块。

function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  opts.parent = options.parent
  opts.propsData = options.propsData
  opts._parentVnode = options._parentVnode
  opts._parentListeners = options._parentListeners
  opts._renderChildren = options._renderChildren
  opts._componentTag = options._componentTag
  opts._parentElm = options._parentElm
  opts._refElm = options._refElm
  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

vdom——VNode一文中我们也说过,自定义组件的构造函数,是Vue对象的一个子对象。在我们新建Vue对象时,会通过mergeOptions方法来把vm.constructor上的options值与传入的options合并然后赋值给vm.$options。而这里我们通过Vue.extends创建新的对象时,已经把当前自定义组件的配置项合并到了vm.constructor.options上,所以我们这里vm.$options只需要继承一下就可以了。

同时,我们还给vm.$options添加了一些内部属性,具体每个属性的含义,我总结在了Vue实例属性中。

new vnodeComponentOptions.Ctor(options)仅仅是初始化了一个新的Vue实例,真正挂载到页面中,是通过child.$mount(hydrating ? vnode.elm : undefined, hydrating)进行的。上面可以知道hydratingfalse,所以第一个参数是undefined,第二个参数是false

$mount方法会先处理模板,最终还是调用src/core/instance/lifecycle中的Vue.prototype._update方法渲染组件。

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

又回到了多次谈到的__patch__方法。

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

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue, parentElm, refElm)
    } else {
      ...
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

这一次oldVnode就是undefined,所以直接走到if块代码中,然后调用createElm方法来创建dom结点。之后的流程,见patch——创建dom

prepatch

从名字我们也可以看出,该方法是在进行diff操作之前进行的处理。它的调用之处在patchVnode中:

  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    if (oldVnode === vnode) {
      return
    }
    ...
    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }
    ...
  }

来看看它里面进行了哪些操作。

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

该方法接受两个参数,分别是旧新Vnode实例。如果数据有所更新,会再次调用render函数生成新的VNode实例,这个过程中会再次调用createComponent函数生成新的自定义组件的vnode

调用prepatch钩子函数的前提,说明该自定义组件得到了复用,也就是说该自定义组件本身没有被替换,我们只需要根据传入的props或者slots等来更新子模板的内容。这里我们直接复用oldVnode.componentInstance,然后调用updateChildComponent方法来更新子内容。

export function updateChildComponent (
  vm: Component,
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: VNode,
  renderChildren: ?Array<VNode>
) {
  
  const hasChildren = !!(
    renderChildren ||               // has new static slots
    vm.$options._renderChildren ||  // has old static slots
    parentVnode.data.scopedSlots || // has new scoped slots
    vm.$scopedSlots !== emptyObject // has old scoped slots
  )
  // 更新vnode相关关系
  vm.$options._parentVnode = parentVnode
  vm.$vnode = parentVnode // update vm's placeholder node without re-render
  if (vm._vnode) { // update child tree's parent
    vm._vnode.parent = parentVnode
  }
  vm.$options._renderChildren = renderChildren

  // 更新 props
  if (propsData && vm.$options.props) {
    observerState.shouldConvert = false
    if (process.env.NODE_ENV !== 'production') {
      observerState.isSettingProps = true
    }
    const props = vm._props
    const propKeys = vm.$options._propKeys || []
    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      props[key] = validateProp(key, vm.$options.props, propsData, vm)
    }
    observerState.shouldConvert = true
    if (process.env.NODE_ENV !== 'production') {
      observerState.isSettingProps = false
    }
    // keep a copy of raw propsData
    vm.$options.propsData = propsData
  }
  // 更新 listeners
  if (listeners) {
    const oldListeners = vm.$options._parentListeners
    vm.$options._parentListeners = listeners
    updateComponentListeners(vm, listeners, oldListeners)
  }
  // 处理slots并强制更新
  if (hasChildren) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }
}

该方法所要做的工作主要有以下几个方面。

1、更新vm上绑定的有关vnode的各项数据。之前我们通过vm.$options._parentVnodevm.$vnodevm.$options._renderChildren等保存了当前自定义组件在父组件中的vnode以及在父组件中当前自定义组件的子元素,patch时会生成新的vnode,所以需要更新相应的内容。

2、更新保存父组件传递过来的数据propsData,并对传递的数据类型等进行校验。

3、更新绑定的事件

4、更新slots相关内容

insert

insert方法的调用是在dom插入到页面之后调用的。具体方法是在__patch__中的invokeInsertHook方法。

  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])
      }
    }
  }

insert钩子函数的具体实现如下所示:

  insert (vnode: MountedComponentVNode) {
    if (!vnode.componentInstance._isMounted) {
      vnode.componentInstance._isMounted = true
      callHook(vnode.componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      activateChildComponent(vnode.componentInstance, true /* direct */)
    }
  },

我们发现,这里的主要操作是调用mounted钩子函数。回顾一下之前讲的调用mounted钩子函数的代码:

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

vm.$vnode保存的是上面init时传入的_parentVnode,即自定义组件在父组件中的VNode对象。根组件的该值为null,所以在上面代码中调用mounted,而对于自定义组件,则在insert钩子函数中调用。

destroy

该钩子函数的调用,是在自定义组件销毁时调用。

  destroy (vnode: MountedComponentVNode) {
    if (!vnode.componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        vnode.componentInstance.$destroy()
      } else {
        deactivateChildComponent(vnode.componentInstance, true /* direct */)
      }
    }
  }

patch过程中调用该钩子函数,是因为在做diff的过程中,要删除当前的组件。对于普通组件,我们直接调用vnode.componentInstance.$destroy()方法来销毁。

  Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    // remove self from parent
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // teardown watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // call the last hook...
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // remove reference to DOM nodes (prevents leak)
    vm.$options._parentElm = vm.$options._refElm = null
  }
}

函数定义如上:

1、调用beforeDestroy钩子函数,并通过vm._isBeingDestroyed来标识正在销毁,避免重复调用。

2、从父元素中删除当前元素。

3、销毁watcher

4、vm._data_的监听对象的vmCount减1

5、标识vm已销毁

6、销毁当前组件

7、调用destroyed钩子函数

8、销毁事件

9、消除各种引用的资源