Vue2源码的学习日记(3)

组件化

(在文章中的一切源码大部分都是只给出头部,因为源码是开源的(Vue2.6),我认为在自己去边查找边看的过程也能提升到自己)

学习日记(2)后,便开始慢慢走向vue这个框架的核心,希望能帮到大家,也希望如果有问题可以促使自己学的更加深入。

在Vue.js官网中讲到,它的一个核心思想是组件化。而组件化就是将一个页面拆分成多个组件,每个组件里的各个部分如JS、模板等放在一起进行维护。组件是独立的,在系统里也可以被复用,而组件和组件之间也可以互相嵌套,嵌套之后便有了父子组件这个说法。

对于下面这段代码我们都是很熟悉的

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

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

之前说过,每个组件都是render函数渲染的,一般render是传入一个原生标签进行渲染,而上面代码中的h也就是**createElement()**函数是传入一个组件来渲染。

createComponent

createElement方法实现的时候,最终会调**_createElement方法,其中如果参数是普通html标签,则会实例化一个普通VNode节点,否则通过createComponent**方法创建一个组件VNode。

而上面传入的App对象,实质上也是一个Component类型,而不是html标签,所以它会通过createComponent方法创建vnode。而createComponent方法的实现在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 {
  if (isUndef(Ctor)) {
    return
  }
...

而对这部分的理解大致可以分为三个部分

  1. 构造子类构造函数。
  2. 安装组件钩子函数。
  3. 实例化vnode。

1、构造子类构造函数

const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}

在我们去写一个组件时,一般都会创造一个普通对象,如:

import NewApp from './components/NewApp'
export default {
  name: 'app',
  components: {
    BewApp
  }
}

在这里export的是一个对象,所以createComponent里的代码逻辑会执行到baseCtor.extend(Ctor),而这个baseCtor实际上就是Vue,在最开始初始化Vue的阶段被定义,在src/core/global-api/index.js中的initGlobalAPI函数中有一段逻辑:

// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue

在这里定义的是Vue.option,而createComponent中取得是context.$options,在src/core/instance/init.js里Vue原型上的_init函数的一部分逻辑:

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

通过上面的逻辑就能把Vue上的部分option扩展到vm. o p t i o n 上,这样我们就能通过 ∗ ∗ v m . option上,这样我们就能通过**vm. option上,这样我们就能通过vm.options._base拿到Vue构造函数。而mergeOption的大致功能是将Vue构造器的option和用户传入的option做一层合并最终到vm.$option**上。

在构造子类构造函数中,有个很关键的点是Vue.extend函数,而与之相关的定义在src/core/global-api/extend.js中。

export function initExtend (Vue: GlobalAPI) {
    Vue.cid = 0
  	let cid = 1
    
    Vue.extend = function (extendOptions: Object): Function {...}
}

Vue.extend 的作用是构造一个Vue的子类,这方法是很经典的,将一个纯对象转换一个继承于Vue的构造器Sub并返回,然后对sub这个对象本身扩展了一些属性,比如扩展options、添加全局API等;并且对配置中的propscomputed做了初始化工作,最后对于这个sub构造函数做了缓存,避免多次执行Vue.extend的时候对相同子组件重复构造。当我们实例化Sub的时候,就会执行this._init逻辑再次走到了Vue实例的初始化逻辑。

const Sub = function VueComponent (options) {
  this._init(options)
}

2、安装组件钩子函数

// install component management hooks onto the placeholder node
installComponentHooks(data)

之前在准备模块中说到Vue.js使用的Virtual DOM参考的开源库snabbdom,他的一个特点便是在VNode的patch流程中对外暴露了各种时机钩子函数,方便我们做一些额外的事情,Vue.js也充分利用这一点,在初始化一个Component类型的VNode的过程中实现了一些钩子函数,在src/core/vdom/create-component.js中:

const componentVNodeHooks = {...}

而整个installComponentHooks的过程就是把componentVNodeHooks的钩子函数合并到data.hook中,在VNode执行patch的过程中执行相关的钩子函数。这里的合并策略是在合并过程中日光某一时刻的钩子已经存在data.hook中,那么通过执行mergeHook函数做合并,就是在最终执行时依次执行这两个钩子函数。

3、实例化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并返回。而相对不同元素节点不同的是,组件的vnode是没有children的。

