杂谈:$parent/$children的底层逻辑

1,500 阅读1分钟

new Vue是全局实例化一个根的Vue实例,Vue支持组件化,所以一个Vue实例下会有其他子组件,子组件中也可以调用其他子组件。那么,Vue实例和组件构造函数实例化的过程中会形成父子关系,我们可以利用$parent$children的关系,让子组件有访问父组件属性和方法的能力,也让父组件有访问子组件属性和方法的能力。

1、initLifecycle

在通过new Vue的方式进行Vue的实例化过程中,会执行各个初始化方法,这其中包括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.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

通过parent = options.parent的方式获取父级parent,并将其赋值给vm.$parent。如果当前parent存在,vm.$parentparent,如果不存在,说明当前vm即为根。

这里也定义了vm.$children为空数组,如果当前vmparent存在,那么通过parent.$children.push(vm)的方式构建父子关系

问题来了,这里的options.parent是哪儿来的呢?

2、子组件init钩子函数

组件渲染的过程中,会执行钩子函数init:

init: function init (vnode, hydrating) {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      var mountedNode = vnode; // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    } else {
      var child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      );
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
  },

这里会执行createComponentInstanceForVnode的逻辑:

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

这里构建了个options,其中_parentVnode就为当前的vnode,而parent就是传入的参数activeInstance

activeInstance从哪里来?

3、_update

在子组件通过init钩子函数渲染之前,走的渲染逻辑是Vue原型上的_update:

var activeInstance = null;
var isUpdatingChildComponent = false;

function setActiveInstance(vm) {
  var prevActiveInstance = activeInstance;
  activeInstance = vm;
  return function () {
    activeInstance = prevActiveInstance;
  }
}
// ...
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.
  }

通过activeInstance = vm可以看出,activeInstance就是当前活跃的Vue实例vm

这条线是已经确认了父子关系是通过parent.$children.push(vm)构建,然后从initLifecycle实例中的parent开始,到子组件渲染的init钩子函数中的activeInstance,再到_update中的vm来寻找parent的。

实际的渲染流程是,首次开始执行this._init时,不存在parent的值,先定义了vm.$children=[]。子组件渲染的时候,执行到渲染逻辑_update,这里确认了activeInstance就是当前的Vue实例vm,然后,在子组件渲染的钩子函数init中,就将activeInstance作为createComponentInstanceForVnode第二个参数parent传入,并且构建options。执行到子组件的this._initoptions中就有了parent,父子关系也就通过parent.$children.push(vm)构建起来啦。