vue源码分析(四)

212 阅读4分钟

三、组件化

vue允许我们使用小型、独立和通常可复用的组件构建大型应用。这样在项目的代码复用性,解耦,维护性上都有很大的提升。

以vue-cli的初始化部分分析组件化的实现

new Vue({
  el: '#app',
  render: h => h(App)
})

1、createComponent 生成Vnode

简单回顾一下生成Vnode的过程

1.执行_render,vm._render()

2.通过$options拿到render const { render, _parentVnode } = vm.$options

3.执行render,把$createElement作为参数传入,vnode = render.call(vm._renderProxy, vm.$createElement)

4.$createElement函数返回createElementvm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

5.createElement返回_createElementcreateElement => _createElement

上一次分析中,传入的tag参数是'dev'符合typeof tag === 'string',本次传入的是App是一个组件,因此走到了else vnode = createComponent(tag, data, context, children)这条逻辑

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  ...
  if (typeof tag === 'string') {
    ...
   } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  ...
}

组件生成VnodecreateComponent函数主要做了

  • 拿到Vue构造函数,通过Ctor = baseCtor.extend(Ctor)生成当前组件的构造函数
  • 初始化相应的钩子函数installComponentHooks(data)
  • 通过 new VNode生成Vnode
// src/core/vdom/create-component.js
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  ...
  // 通过 context.$options._base 拿到Vue构造函数
  const baseCtor = context.$options._base // Vue
  
  // 把传入的Ctor调用Vue.extend方法,生成新的构造函数
  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  ...

  // install component management hooks onto the placeholder node
  // 绑定相应的钩子
  installComponentHooks(data)

  // return a placeholder vnode
  // 创建组件Vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  ...

  return vnode
}

1-1 baseCtor.extend(Ctor)

首先执行了const baseCtor = context.$options._base 来定义baseCtor,context是vm也就是当前的vue实例

$options._base,在initGlobalAPI的时候,给Vue.options._base赋值

// src/core/global-api/index.js
Vue.options._base = Vue

vue在执行_init的时候会初始化$options,通过mergeOptions方法,把Vue.options合并到vm.$options

// src/core/instance/init.js
vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

所以baseCtor其实就是Vue构造函数,baseCtor.extend(Ctor) === Vue.extend(Ctor)

调用Ctor = baseCtor.extend(Ctor)Ctor其实就是一个继承于Vue的构造函数

src/core/global-api/extend.js
export function initExtend (Vue: GlobalAPI) {
  ...
  Vue.extend
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this // Vue
    const SuperId = Super.cid
    ...
    const name = extendOptions.name || Super.options.name
    ...
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    // Sub继承于Vue
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    // 合并配置处理
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super
    ...
    return Sub
  }
}

1-2 installComponentHooks(data)

installComponentHooks函数为data初始化了4个钩子

src/core/vdom/create-component.js
function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  for (let i = 0; i < hooksToMerge.length; i++) {
    /**
      * componentVNodeHooks 中有4个函数钩子
      * const componentVNodeHooks = {
      *  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {...},
      *  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode){...},
      *  insert (vnode: MountedComponentVNode) {...},
      *  destroy (vnode: MountedComponentVNode) {...}
      * }
      * const hooksToMerge = Object.keys(componentVNodeHooks)
      */
    const key = hooksToMerge[i]
    const existing = hooks[key]
    const toMerge = componentVNodeHooks[key]
    if (existing !== toMerge && !(existing && existing._merged)) {
    /** 
      * function mergeHook (f1: any, f2: any): Function {
      *   const merged = (a, b) => {
      *   // flow complains about extra args which is why we use any
      *   f1(a, b)
      *   f2(a, b)
      *  }
      * merged._merged = true
      * return merged
      * }
      */
  	  // 合并策略
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

1-3 new VNode 生成一个vnode对象

 // src/core/vdom/create-component.js
 const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

2、patch

patch中会调用createElm函数,在vnode转换为真实dom的过程中组件的和标签有所区别,会进入createComponent这个函数

src/core/vdom/patch.js
  function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    ...
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
    ...
  }

在组件vnode的生成中,会调用installComponentHooks(data),为vnode的data挂载4个钩子

// src/core/vdom/patch.js
 function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    // 首先判断有没有vnode.data
    if (isDef(i)) {
      ...
      // 执行vnode.data.init
      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
      }
    }
  }