最后可以得到一个相对具体的createComponent的实现,以及其渲染时的三个关键逻辑:构造子类构造函数,安装组件钩子函数和实例化vnode。在createComponent后返回的是组件vnode,它一样走到vm._update方法,进而执行patch函数。

patch

上面提到我们通过createComponent创建了VNode组件,接下来会走到vm._update,执行vm.__ patch__去把VNode转换成真正的DOM节点。但如果只是个普通的VNode节点,patch的过程会调用createElm创建元素节点,createElm的定义在src/core/vdom/patch.js中:

function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
){...}

在方法中会判断createComponent(vnode, insertedVnodeQueue, parentElm, refElm)的返回值,如果为true则直接结束。

createComponent函数中会先对vnode.data做一些判断,如果vnode是一个组件VNode,那得到的i就是init钩子函数,合并钩子函数里就有init钩子函数,被定义在src/core/vdom/create-component.js中:

init(vnode: VNodeWithData, hydrating: boolean): ?boolean {...}

init钩子函数在不考虑keepAlive的情况下,通过createComponentInstanceForVnode创建一个Vue的实例,然后调用**$mount**方法挂载子组件,

export function createComponentInstanceForVnode (
  // we know it's MountedComponentVNode but flow doesn't
  vnode: any,
  // activeInstance in lifecycle state
  parent: any
): Component {...}

createComponentInstanceForVnode函数构造一个内部组件的参数,然后执行new vnode.componentOptions.Ctor(options)。这里的vnode.componentOptions.Ctor对应的是自组建的构造函数,它实际上是继承于Vue的一个构造器Sub,相当于new Sub(options),其中部分参数:_isComponent为true表示他是一个组件,parent表示当前激活的组件实例,也有方法拿到这个参数。所有的子组件实例化都是在这个时候执行的,同事执行实例的 _init方法,在src/core/instance/init.js中:

Vue.prototype._init = function (options?: Object) {...}

在源码中首先是合并options有了一些改变,_isComponenttrue,于是走到initInternalComponent过程:

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {...}

在里面有一点:opts.parent = options.parent、opts._parentVnode = parentVnode,这是把之前我们通过createComponentInstanceForVnode函数传入的参数合并到内部的选项**$options**里了。

最后**_init**函数执行代码:

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

由于组件初始化不传入el,所以组件是自己接管了 m o u n t 的过程,转回 i n i t 的过程, c o m p o n e n t V N o d e H o o k s 的 i n i t 钩子函数,在完成实例化 i n i t 后,接着会执行 c h i l d . mount的过程,转回init的过程,componentVNodeHooks的init钩子函数,在完成实例化init后,接着会执行child. mount的过程,转回init的过程,componentVNodeHooksinit钩子函数,在完成实例化init后,接着会执行child.mount(hydrating ? vnode.elm : undefined, hydrating)。这里的 m o u n t 相当于执行 c h i l d . mount相当于执行child. mount相当于执行child.mount(undefined, false),他最终会调用mountComponent方法,进而执行vm._render()方法:

Vue.prototype._render = function (): 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) {...}

_update过程中的几个关键点,第一,vm._vnode = vnode,这个vnode是通过vm._render()反蝴蝶组件渲染VNodevm._vnode和vm. v n o d e 的关系就是一种父子关系,可以理解为 v m . v n o d e . p a r e n t = = = v m . vnode的关系就是一种父子关系,可以理解为vm._vnode.parent === vm. vnode的关系就是一种父子关系,可以理解为vm.vnode.parent===vm.vnode。还有一处:

export let activeInstance: any = null
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {...}

这个activeInstance作用是保持上下文的Vue实例,他是在lifecycle模块的全局变量,定义是 export let activeInstance:any = null,并且之前调用createComponentInstanceForVnode方法时从lifecycle模块获取,并且作为参数传入。因为从线程来说JavaScript是一个单线程,Vue的整个初始化是一个深度遍历的过程,在实例化子组件的过程中,它需要知道当前上下文的Vue实例是什么,并把它作为子组件的父Vue实例。之前提到过对子组件的实例化过程会先调用initInternalComponent(vm,options)合并options,把parent存储在vm. o p t i o n s 中,在 options中,在 options中,在mount之前会调用iinitLifecycle(vm):

export function initLifecycle (vm: Component) {...}

