Vue源码翻译之组件初始化。

时间:2022-03-04 19:42:13

废话不多说。

  

  我们先来看看Vue的入口文件。

 1 import { initMixin } from './init'
 2 import { stateMixin } from './state'
 3 import { renderMixin } from './render'
 4 import { eventsMixin } from './events'
 5 import { lifecycleMixin } from './lifecycle'
 6 import { warn } from '../util/index'
 7 
 8 function Vue (options) {
 9   if (process.env.NODE_ENV !== 'production' &&
10     !(this instanceof Vue)
11   ) {
12     warn('Vue is a constructor and should be called with the `new` keyword')
13   }
14   this._init(options)
15 }
16 
17 initMixin(Vue)
18 stateMixin(Vue)
19 eventsMixin(Vue)
20 lifecycleMixin(Vue)
21 renderMixin(Vue)
22 
23 export default Vue

 

本章先讲第17行开始的initMixin方法 —— 组件初始化 

initMixin   

 1 export function initMixin (Vue: Class<Component>) {
 2   Vue.prototype._init = function (options?: Object) {
 3     const vm: Component = this
 4     // a uid
 5     vm._uid = uid++
 6 
 7     let startTag, endTag
 8     /* istanbul ignore if */
 9     if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
10       startTag = `vue-perf-start:${vm._uid}`
11       endTag = `vue-perf-end:${vm._uid}`
12       mark(startTag)
13     }
14 
15     // a flag to avoid this being observed
16     vm._isVue = true
17     // merge options
18     if (options && options._isComponent) {
19       // optimize internal component instantiation
20       // since dynamic options merging is pretty slow, and none of the
21       // internal component options needs special treatment.
22       initInternalComponent(vm, options)
23     } else {
24       vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm)
25     }
26     /* istanbul ignore else */
27     if (process.env.NODE_ENV !== 'production') {
28       initProxy(vm)
29     } else {
30       vm._renderProxy = vm
31     }
32     // expose real self
33     vm._self = vm
34     initLifecycle(vm)
35     initEvents(vm)
36     initRender(vm)
37     callHook(vm, 'beforeCreate')
38     initInjections(vm) // resolve injections before data/props
39     initState(vm)
40     initProvide(vm) // resolve provide after data/props
41     callHook(vm, 'created')
42 
43     /* istanbul ignore if */
44     if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
45       vm._name = formatComponentName(vm, false)
46       mark(endTag)
47       measure(`vue ${vm._name} init`, startTag, endTag)
48     }
49 
50     if (vm.$options.el) {
51       vm.$mount(vm.$options.el)
52     }
53   }
54 }

  这里记一下:

Vue源码翻译之组件初始化。

  每一个VM对象在实例化的时候,会给一个uid。 然后我们再看后续的代码:

Vue源码翻译之组件初始化。

  这里可以看到有一个属性是_isComponent,这个属性在我们整个程序执行第一次的时候,是不存在的,所以这个地方,在Vue首次实例化Vm的时候,肯定是会跳过的,这个怎么理解?其实很简单,整个项目的根VM对象肯定是非内部组件,你看你项目的main.js,是不是这样一段代码:

new Vue({
  el: '#app',
  router,
  template: '<App/>',
  components: {App}
})

  这个Vue对象并非内部组件,而是一个根组件。那上面那段代码的意思就是,如果options存在,且当前正在执行初始化的是一个组件(根组件_isComponent估计是undefind或者null),那就要进行内部组件的初始化。我们知道,一般一个Vue单页应用个,是一个根vm,然后根vm实例下面有多个VueComponent,这么一个结构。然后我们现在先把initInternalComponent(vm,options)这个部分先跳过,走else的代码——options融合(mergeOptions)。在理解这个mergOptions的之前,还得先解决

Vue源码翻译之组件初始化。

  这段代码有点复杂,js的是有一个原型链的概念,对象的constructor(构造函数)实际上就是指向这个类(不是对象本身,对象本身是类的实例。就像Java中的类,和你new一个类的实例这样的区别),还有,需要注意的是,这个方法的入参,有一个:Class<Component>,这个是Flow的语法,然后这个Component连接的类是在/flow/component.js这个文件里定义的。打开这个类,你就能理解这段代码里面的Ctor.superOptions,Ctor.super这些意思。

  然后这段代码的意义就是:通过循环递归,找到Component这个类的继承链,然后把所有的配置都进行融合。为什么这么做,得后面再说。我们先mark一下这个位置。反正知道这是一个把所有类的继承链的配置属性进行融合的一个过程。然后回归上面说的,mergerOptions,这段代码,

