Vue2.0源码学习(3) - 组件的创建和patch过程

x33g5p2x  于2022-02-20 转载在 Vue.js  
字(14.3k)|赞(0)|评价(0)|浏览(214)

组件化

组件化是vue的另一个核心思想,所谓的组件化就,就是说把页面拆分成多个组件(component),每个组件依赖的css、js、图片等资源放在一起开发和维护。组件是资源独立的,在内部系统中是可以多次复用的,组间之间也是可以互相嵌套的。
接下来我们用vue-cli为例,来分析一下Vue组件是如何工作的,还是它的创建及其工作原理。

import Vue from 'vue'
import App from './App.vue'

var app = new Vue({
  el: '#app',
  // 这里的 h 是 createElement 方法
  render: h => h(App)
})
创建组件 - createComponent

在分析createComponent函数前,我们得先知道vue的源码执行过程中是怎么调用到createComponent的。其实我们在上一章就有所提及,具体流程如下:
①:Vue.prototype.$mount;(src\platforms\web\entry-runtime-with-compiler.js和src\platforms\web\runtime\index.js)
②:mountComponent;(src\core\instance\lifecycle.js)
③:vm._update(vm._render());(src\core\instance\lifecycle.js)
④:render.call(vm._renderProxy, vm.$createElement);(src\core\instance\render.js)
⑤:vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);(src\core\instance\render.js)
⑥:createElement;(src\core\vdom\create-element.js)
⑦:_createElement;(src\core\vdom\create-element.js)
⑧:createComponent;

//src\core\vdom\create-element.js
// part 3
export function _createElement (
  context: Component,   //上下文环境,一般就是vm
  tag?: string | Class<Component> | Function | Object,  //标签(element)
  data?: VNodeData,     //VNode数据,VnodeData类型,详见flow\vnode.js
  children?: any,       //Vnode子节点
  normalizationType?: number    //子节点规范类型
): VNode | Array<VNode> {
  ...
  //这次这个tag就是Class<Component>了
  if (typeof tag === 'string') {
    ...
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  ...
}

这次我们进入到_createElement函数走的就是createComponent的流程了。

// src\core\vdom\create-component.js
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,   //当前vm实例
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }
  
  // 标注①
  const baseCtor = context.$options._base  //实际上就是Vue
  if (isObject(Ctor)) {
    // 标注②
    Ctor = baseCtor.extend(Ctor)   //即Vue.extend(src\core\global-api\extend.js)
  }
  ...
  //   钩子函数挂载到data对象,详情查阅源码 
  installComponentHooks(data)
}

①:baseCtor其实就是Vue,流程如下

// src\core\global-api\index.js
// 初始化时候定义了Vue.options._base = Vue
Vue.options._base = Vue
// src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
  // 在这里吧Vue.options合并到vm.$options上
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}

而context其实就是vm,所以baseCtor =context.$options._base = vm.$options._base = Vue.options._base = Vue;

②:分析完baseCtor的由来,那么baseCtor.extend显然就是Vue.extend了,把Ctor对象转换成新的构造器,我们下面来详细看看Vue.extend。

Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this  //vue
    const SuperId = Super.cid
    //添加了一个_Ctor空对象属性
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    //后面会缓存cachedCtors[SuperId],防止多次生成相同构造器
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      //名称校验,防止你们整些花里胡哨的关键字段。
      validateComponentName(name)  
    }

    const Sub = function VueComponent (options) {
      this._init(options) //vue._init
    }
    Sub.prototype = Object.create(Super.prototype)  //子构造器原型指向父构造器原型
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    //入参配置和vue配置合并
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor  缓存起来
    cachedCtors[SuperId] = Sub
    return Sub
  }

Vue.extend的作用其实就是构建一个Vue的子类,把对象转换成继承于Vue的构造器Sub并返回,然后对Sub本身扩展了option、全局API等,并对配置中的props和computed做了初始化工作,最后对Sub做了缓存,防止多次生成相同构造器。

// src\core\vdom\create-component.js
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,   //当前vm实例
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  ...
  //   钩子函数挂载到data对象
  installComponentHooks(data)
  ...
}

我们继续看createComponent函数,中间忽略了一些代码块,后续涉及到的时候再分析,现在我们先看看installComponentHooks函数。