在具体源码中可以看到vm. o p t i o n 就是用来保留当前 v m 的父实例,通过 p a r e n t . option就是用来保留当前vm的父实例,通过parent. option就是用来保留当前vm的父实例,通过parent.children.push(vm)来把当前的vm存储到父实例的$children中。在vm._update过程中会把当前vm赋值给activeInstance,同时通过const prevActiveInstance = activeInstance 用prevActiveInstance保留上一次的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) {...}

在最开始提到,负责渲染DOM的函数是createElm,我们传入两个参数对应parentElmundefined

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {...}

这里传入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过程中又创建了子组件,那么插入顺序是先子后父。

简单来说,编写一个组件实际上市编写一个JavaScript对象,对象的描述就是各种配置。

merge options

这节是关于合并配置,当我们在new Vue的过程中通常有2种场景,一种是我们主动调用**new Vue(options)的方式实例化一个Vue对象,另一种是内部通过new Vue(option)**实例化子组件。

无论是哪一种情景,都会执行实例的**_init(option)方法,它首先会执行merge options**的逻辑,在源码src/core/instance/init.js中:

Vue.prototype._init = function (options?: Object) {
  // 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
    )
  }
  // ...
}

对于不同场景中options的合并代码逻辑是不一样的,并且传入的options值也有很大不同。

1、外部调用场景

当正在执行new Vue的时候,如果在执行this._init(options),便会执行下面逻辑合并options:

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

这里通过调用mergeOptions方法来合并,它实际上是将resolveConstructorOptions(vm.constructor)的返回值和options做合并,而resolveConstructorOptions的返回值在此时可以理解为返回vm.construtor.options,相当于Vue.options,而这个值在**initGlobalAPI(Vue)**的时候被定义,具体在src/core/global-api/index.js中:

export function initGlobalAPI (Vue: GlobalAPI) {
  // ...
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)
  // ...
}

首先通过Vue.options = Object.create(null)创建一个空对象,然后遍历ASSET_TYPESASSET_TYPES的定义在src/shared/constants.js中:

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

所以上面的遍历代码等同于:

Vue.options.components = {}
Vue.options.directives = {}
Vue.options.filters = {}

接着执行了Vue.options._base = Vue,最后通过extend(Vue.options.components, builtInComponents)把一些内置组件扩展到Vue.options.components上,Vue的内置组件目前有< keep-alive >、< transition>和< transition-group>组件,这些组件因为是内置组件所以不需要进行注册。

回到主题,mergeOptions这个函数定义在src/core/util/options.js中:

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {...}

mergeOptions主要功能就是把parentchild这两个对象根据一些合并策略,合并成一个新对象并返回。其中比较核心的几步分别是,先递归把extendsmixixns合并到parent上,然后遍历parent,调用mergeField,然后再遍历child,如果key不在parent自身属性上,就调用mergeFieldmergeField函数对于不同的key有着不同的合并策略,比如生命周期函数:

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {...}
                     
LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

其中LIFECYCLE_HOOKS的定义在src/shared/constants.js中:

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured'
]

这里定义了Vue.js所有的钩子函数名称,对于钩子的合并函数,他们的合并函数都是mergeHook函数。这个函数用了一个多层 3 元运算符,逻辑就是如果不存在 childVal,就返回 parentVal;否则再判断是否存在 parentVal,如果存在就把 childVal 添加到 parentVal 后返回新数组;否则返回 childVal 的数组。所以回到 mergeOptions 函数,一旦 parentchild 都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组。通过执行mergeField函数,把合并后的结果保存到options对象中,最终返回它。所以在当前case下,执行完合并后:

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

vm.$options的值差不多是:

vm.$options = {
  components: { },
  created: [
    function created() {
      console.log('parent created') 
    }
  ],
  directives: { },
  filters: { },
  _base: function Vue(options) {
    // ...
  },
  el: "#app",
  render: function (h) {  
    //...
  }
}
组件场景

组件的构造函数时通过Vue.extend继承自Vue的,而代码定义在src/core/global-api/extend.js中。

/**
 * Class inheritance
 */
Vue.extend = function (extendOptions: Object): Function {
  // ...
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  // ...
  // 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)

  // ...
  return Sub
}

这里的的extendOptions对应的就是之前定义的组件对象,它会和Vue.options合并到Sub.options中。

而子组件的初始化过程代码定义在src/core/vdom/create-component.js中:

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

这里的vnode.componentOptions.Ctor就是指向Vue.extend的返回值Sub,所以执行new vnode.componentOptions.Ctor(options) 接着执行 this._init(options),因为options._isComponenttrue,那么合并 options的过程走到了 **initInternalComponent(vm, options)**逻辑。先来看一下它的代码实现,在 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
  }
}