Vue源码翻译之组件初始化。

  做的事情也不难理解,就是把你组件属性中的props、inject,directive等进行规范化,并且还会检验你是否按Vue的照规范写,如果没有,还会报出提示。然后就开始根据不同的合并策略,进行数据的合并,合并什么呢?合并刚才从类的继承链中获取的配置对象及你自己在代码中编写的配置对象(从第一次合并肯定是new Vue(options)这个options,当然这个mergeOptions方法在很多地方都有用到,也有用来合并Component的Options,所以需要对props等属性的规范进行检查)。好了,反正这里,就是进行参数信息的融合。我们进入下一步:

  Vue源码翻译之组件初始化。

initLifecycle

  这里呢,可以看到,就是vm实例进行初始化,并且开始执行写生命周期函数。好了,来看看initLifecycle这个方法,这个方法就有点好玩了。

 1 export function initLifecycle (vm: Component) {
 2   const options = vm.$options
 3 
 4   // locate first non-abstract parent
 5   let parent = options.parent
 6   if (parent && !options.abstract) {
 7     while (parent.$options.abstract && parent.$parent) {
 8       parent = parent.$parent
 9     }
10     parent.$children.push(vm)
11   }
12 
13   vm.$parent = parent
14   vm.$root = parent ? parent.$root : vm
15 
16   vm.$children = []
17   vm.$refs = {}
18 
19   vm._watcher = null
20   vm._inactive = null
21   vm._directInactive = false
22   vm._isMounted = false
23   vm._isDestroyed = false
24   vm._isBeingDestroyed = false
25 }

  我们看看,截出的代码第六行开始,这个函数的入参是一个Component —— 组件,所以你要这样去理解。如果组件的parent存在,并且组件还不是抽象的(其实抽象组件目前好像就只有keep-alive,这个也先mark,回头遇到说这个概念,或者看Vue文档,里面有解释抽象组件的意义。),然后做一件什么事儿呢,就是找到这个Component最上级的,非抽象的父组件,然后让这个父组件添加自己到父组件的children数组当中。并且看第13行代码,这个vm对象做了一个与父vm的关联,从而完善这个tree型的组件树。这样其实在每个组件在初始化的时候,都会给自己的父组件进行关联。从父组件的children中可以找到该父组件下所有的子组件,这就是一颗树形结构的数据对象,或者说是一个金字塔。

  其实对这个vm的理解,我个人是这么理解的,vm自然是一个Vue的实例,就是一个VueComponent的实例,但是这个VueComponent与我们自己编码的Component可不是同一个东西,我们自己编码的那个Component只是一个模板,框架加载了这个模板,然后,根据组件使用的次数实例化对应个数的VueComponent实例。

  为什么这么做呢?现在解释还太早,往后看。这个方法的后续代码就是初始化一些vm的参数,现在也没啥可解释的。反正记住这里给了这些值一个初始化的值,以后有遇到再回头就知道是在这里进行了初始赋值。

initEvents

  好了,进入下一个方法,initEvents(vm)

1 export function initEvents (vm: Component) {
2   vm._events = Object.create(null)
3   vm._hasHookEvent = false
4   // init parent attached events
5   const listeners = vm.$options._parentListeners
6   if (listeners) {
7     updateComponentListeners(vm, listeners)
8   }
9 }

  从这段代码可以看出,首先是为vm的一些参数设置了值,这边可能会有让我们困惑的地方就是_parentListeners这个属性特么又是从哪里来的?不要着急,我们之所以错过了这个方法,是因为我们前面不是有一个_isComponent的判断,然后跳过了initInternalComponent方法么?其实这个listeners是针对父子组件的事件通知的,就是你可能经常会在html标签上写 v-on。那其实这部分代码就是对组件的事件监听,及更新组件的监听事件,并且对v-on设置的参数进行一些规范化,比如你会有一些方法后缀如.capture || .once,把它们从你的配置,转成js对象,这样也才方便后续对事件进行一些操作。  