首先会执行data中的init函数,执行createComponentInstanceForVnode函数

// src/core/vdom/create-component.js
const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    // keep-alive逻辑
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
     ...
    } else {
      // 执行createComponentInstanceForVnode函数,他返回的是 vnode.componentInstance
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  }
  ...
 }

createComponentInstanceForVnode函数,执行new vnode.componentOptions.Ctor(options)

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
  }
  ...
  // 调用 new vnode.componentOptions.Ctor(options)
  return new vnode.componentOptions.Ctor(options)
}

vnode.componentOptions.Ctor(options)函数中,vnode.componentOptions是在组件vnode创建中 { Ctor, propsData, listeners, tag, children }其中的Ctor是通过Ctor = baseCtor.extend(Ctor)赋值的,也就是说Ctor实际上是组件的构造函数,那么在new vnode.componentOptions.Ctor(options)的过程中,实际上是执行了Sub中的 this._init(options)也就是Vue的_init方法

那么创建组件在执行Vue._init方法中的区别,首先合并options的操作发生了改变

// src/core/instance/init.js
// options._isComponent在createComponentInstanceForVnode中被赋值为了true
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 {
     ...
}

initInternalComponent函数中parent实际上是第二个参数activeInstance,他定义在lifeccyle.js文件中的一个全局变量

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

_update会调用setActiveInstance(vm),对activeInstance赋值为当前的vm实例

// src/core/instance/lifecycle.js
export function setActiveInstance(vm: Component) {
  // 把之前的vm实例保存
  const prevActiveInstance = activeInstance
  // 把当前activeInstance实例赋值为当前vm实例
  activeInstance = vm
  // 当再次调用时,进行还原
  return () => {
    activeInstance = prevActiveInstance
  }
}

initLifecycle中因为当前的parent是Vue,当前的vm是组件app的vm,所以通过$parent$children建立父子关系

// src/core/instance/lifecycle.js
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
  ...
}

_init中因为组件创建中没有vm.$options.el,最后的vm.$mount(vm.$options.el)是不会执行的,_init结束后,手动调用了child.$mount(hydrating ? vnode.elm : undefined, hydrating)也就是子组件的mountComponent

// src/core/instance/lifecycle.js
function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  ...
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  ...
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  ...
  return vm
}

子组件的vm._render()vm.$vnode赋值为vm.$options._parentVnode也就是父的vnode,然后给当前的vnode的parent也赋值为vm.$options._parentVnode

  // 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,也就是占位符vnode,即父的vnode
    vm.$vnode = _parentVnode
    ...
    let vnode
    ...
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } 
    ...
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }

到了子组件的_update中,首先把activeInstance = vm把当前的vm作为了activeInstance,vm._vnode = vnode也就是说 vm._vnodevm.$vnode是一个父子关系。 vm._vnode是一个渲染的vnode,vm.$vnode是占位符vnode,之后执行了 vm.__patch__

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    ...
    // setActiveInstance函数把activeInstance赋值为当前的vm
    const restoreActiveInstance = setActiveInstance(vm)
    // _vnode赋值为当前的渲染vnode
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    // 执行__patch__
    if (!prevVnode) {
      // initial render
      // 此时的vm.$el是undefined
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
    ...
    }
    ...
  }

执行patch的时候第一个参数为空,走到了createElm(vnode, insertedVnodeQueue),如果子组件中还有子组件,就会再次调用createComponent建立这样的组件树,在createElm中如果是组件则不会执行下边的dom插入操作,组件的插入会在createComponent中接着执行initComponentvnode.elm = vnode.componentInstance.$el,$el为子组件的根元素,再执行insert(parentElm, vnode.elm, refElm)进行插入,插入的顺序一定是先子后父的,因为父组件的init最终会去先创建子组件,子组件会先执行insert

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

总结

  • 组件patch的流程, createComponent -> 子组件init -> 子组件render -> 子组件patch
  • activeInstance为当前激活的vm实例,vm.$vnode为组件的占位符vnode,vm._vnode是渲染vnode
  • 插入的顺序是先子后父