initInternalComponent方法首先执行 const opts = vm. o p t i o n s = O b j e c t . c r e a t e ( v m . c o n s t r u c t o r . o p t i o n s ) ,这里的 v m . c o n s t r u c t i o n 就是子组件的构造函数 S u b ,相当于 v m . options = Object.create(vm.constructor.options),这里的 vm.construction就是子组件的构造函数 Sub,相当于 vm. options=Object.create(vm.constructor.options),这里的vm.construction就是子组件的构造函数Sub,相当于vm.options = Sub.options。

接着又把实例化子组件传入的子组件父 VNode 实例 parentVnode、子组件的父 Vue 实例 parent 保存到 vm.$options 中,另外还保留了 parentVnode 配置中的如 propsData 等其它的属性。

这么看来,initInternalComponent 只是做了简单一层对象赋值,并不涉及到递归、合并策略等复杂逻辑。

因此在当前case下,执行完如下合并后:

initInternalComponent(vm, options)

vm.$options的值差不多是:

vm.$options = {
  parent: Vue /*父Vue实例*/,
  propsData: undefined,
  _componentTag: undefined,
  _parentVnode: VNode /*父VNode实例*/,
  _renderChildren:undefined,
  __proto__: {
    components: { },
    directives: { },
    filters: { },
    _base: function Vue(options) {
        //...
    },
    _Ctor: {},
    created: [
      function created() {
        console.log('parent created') 
      }, function created() {
        console.log('child created') 
      }
    ],
    mounted: [
      function mounted() {
        console.log('child mounted') 
      }
    ],
    data() {
       return {
         msg: 'Hello Vue'
       }
    },
    template: '<div>{{msg}}</div>'
  }
}

总的来说,Vue的初始化阶段对于Options的合并有2种方式,子组件初始化过程通过initInternalComponent方式比外部初始化Vue通过mergeOptions的过程要快,合并完的结果保留在vm.$options中。

生命周期

再Vue实例在被创建之前要经过一系列的初始化过程。如设置数据监听、编译模板、挂载实例DOM、在数据变化时更新DOM等。并且在此期间也会运行一些生命钩子的函数,给用户在一定特定场景下添加他们自己的代码的机会。

在源码中最终执行生命周期的函数都是调用callHook方法,它被定义在src/core/instance/lifecycle中:

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}

这段代码首先根据传入的字符串hook,去拿到**vm.$options[hook]**对应的回调函数数组,然后遍历执行,执行的时候把vm作为函数执行的上下文。

当Vue.js合并options的过程中,各个解读那的生命周期的函数也被合并到$vm.options里,并且是一个数组。因此callhook函数的功能便是调用某个生命周期函数钩子注册的所有回调函数。其中每个生命周期钩子都有其存在的必要性。

beforeCreate & created

beforeCreatecreated 函数都是在实例化Vue的阶段,在_init方法中执行的,它的定义在src/core/instance/init.js中:

Vue.prototype._init = function(options?: Object) {...}

其中beforeCreatecreated 的钩子函数在initState前后被调用,而initState的作用是初始化propsdatamethodswatchcomputed等属性。所以在beforeCreate钩子函数中就不能获取到props、data中定义的值,也不能调用methods中的函数。

在这两个钩子函数执行的时候,并没有渲染DOM,所以我们是访问不到DOM的,总的来说,如果组件在加载的时候需要和后端进行交互,便可以放在两个钩子函数任意一个里面都可以,如果是需要访问propsdata等数据,便需要使用created钩子函数。(在vue-routervuex里其实都混合了beforeCreated钩子函数)

beforeMount & mounted

beforeMount钩子函数发生在mount,也就是DOM挂在之前,它的调用时机是在mountComponent函数中,定义在src/core/instance/lifecycle.js中:

export function mountComponent (
	vm: Component,
	el: ?Element,
	hydrating?: boolean
): Component {...}

在执行vm. _ render() 函数渲染VNode之前,执行了beforeMount钩子函数,在执行完vm._update()把VNode patch到真实DOM后,执行了mounted钩子。这里再执行时会对mounted钩子函数进行一个判断,vm.$vnode如果为null,则表明这不是一次组件的初始化过程,而是通过外部new Vue的过程。那么,对于这个组件,它的mounted的时机会让人困惑。