initRender:

  好,接下来,初始化render:

 1 export function initRender (vm: Component) {
 2   vm._vnode = null // the root of the child tree
 3   vm._staticTrees = null // v-once cached trees
 4   const options = vm.$options
 5   const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
 6   const renderContext = parentVnode && parentVnode.context
 7   vm.$slots = resolveSlots(options._renderChildren, renderContext)
 8   vm.$scopedSlots = emptyObject
 9   // bind the createElement fn to this instance
10   // so that we get proper render context inside it.
11   // args order: tag, data, children, normalizationType, alwaysNormalize
12   // internal version is used by render functions compiled from templates
13   vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
14   // normalization is always applied for the public version, used in
15   // user-written render functions.
16   vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
17 
18   // $attrs & $listeners are exposed for easier HOC creation.
19   // they need to be reactive so that HOCs using them are always updated
20   const parentData = parentVnode && parentVnode.data
21 
22   /* istanbul ignore else */
23   if (process.env.NODE_ENV !== 'production') {
24     defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
25       !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
26     }, true)
27     defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
28       !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
29     }, true)
30   } else {
31     defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
32     defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
33   }
34 }

  在initRender当中,我们可以看到,对vm的一些关于界面绘制相关的属性和方法都会被进行初始化,比如:vm._c及vm.$createElement,这个方法的具体内容,暂时不看,先看主线任务。代码第23行开始,可以看到$attrs及$listeners这两个对象的数据从父Vnode中来。而这两个属性的解释,也可以从Vue的Api文档中找到:$attrs && $listeners

  好了这里我来插播一个概念:我们刚才在initLifecycle的地方有接触到Component有一个树形结构,但是我们也都知道一个问题,就是Vue里的组件,是可以复用的,如果在一个页面中同时出现两个相同的组件,这两个组件的数据是相互隔离的,并不会互相干涉,这是因为实际上在Vue当中,真正跟数据相关联的,不是咱们自己写的那个Component(*.vue),而是通过Component(*.vue)组件创建的VueComponent实例。所以你写的组件,其实就是相当于一个VueComponent的模板,框架根据模板,创建一个或多个VueComponent,而这些VueComponent相互之间都是独立的,否则当你使用v-for并且循环体是一个组件的话,你就会发现,循环后的每个组件值都是一样的,而且改一个组件的数据,其他的也都会跟着改。这里非常的有意思。你编写的Component,其实只是一个模板,实际被创建出来的是Vdom,这个概念一定要分清楚。要理解到Component(*.vue)与VueComponent的关系其实真的不太容易,我是按照Vue的原理自己实现一个框架走到这一步的时候,才理解了其中的深意,而这部分,可以从/src/core/vdom/create-component.js这个文件export出的createComponent()方法中看出细节。

  还有VueComponent是包含一个或多个Vnode(一个HTML节点就会对应生成一个Vnode)组成的,这段代码中的vm代表的是一个组件,一个组件是有好多个Vnode组成的一个Vdom,你可能会问,为什么是从parentVnode当中获取data的attrs?所谓attrs其实就是html'标签上的attributes(元素),我们回想一下,当我们制作好一个组件后,是不是使用类似<component-name/>的自定义标签来代表在html文档中的某个地方使用组件?然后你要给这个组件所配置的所有属性,都是在这个<component-name/>标签上去写,比如传入一个title属性:<component-name title="testAttrs"/>所以其实这个自定义标签,就是你自己编写的组件的父节点,而这个attrs属性,也只可能从你的父节点中获取。简单讲一下这个$attr的作用,比如你要写一个列表组件,肯定是先有一个<custom-ul/>然后再来<custom-li/>,但是我们希望这两个组件要配合一起用,然后我们又希望,只需要把数组数据设置给<custom-ul/>这个标签而省去写<custom-li/>,那你就要在外层ul组件上传递给li组件的数据,为了区分这个数据其实是给内部li使用的,而ul这个外层组件其实是不需要用的,不需要自然不用定义props,而我们又想要把数据传递进去给内部的li组件用,那就可以使用$attrs来实现了。当然这个例子不一定恰当,但是应该能解释的清除这个特性。

  继续我们看31行和32行,这地方开始对这两个参数进行响应式配置。这里是整个Vue响应式原理的核心。我已经迫不及待的想要解析这部分的代码,但是这个要讲还挺占篇幅,所以放到后面,反正同学们只要知道,通过了这个defineReacttive方法之后,这个数据就会具有响应式的特性,数据变动就会触发页面重绘及变更。好了,这个render的初始化就告一段落,我们进入下一段,下面紧接着,是一个生命周期函数,就是beforeCreate,我们从Vue的文档中可以看到,在这个生命周期函数当中,其实数据观测是尚未设定的,从源码中我们也可以看到,执行到beforeCreate这个步骤的时候,其实Vue只是做了一些数据、实例初始化操作,这也是为什么在beforeCreate方法中如果对数据进行更改,你会发现数据并没有如你所愿的变更。