// src\core\vdom\create-component.js
function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    const existing = hooks[key]
    const toMerge = componentVNodeHooks[key]
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

const hooksToMerge = Object.keys(componentVNodeHooks)
//默认钩子
const componentVNodeHooks = {
  init(){},
  prepatch(){},
  insert(){},
  destroy(){},
}
//合并钩子
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
}

installComponentHooks其实就遍历了hooksToMerge,其实就是遍历了componentVNodeHooks的钩子然后和data.hook合并。
接下来我们继续往下看createComponent函数

// src\core\vdom\create-component.js
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,   //当前vm实例
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  ...
  // return a placeholder 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
}

组件的new VNode和之前入参不太一样,我们先回顾一下Vnode的入参分别是什么?

// src\core\vdom\vnode.js
export default class VNode {
  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ){},  
}

主要有三个需要关注的点:
tag:会有一个'vue-component-'标识这个是个组件;
children:入参是undefined,记住组件的children是空的,这个到时候再patch遍历时候会用到;
componentOptions:组件的很多数据都存放在这里虽然chilrend入参为空,但是这里有传入;

new Vnode之后把vnode return到createComponent,到此我们createComponent的流程就跑完了。接下来我们又回到了vm._update(vm._render(), hydrating)中,开始了vm._update之旅了,其实也就是回到了把vnode转换成真实dom的patch函数。

patch函数 - 组件处理

又回到最初的起点,呆呆的站在patch前。

// src\core\vdom\patch.js
return function patch (oldVnode, vnode, hydrating, removeOnly) {
  ...
  // create new node
  createElm(
    vnode,
    insertedVnodeQueue,
    // extremely rare edge case: do not insert if old element is in a
    // leaving transition. Only happens when combining transition +
    // keep-alive + HOCs. (#4590)
    oldElm._leaveCb ? null : parentElm,
    nodeOps.nextSibling(oldElm)
  )
}

patch的流程和配置el或者template一样,会走到createElm

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

与之前最大的不同一就是跑到了createComponent函数时候的处理了,这边需要注意的是这个createComponent函数是patch.js中的,而不是我们上文提及到的create-component.js中的,这里要区分开来,不要混淆了。
下面我们来看看我们调用到的patch.js中的pcreateComponent函数

// src\core\vdom\patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      //keepalive逻辑,先不解读
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      // i.hook = data.hook,再判断是否有init方法,都成立是运行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
      }
    }
  }

createComponent一次赋值且判断vnode.data、vnode.data.hook、vnode.data.init是否为空,都成立是则运行init方法。那么这个init方法又是哪个呢?我们还记得上文生成vnode时候在create-component.js中的调用createComponent函数的时候有个installComponentHooks方法吗?在那里我们插入了init钩子,忘了的可以回顾一下前文,下面我们看看init的代码。

// src\core\vdom\create-component.js
const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    // componentInstance是undefined,进入else逻辑
    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)
    }
  },
}

在vnode.componentInstance和vnode.data.keepAlive都是undefined的情况下我们进入了else逻辑。
其中执行了createComponentInstanceForVnode函数,它返回的其实就是一个vm实例,下面我们具体看看createComponentInstanceForVnode函数。

// src\core\vdom\create-component.js
export function createComponentInstanceForVnode (
  // we know it's MountedComponentVNode but flow doesn't
  vnode: any,
  // activeInstance in lifecycle state
  parent: any //vm实例
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,   //重新进入vue._init时候做判断用到
    _parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate  //undefined
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}

上述函数一开始配置了options,inlineTemplate是undefined,直接忽略,然后到了return new vnode.componentOptions.Ctor(options);
vnode.componentOptions.Ctor究竟是什么?这个我们得回到上诉的componentOptions,还记得那里new vnode传入的参数吗?其实倒数第二个就是componentOptions,入参是{ Ctor, propsData, listeners, tag, children},
所以知道这个怎么来了吧,vnode.componentOptions.Ctor其实就是入参的Ctor,而Ctor忘记了的,自己回顾一下Ctor。
所以其实它运行的就是extend中的Sub构造器:

// src\core\global-api\extend.js
const Sub = function VueComponent (options) {
  this._init(options)
}

因为Sub构造器继承的是Vue,因此this._init又回来Vue._init这个初始化操作。
那么组件进入_init和普通节点有什么不一样呢?我们来重新进入vue._init,下面只选择性展示不一样的地方。