而可以知道的是,组件的VNode patch到DOM后,会执行invokeInsertHook函数,把insertedVnodeQueue里保存的钩子函数依次执行一遍,invokeInsertHook被定义在src/core/cdom/patch.js中:

function invokeInsertHook (vnode, queue, initial) {
  // delay insert hooks for component root nodes, invoke them after the
  // element is really inserted
  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钩子函数的定义在src/core/vdom/create-component.js中的componentVNodeHooks中:

const componentVNodeHooks = {
  // ...
  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    // ...
  },
}

通过源码,我们可以看到,每个子组件都是在这个钩子函数中执行mounted钩子函数,并且联合insertedVnodeQueue的添加顺序先子后父,所以对于同步渲染的子组件而言,mounted钩子函数的执行顺序也是先子后父。

beforeUpdate & updated

beforeUpdateupdated的钩子函数执行时机是在数据更新的时候。

beforeUpdate的执行时机是在渲染Watcherbefore函数中,

export function mountComponent (
	vm: Component,
	el: ?Element,
	hydrating?: boolean
): Component {...}

在里面有个判断:

new Watcher(vm, updateComponent, noop, {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

如果组件已经mounted了之后才会调用这个钩子函数。

而update执行的时机实在flushSchedulerQueue函数调用的时候,它的定义在src/core/observer/scheduler.js中:

function flushSchedulerQueue () {
  // ...
  // 获取到 updatedQueue
  callUpdatedHooks(updatedQueue)
}
function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated')
    }
  }
}

在其中,updatedQueue是更新之后的watcher数组,而在callUpdatedHooks函数中,它对这些数组进行遍历,只有当watcher为vm._watcher并且已经mounted这两个条件,才会执行updated钩子函数。

在组件mount的过程中会实例化一个渲染Watcher去监听vm上的数据变化来重新渲染,而这段逻辑发生在mountComponent函数执行的时候:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // ...
  // 这里是简写
  let updateComponent = () => {
      vm._update(vm._render(), hydrating)
  }
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  // ...
}

在实例化Watcer的过程中,在它的构造函数里会判断isRenderWather,然后将当前watcher的实例赋值给vm._watcher,定义在src/core/observe/watcher中:

export default class Watcher {
  // ...
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // ...
  }
}

同时,还会把当前watcher实例push到vm. _ watchers中,vm. _ watcher是专门用来监听vm上数据变化然重新渲染的,所以它是一个渲染相关的watcher,所以在callUpdatedHooks函数中,只有vm._watcher的回调执行完毕后,才会执行updated钩子函数。

beforeDestroy & destroyed

beforeDestroydestroyed钩子函数的执行时机在组件销毁的阶段,最后会调用$destroy方法,它被定义在src/core/instance/lifecycle.js中:

Vue.prototype.$destroy = function () {...}

beforeDestroy钩子函数的执行时机实在destroy函数执行最开始的地方,接着执行了一系列的销毁操作,包括从parentchildren中删掉自身,删除watcher,当前渲染的VNode执行销毁钩子函数等,执行完后再调用destroy钩子函数。而在**$destroy**的执行过程中,它又会执行vm. __ patch__(vm. _ vnode, null)出发它子组件的销毁钩子函数,然后一层层的递归调用,所以destroy钩子函数执行顺序和mounted过程一样都是先子后父。

activated & deactivated

activateddeactivated钩子函数是专门为keep-alive组件定制的钩子,作为现阶段粗略了解。

组件注册

再Vue.js中,除了它内置的组件如keep-alivecomponenttransitiontransition-group等,其他用户自定义组件在使用前必须注册,在Vue2中提供了2中组件的注册方式,全局注册和局部注册。

全局注册

如果要注册一个全局组件,可以使用Vue.component(tagName, options):

Vue.component('global-component', {
	//选项
})

而Vue.component函数是在最开始初始化Vue的全局函数的时候,代码在src/core/global-api/assets.js中:

export function initAssetRegisters (Vue: GlobalAPI) {...}

函数首先遍历ASSET_TYPES,得到type后挂载到Vue上。ASSET_TYPES的定义在src/shared/constants

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

所以Vue是初始化了3个全局函数,并且如果typecomponentdefinition是一个对象的话,通过this.options._base.extend,相当于Vue.extend把这个对象转换成一个继承于Vue的构造函数,最后通过this.options【type + ‘s’】【id】 = definition把它挂载到Vue.options.components上。