initInjections/initProvide

  这两个函数要一起说,因为这两个函数所对应的inject和provider是成对出现的。这两个函数其实内容也不会很复杂,来看第一个

 1 export function initInjections (vm: Component) {
 2   const result = resolveInject(vm.$options.inject, vm)
 3   if (result) {
 4     observerState.shouldConvert = false
 5     Object.keys(result).forEach(key => {
 6       /* istanbul ignore else */
 7       if (process.env.NODE_ENV !== 'production') {
 8         defineReactive(vm, key, result[key], () => {
 9           warn(
10             `Avoid mutating an injected value directly since the changes will be ` +
11             `overwritten whenever the provided component re-renders. ` +
12             `injection being mutated: "${key}"`,
13             vm
14           )
15         })
16       } else {
17         defineReactive(vm, key, result[key])
18       }
19     })
20     observerState.shouldConvert = true
21   }
22 }

  这部分的代码,开始就是resolveInject,这个方法其实也就是从配置当中读取inject配置,

  Vue源码翻译之组件初始化。

  这段代码的意思是,是否是ES2015的Symbol?如果是则通过Reflect来获取key,Object.getOwnPropertyDescriptor的方法时获取对象某个值的属性描述,这一小段的意思是,通过ownKeys得到了一个key数组,但是这个数组要使用filter过滤,只留下“可枚举”的特性的Key,不可枚举的就不需要留下了,至于这么做的原因,嗯,如果有兴趣,可以去看看相关知识,我就不拓展了,否则要跑题了。至于Object.keys就没什么好解释了,这个方法直接就返回的就是具有可枚举特性的key值数组。

Vue源码翻译之组件初始化。

  这段代码的意思是,从当前的vm向上查找,如果从父级vm上找到了对应的provide那就取对应的值,然后跳出循环,我们观察一下,这个while只有在两种情况下回跳出循环,一个是source为undefind或null,另一个就是找到了对应provide的值,那如果跳出循环的时候,发现父级的vm都undefind了(就是代码中if(!source)这个判断)那说明这个值还没找到,那就要看inject的配置当中,是否有默认配置,其实这个逻辑可以看Vue的文档 provide/inject 就能理解这个逻辑的意义,provide是能注入到所有子组件当中,这里循环向上查找provide就是这个所谓的注入到所有子组件的道理。好了,回到上层代码,剩下的,其实就是把数据进行响应化,上面讲过就不多说了。至于provide,文档里说了,他必须是Object或者返回Object的函数,所以initProvide其实也没有什么特别的,基本就是一看就懂。

  至于两个函数一个在initState前初始化,一个在后初始化,按照我的理解,因为provide不是跟自己组件使用,而是给子组件使用,而inject是给当前组件自己用的,并且provide的数据还有可能是从其他props或data传入,这些数据都是需要经过initState进行可响应化。

initState

  接下来说说这个initState

 

 1 export function initState (vm: Component) {
 2   vm._watchers = []
 3   const opts = vm.$options
 4   if (opts.props) initProps(vm, opts.props)
 5   if (opts.methods) initMethods(vm, opts.methods)
 6   if (opts.data) {
 7     initData(vm)
 8   } else {
 9     observe(vm._data = {}, true /* asRootData */)
10   }
11   if (opts.computed) initComputed(vm, opts.computed)
12   if (opts.watch && opts.watch !== nativeWatch) {
13     initWatch(vm, opts.watch)
14   }
15 }

 

  其实这个方法也不会很复杂,首先就是初始化Props,其实就是对Props数据进行一些校验或赋值,来看一下initProps

Vue源码翻译之组件初始化。

  在这段代码中,我们可以看到,如果vm是根节点或根vm,则此vm的props,需要进行响应化转换,但是如果他不是根vm,则不需要进行响应化转换。这是为什么呢?因为,我们的props都是从父级组件传进来的,而父级组件传进来的值大多是定义在父级组件的data属性,而data属性是必须响应化,所以到了子vm自然就不需要再做一次响应化处理。(就算传入子组件的值是来自父组件的props,并且向上依然如此,当追溯到根vm时,根vm的props是经过了响应化的,所以最终依然还是会成为可响应的属性。)

  这剩下initState剩下的部分倒还真的没啥好说,都很容易就能看明白。

    接下来,就是 stateMixin了。