// src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
  ...
  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)
  }
  ...
}

_isComponent在createComponentInstanceForVnode函数是已经配置好是true了,所以会进入到initInternalComponent方法,下面看看它的定义:

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

主要就是把options的配置给到vm.$options上。
然后接下来看其他的差异:

// src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
  ...
  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.$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
}

这里其实就是建立一个父子vm的关联,下面主要分析一下parent是什么?看字面意思也知道这是父级的东西,没错就是父级vm实例,他是在哪里定义的呢,这时候我们的回顾一下createComponentInstanceForVnode的方法,其中有配置options.parent = parent,而这个入参parent再往上追溯,其实就在componentVNodeHooks中的init方法中传递过来的,它传递的activeInstance其实是一个全局变量,那么这个又是在什么时候定义的?其实它在src\core\instance\lifecycle.js中做了定义,运行Vue._update的时候已经做了赋值,下面我们看一下代码。

// src\core\instance\lifecycle.js
//全局定义
export let activeInstance: any = null
// activeInstance的赋值
export function setActiveInstance(vm: Component) {
  const prevActiveInstance = activeInstance
  activeInstance = vm
  return () => {
    activeInstance = prevActiveInstance
  }
}

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  // 调用activeInstance赋值方法
  const restoreActiveInstance = setActiveInstance(vm)
}

因此在每次运行到_update的时候,在进入下个阶段patch函数之前,它都会缓存住生成真实dom之前的vm,因此在patch做递归进入一下个阶段的时候,activeInstance就是它的父vm实例。知道了parent的来源,那么这父子关联的代码块的逻辑也就明了了。
这时候patch也进入了递归阶段,递归方法还是和之前相似,在回顾一下流程:
①:Vue.prototype._init;(src\core\instance\init.js)
②:Vue.prototype.$mount;(src\platforms\web\entry-runtime-with-compiler.js)
②:Vue.prototype.$mount;(src\platforms\web\runtime\index.js)
③:mountComponent;(src\core\instance\lifecycle.js)
④:Vue.prototype._render;(src\core\instance\render.js)
⑤:Vue.prototype._update;(src\core\instance\lifecycle.js)
⑥:vm.patch;(src\platforms\web\runtime\index.js)
⑦:createPatchFunction;(src\core\vdom\patch.js)
⑧:patch;(src\core\vdom\patch.js)
⑨:createElm;(src\core\vdom\patch.js)
⑩:createComponent;(src\core\vdom\patch.js)
在createComponent中的i.init开始了新一轮的初始化,当递归结束后我们还有接下来的流程,我们继续看createComponent方法。

// src\core\vdom\patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      //这里会完成整个patch的递归流程
      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. 
      // 在初始化hook之后,如果vnode是一个子组件,那么它应该创建一个子实例并挂载它。
      // the child component also has set the placeholder vnode's elm.
      // 子组件还设置了占位符vnode的elm
      // in that case we can just return the element and be done.
      // 在这种情况下,我们只需返回element就可以了

      // 只有在patch结束后才进入了这里
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

递归调用i.init结束后,我们进入下一个逻辑。vnode.componentInstance,在i.int中得到了赋值,就是一个vm实例,详情回顾componentVNodeHooks,因此我们进入了initComponent函数,下面看看initComponent是做什么的。

// src\core\vdom\patch.js
function initComponent (vnode, insertedVnodeQueue) {
    ...
    vnode.elm = vnode.componentInstance.$el
    if (isPatchable(vnode)) {
      // 创建一些钩子,后续再分析
      invokeCreateHooks(vnode, insertedVnodeQueue)
      setScope(vnode)
    } else {
      ...
    }
  }

initComponent主要是给vnode.elm赋值,vnode.componentInstance.$el即vm.$el,__patch__方法有做返回。我们继续回到initComponent的下一步insert,insert其作用就是根据判断插入dom(insertBefore/appendChild),之前有讲述过,至此,子组件的真实dom就生成了。由于这是递归插入的模式,因此dom的插入顺序是先子后父。

到此组件的patch的过程就到此结束了,建议配合代码运行调试,反复几次理解运行逻辑,及参数传递与缓存。

相关文章

微信公众号

最新文章

更多