而每个组件的创建都是通过Vue.extend继承而来,在继承过程中有一段逻辑:

Sub.options = mergeOptions(
	Super.options,
	extendOptions
)

所以它会把Vue.options合并到Sub.options,也就是组件的options上,然后在组件的实例化阶段执行mergeOptions逻辑,把Sub.options.components合并到vm.$options.components上,然后在创建vnode的过程中,会执行**_createElement**方法,它被定义在src/core/vdom/create-element.js中:

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {...}

在里面会有一个判断逻辑:

isDef(Ctor = resolveAsset(context.$options, 'components', tag))

其中resolveAsset的定义在src/core/utils/options.js中:

export function resolveAsset (
  options: Object,
  type: string,
  id: string,
  warnMissing?: boolean
): any {...}

这段逻辑先通过 const assets = options[type]拿到 assets,然后再尝试拿 assets[id],这里有个顺序,先直接使用 id 拿,如果不存在,则把 id变成驼峰的形式再拿,如果仍然不存在则在驼峰的基础上把首字母再变成大写的形式再拿,如果仍然拿不到则报错。这样说明了我们在使用 Vue.component(id, definition) 全局注册组件的时候,id 可以是连字符、驼峰或首字母大写的形式。

转回调用 resolveAsset(context.$options, ‘components’, tag),即拿vm.options.components[tag],这样我们就可以在 resolveAsset 的时候拿到这个组件的构造函数,并作为 createComponent 的钩子的参数。

局部注册

Vue.js也支持局部注册,例如:

import PartComponent from './components/PartComponent'
export default {
  components: {
    PartComponent
  }
}

在组建的Vue实例化阶段有一个合并option的逻辑,把components合并到vm.$options.components上,这样我们就可以在resolveAsset的时候拿到这个组件的构造函数并作为createComponent的钩子参数。

局部注册和全局注册不同的是,只有该类型的组件才可以访问局部注册的子组件,而全局注册是扩展到 Vue.options 下,所以在所有组件创建的过程中,都会从全局的 Vue.options.components 扩展到当前组件的 vm.$options.components 下,这就是全局注册的组件能被任意使用的原因。

异步组件

在我们平时的开发工作中,为了减少首屏代码体积,往往会把一些非首屏的组件设计成异步组件,按需加载。同时Vue也原生支持了一部组件的能力

Vue.component('async-example', function (resolve, reject) {
   // 这个特殊的 require 语法告诉 webpack
   // 自动将编译后的代码分割成不同的块,
   // 这些块将通过 Ajax 请求自动下载。
   require(['./my-async-component'], resolve)
})

在示例中,Vue注册的不再是对象,而是一个工厂函数,函数有两个参数resolvereject,函数内部用 setTimout 模拟了异步,实际使用可能是通过动态请求异步组件的 JS 地址,最终通过执行 resolve 方法,它的参数就是我们的异步组件对象。

在组件的注册逻辑中,由于组件的定义并不是一个普通对象,所以不会执行 Vue.extend 的逻辑把它变成一个组件的构造函数,但是它仍然可以执行到 createComponent函数,createComponent被定义在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 {...}

在源码里,由于我们传入的是Ctor函数,所以不会执行Vue.extend逻辑,因此它的cid是undefined,进入异步组件创建的逻辑中,先执行了:

resolveAsyncComponent(asyncFactory, baseCtor, context)

它被定义在src/core/vdom/helpers/resolve-async-component.js中:

export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>
): Class<Component> | void {...}

这个函数的逻辑略复杂是因为它实际上处理了3种异步组件的创建方式,除了第一种示例以外还有两种,一种是支持Promise创建组件的方式:

Vue.component(
  'async-webpack-example',
  // 该 `import` 函数返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

另一种是高级异步组件:

const AsyncComp = () => ({
  // 需要加载的组件。应当是一个 Promise
  component: import('./MyComp.vue'),
  // 加载中应当渲染的组件
  loading: LoadingComp,
  // 出错时渲染的组件
  error: ErrorComp,
  // 渲染加载中组件前的等待时间。默认:200ms。
  delay: 200,
  // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
  timeout: 3000
})
Vue.component('async-example', AsyncComp)

而根据三种异步组件来分别分析resolveAsyncComponent逻辑将会方便一些。

普通函数异步组件

针对普通函数的情况,前面的if判断可以忽略,它们是配合高级组件使用的,对于factory.contexts的判断,是考虑到多个地方同时初始化一个异步组件,但它实际加载应该只有一次。然后接着进入实际加载逻辑,定义了forceRenderresolvereject函数,而resolverejectonce函数做了一层包装,它被定义在src/shared/util.js中:

/**
 * Ensure a function is called only once.
 */
export function once (fn: Function): Function {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}

once逻辑是传入一个函数返回一个新函数,它利用闭包和一个标志位保证函数只执行一次,是一个很巧妙地方法。接下来执行:

const res = factory(resolve, reject)

这块儿就是执行我们组件的工厂函数,同时把 resolvereject 函数作为参数传入,组件的工厂函数通常会先发送请求去加载我们的异步组件的 JS 文件,拿到组件定义的对象 res后,执行 **resolve(res)**逻辑,它会先执行 factory.resolved = ensureCtor(res, baseCtor)

function ensureCtor (comp: any, base) {
  if (
    comp.__esModule ||
    (hasSymbol && comp[Symbol.toStringTag] === 'Module')
  ) {
    comp = comp.default
  }
  return isObject(comp)
    ? base.extend(comp)
    : comp
}

这个函数是为了保证能找到异步组件JS定义的组件对象,如果它是普通对象,就调用Vue.extend把它转换成一个组件的构造函数。

resolve 逻辑最后判断了sync,显然我们这个场景下 syncfalse,那么就会执行 forceRender 函数,它会遍历 factory.contexts,拿到每一个调用异步组件的实例 vm, 执行 vm.$forceUpdate() 方法,它的定义在src/core/instance/lifecycle.js

Vue.prototype.$forceUpdate = function () {
  const vm: Component = this
  if (vm._watcher) {
    vm._watcher.update()
  }
}

forceUpdate 的逻辑非常简单,就是调用渲染 watcherupdate 方法,让渲染 watcher 对应的回调函数执行,也就是触发了组件的重新渲染。之所以这么做是因为 Vue 通常是数据驱动视图重新渲染,但是在整个异步组件加载过程中是没有数据发生变化的,所以通过执行 $forceUpdate 可以强制组件重新渲染一次。

Promise异步组件

Vue.component(
  'async-webpack-example',
  // 该 `import` 函数返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

webpack 2+ 支持了异步加载的语法糖:() => import(‘./my-async-component’),当执行完 res = factory(resolve, reject),返回的值就是 import(‘./my-async-component’) 的返回值,它是一个 Promise 对象。接着进入 if 条件,又判断了 typeof res.then === ‘function’),条件满足,执行

if (isUndef(factory.resolved)) {
  res.then(resolve, reject)
}

当组件异步加载成功后,执行 resolve,加载失败则执行 reject,这样就非常巧妙地实现了配合 webpack 2+ 的异步加载组件的方式(Promise)加载异步组件。

高级异步组件

由于异步加载组件需要动态加载 JS,有一定网络延时,而且有加载失败的情况,所以通常我们在开发异步组件相关逻辑的时候需要设计 loading 组件和 error 组件,并在适当的时机渲染它们。Vue.js 2.3+ 支持了一种高级异步组件的方式,它通过一个简单的对象配置,帮你搞定 loading 组件和 error 组件的渲染时机,你完全不用关心细节,非常方便。

const AsyncComp = () => ({
  // 需要加载的组件。应当是一个 Promise
  component: import('./MyComp.vue'),
  // 加载中应当渲染的组件
  loading: LoadingComp,
  // 出错时渲染的组件
  error: ErrorComp,
  // 渲染加载中组件前的等待时间。默认:200ms。
  delay: 200,
  // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
  timeout: 3000
})
Vue.component('async-example', AsyncComp)

高级异步组件的初始化逻辑和普通异步组件一样,也是执行 resolveAsyncComponent,当执行完 res = factory(resolve, reject),返回值就是定义的组件对象,显然满足 else if (isDef(res.component) && typeof res.component.then === ‘function’) 的逻辑,接着执行 res.component.then(resolve, reject),当异步组件加载成功后,执行 resolve,失败执行 reject。因为异步组件加载是一个异步过程,它接着又同步执行了如下逻辑:

if (isDef(res.error)) {
  factory.errorComp = ensureCtor(res.error, baseCtor)
}
if (isDef(res.loading)) {
  factory.loadingComp = ensureCtor(res.loading, baseCtor)
  if (res.delay === 0) {
    factory.loading = true
  } else {
    setTimeout(() => {
      if (isUndef(factory.resolved) && isUndef(factory.error)) {
        factory.loading = true
        forceRender()
      }
    }, res.delay || 200)
  }
}
if (isDef(res.timeout)) {
  setTimeout(() => {
    if (isUndef(factory.resolved)) {
      reject(
        process.env.NODE_ENV !== 'production'
          ? `timeout (${res.timeout}ms)`
          : null
      )
    }
  }, res.timeout)
}

先判断 res.error 是否定义了 error 组件,如果有的话则赋值给 factory.errorComp。接着判断 res.loading 是否定义了 loading 组件,如果有的话则赋值给 factory.loadingComp,如果设置了 res.delay 且为 0,则设置 factory.loading = true,否则延时 delay 的时间执行:

if (isUndef(factory.resolved) && isUndef(factory.error)) {
    factory.loading = true
    forceRender()
}

最后判断 res.timeout,如果配置了该项,则在 res.timout 时间后,如果组件没有成功加载,执行 reject

resolveAsyncComponent 的最后有一段逻辑:

sync = false
return factory.loading
  ? factory.loadingComp
  : factory.resolved

如果 delay 配置为 0,则这次直接渲染 loading 组件,否则则延时 delay 执行 forceRender,那么又会再一次执行到 resolveAsyncComponent

那么这时候有几种情况,按逻辑的执行顺序,对不同的情况做判断。

当异步组件加载失败,会执行 reject 函数:

const reject = once(reason => {
  process.env.NODE_ENV !== 'production' && warn(
    `Failed to resolve async component: ${String(factory)}` +
    (reason ? `\nReason: ${reason}` : '')
  )
  if (isDef(factory.errorComp)) {
    factory.error = true
    forceRender()
  }
})

这个时候会把 factory.error 设置为 true,同时执行 forceRender() 再次执行到 resolveAsyncComponent

if (isTrue(factory.error) && isDef(factory.errorComp)) {
  return factory.errorComp
}

那么这个时候就返回 factory.errorComp,直接渲染 error 组件。

当异步组件加载成功,会执行 resolve 函数:

const resolve = once((res: Object | Class<Component>) => {
  factory.resolved = ensureCtor(res, baseCtor)
  if (!sync) {
    forceRender()
  }
})

首先把加载结果缓存到 factory.resolved 中,这个时候因为 sync 已经为 false,则执行 forceRender() 再次执行到 resolveAsyncComponent

if (isDef(factory.resolved)) {
  return factory.resolved
}

如果异步组件加载中并未返回,这时候会走到这个逻辑:

if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
  return factory.loadingComp
}

那么则会返回 factory.loadingComp,渲染 loading 组件。

如果超时,则走到了 reject 逻辑,之后逻辑和加载失败一样,渲染 error 组件。

异步组件patch

回到createComponent

Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor === undefined) {
  return createAsyncPlaceholder(
    asyncFactory,
    data,
    context,
    children,
    tag
  )
}

如果是第一次执行 resolveAsyncComponent,除非使用高级异步组件 0 delay 去创建了一个 loading 组件,否则返回是 undefiend,接着通过 createAsyncPlaceholder 创建一个注释节点作为占位符,它被定义在src/core/vdom/helpers/resolve-async-components.js:

export function createAsyncPlaceholder (
  factory: Function,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag: ?string
): VNode {
  const node = createEmptyVNode()
  node.asyncFactory = factory
  node.asyncMeta = { data, context, children, tag }
  return node
}

实际上就是就是创建了一个占位的注释 VNode,同时把 asyncFactoryasyncMeta 赋值给当前 vnode

当执行 forceRender 的时候,会触发组件的重新渲染,那么会再一次执行 resolveAsyncComponent,这时候就会根据不同的情况,可能返回 loadingerror 或成功加载的异步组件,返回值不为 undefined,因此就走正常的组件 renderpatch 过程,与组件第一次渲染流程不一样,这个时候是存在新旧 vnode 的。

在高级异步组件中,它实现了loadingresolverejecttimeout四种状态。异步组件实现的本质是 2 次渲染,除了 0 delay 的高级异步组件第一次直接渲染成 loading 组件外,其它都是第一次渲染生成一个注释节点,当异步获取组件成功后,再通过 forceRender 强制重新渲染,这样就能正确渲染出我们异步加载的组件了。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

neMew

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值