vue源码分析(十七) 初始化

10/17/2018 vue源码分析面试

# 1. 概述

​ 上一章节我们已经分析了 vm.$options 这个属性了,它才是用来做一系列初始化工作的最终选项,那么接下来我们就继续看 _init 方法中的代码,继续了解 Vue 的初始化工作。

​ 在分析之前,我们还是以上一章的案例进行讲解,如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>深入响应式原理-响应式对象</title>
  <script src="../../dist/vue.js"></script>
</head>
<body>
  <div id="app"></div>
    <script>
      const child = {
        template: `<div v-on:click="getName"> {{ name }} </div>`,
        data() {
          return { name: 'robin' }
        },
        methods: {
          getName() {
            console.log(this.name)
          }
        }
      }
      
      new Vue({
        el: '#app',
        components: {child},
        template: `<child/>`,
        data: {
          msg: 'message'
        }
      })
    </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 2. initProxy

​ 我们回到 _init 函数中,继续往下看,代码如下:

​ 源码目录:src/core/instance/init.js

/* 
 * istanbul ignore else 
 * 参考:https://segmentfault.com/a/1190000014824359
 */
if (process.env.NODE_ENV !== 'production') {
  initProxy(vm)
} else {
  vm._renderProxy = vm
}
1
2
3
4
5
6
7
8
9

​ 这段代码是一个 if...else语句块,如果是开发环境的话则执行 initProxy(vm) 函数,如果在生产环境则直接在实例上添加 _renderProxy 实例属性,该属性的值就是当前实例。

​ 接下来我们看看在开发环境中,initProxy 的内容,如下:

源码目录:`scr/core/instance/proxy.js` 
let initProxy

if (process.env.NODE_ENV !== 'production') {
  const allowedGlobals = function(){}
  const warnNonPresent = function(){}
  const warnReservedPrefix = function(){}
  const hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy)
  const hasHandler = function(){}
  const getHandler = function(){}
  initProxy = function initProxy (vm) {}
}

export { initProxy }
1
2
3
4
5
6
7
8
9
10
11
12
13

​ 上面是我们把 proxy.js 中的代码,用伪代码的方式梳理了一下,从上面代码可以知道在一开始声明了 initProxy 但没有赋值,所以为 undefined ,如果当前环境为开发环境,才会执行 if 语句进行初始化。即在生产环境 initProxyundefined,在开发环境 initProxy 才是一个函数。

​ 我们继续看 if 语句中的代码,如下:

​ 源码目录:scr/core/instance/proxy.js

if (process.env.NODE_ENV !== 'production') {
  const allowedGlobals = makeMap(
    'Infinity,undefined,NaN,isFinite,isNaN,' +
    'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
    'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
    'require' // for Webpack/Browserify
  )
  // 省略...
}
1
2
3
4
5
6
7
8
9

​ 从上面代码我们可以看到 allowedGlobals 实际上是通过 makeMap 生成的函数,所以 allowedGlobals 函数的作用是判断给定的 key 是否出现在上面字符串中定义的关键字中的。这些关键字都是在 js 中可以全局访问的。

​ 我们继续看往下看,源码如下:

​ 源码目录:scr/core/instance/proxy.js

if (process.env.NODE_ENV !== 'production') {
  // 省略...
  const warnNonPresent = (target, key) => {
    warn(
      `Property or method "${key}" is not defined on the instance but ` +
      'referenced during render. Make sure that this property is reactive, ' +
      'either in the data option, or for class-based components, by ' +
      'initializing the property. ' +
      'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
      target
    )
  }

  const warnReservedPrefix = (target, key) => {
    warn(
      `Property "${key}" must be accessed with "$data.${key}" because ` +
      'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
      'prevent conflicts with Vue internals. ' +
      'See: https://vuejs.org/v2/api/#data',
      target
    )
  }
  // 省略...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

warnNonPresent 这个函数就是通过 warn 打印一段警告信息,警告信息提示你 “在渲染的时候引用了 key,但是在实例对象上并没有定义 key 这个属性或方法”。

warnReservedPrefix 这个函数就是通过 warn 打印一段警告信息,作用是提示开发者以 _$ 开头的 property 不会Vue 实例代理,因为它们可能和 Vue 内置的 propertyAPI 方法冲突。

​ 我们继续看往下看,源码如下:

​ 源码目录:scr/core/instance/proxy.js

if (process.env.NODE_ENV !== 'production') {
  // 省略...
  const hasProxy =
    typeof Proxy !== 'undefined' && isNative(Proxy)
  // 省略...
}
1
2
3
4
5
6

​ 上面代码的作用是判断当前宿主环境是否支持原生 Proxy

​ 我们继续看往下看,源码如下:

​ 源码目录:scr/core/instance/proxy.js

if (process.env.NODE_ENV !== 'production') {
  // 省略...
  if (hasProxy) {
    const isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact')
    config.keyCodes = new Proxy(config.keyCodes, {
      set (target, key, value) {
        if (isBuiltInModifier(key)) {
          warn(`Avoid overwriting built-in modifier in config.keyCodes: .${key}`)
          return false
        } else {
          target[key] = value
          return true
        }
      }
    })
  }
  // 省略...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

​ 上面的代码首先检测宿主环境是否支持 Proxy,如果支持的话才会执行里面的代码,内部的代码首先使用 makeMap 函数生成一个 isBuiltInModifier 函数,该函数用来检测给定的值是否是内置的事件修饰符。

​ 然后为 config.keyCodes 设置了 set 代理,其目的是防止开发者在自定义键位别名的时候,覆盖了内置的修饰符。

​ 我们继续看往下看,源码如下:

​ 源码目录:scr/core/instance/proxy.js

if (process.env.NODE_ENV !== 'production') {
  // 省略...
  const hasHandler = {
    has (target, key) {
      const has = key in target
      const isAllowed = allowedGlobals(key) ||
        (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
      if (!has && !isAllowed) {
        if (key in target.$data) warnReservedPrefix(target, key)
        else warnNonPresent(target, key)
      }
      return has || !isAllowed
    }
  }

  const getHandler = {
    get (target, key) {
      if (typeof key === 'string' && !(key in target)) {
        if (key in target.$data) warnReservedPrefix(target, key)
        else warnNonPresent(target, key)
      }
      return target[key]
    }
  }

  initProxy = function initProxy (vm) {
    if (hasProxy) {
      // determine which proxy handler to use
      const options = vm.$options
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

​ 上面这段代码我们先看看 initProxy 的实现, initProxy 的作用实际上就是对实例对象 vm 的代理,通过原生的 Proxy 实现。

initProxy 接收一个参数为 Vue 实例,然后是一个 if...else 语句块,如果当前环境支持 Proxy 则执行 if 语句块,如果当前环境不支持 Proxy 则执行else 语句块,直接在 vm 对象上添加了 _renderProxy 属性,值为当前实例。

​ 当前环境支持 Proxy 的情况下,首先将 vm.$options 的引用赋值给 options,然后判断 options.renderoptions.render._withStripped 是否都为真时,handlers 的值为 getHandler,否则都为 hasHandler ,在们当前的案例中 options.render._withStripped 一般情况下这个条件都会为假,也就是使用 hasHandler 作为代理配置。

​ 我们再来看看 hasHandlerhasHandler 方法的应用场景在于查看 vm 实例是否拥有某个属性,首先使用 in 操作符判断该属性是否在 vm 实例上存在,接下来通过判断 keyallowedGlobals 之内即访问了全局属性也是不允许的,或者 key 是以下划线 _ 开头的字符串并且不在 target.$data 对象中的字符串,则为真。接下来如果 hasisAllowed 都为假时,报一个警告。

​ 报警告也是分两种类型,如上面的代码,首先判断 key 是否在 target.$data 对象中,如果存在说明存在以 _$ 开头的 property,如果不存在说明属性名不存在。

​ 我们再来看看 getHandlergetHandler 的作用是设置的 get 拦截,其最终实现的效果无非就是检测到访问的属性不存在就给你一个警告。警告的类型和 hasHandler 中一样,这里就不重复说明了。

​ 最后我们回到 initProxy 看看这句 vm._renderProxy = new Proxy(vm, handlers),当前环境支持 Proxy 的情况下,那么将会使用 Proxyvm 做一层代理,代理对象赋值给 vm._renderProxy,所以今后对 vm._renderProxy 的访问,如果有代理那么就会被拦截。

​ 至此我们已经分析完了 initProxy ,其作用就是设置渲染函数的作用域代理,其目的是为我们提供更好的提示信息。

# 3. initLifecycle

​ 我们回到 _init 函数中,继续往下看,代码如下:

​ 源码目录:src/core/instance/init.js

vm._self = vm 
initLifecycle(vm) 
1
2

​ 首先执行了 vm._self = vm 语句,这句话在 Vue 实例对象 vm 上添加了 _self 属性,指向 Vue 实例对象本身。然后是执行 initLifecycle(vm) ,作用是初始化生命周期,接下来我们来看看 initLifecycle 的定义,如下:

源码目录:`scr/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

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

  vm._watcher = null
  vm._inactive = null 
  vm._directInactive = false
  vm._isMounted = false 
  vm._isDestroyed = false 
  vm._isBeingDestroyed = false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

​ 接下来我们将 initLifecycle 函数中的代码拆开来分析一下,首先定义 options 常量,它是 vm.$options 的引用,如下:

const options = vm.$options
1

​ 我们继续往下看,代码如下:

// locate first non-abstract parent
let parent = options.parent
1
2

​ 这句代码是的作用是获取父实例,即获取 options.parent ,前面我们分析过 optionsvm.$options 的引用,所以 options.parent 等于 vm.$options.parent

​ 这里涉及到了父子关系,逻辑比较绕,我们用一个案例来说明一下,如下:

const child = {
  template: `<div> {{ name }} </div>`,
  data() {
    return { name: 'robin' }
  }
}
var vm = new Vue({
  el: '#app',
  components: { child },
  template: `<child/>`,
  data: {
    msg: 'this is message'
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

​ 在我们这个案例中,父实例为 options.parent = vm.$options.parent 等于 new Vue 的实例化对象,子实例为 Vue.extend 实例化的对象。

​ 我们继续往下看,代码如下:

if (parent && !options.abstract) {
  while (parent.$options.abstract && parent.$parent) {
    parent = parent.$parent
  }
  parent.$children.push(vm)
}
1
2
3
4
5
6

​ 这段代码是一个 if 语句,判断条件如果父实例存在,且当前实例不是抽象的,则执行 if 语句。

​ 这里我们需要解释一点的是关于抽象组件,如果一个组件的 $options.abstract,选项为 true,则该组件是抽象的,那么通过该组件创建的实例也都是抽象的。

​ 抽象组件一个最显著的特点就是它们一般不渲染真实 DOMVue 内置了一些全局组件比如 keep-alive 或者 transition,我们知道这两个组件它是不会渲染 DOM 至页面的,但他们依然给我提供了很有用的功能。抽象组件还有一个特点,就是它们不会出现在父子关系的路径上

​ 我们在回到 if 语句,如果 !options.abstract 为真,说明当前实例是抽象组件,不会执行 if 语句,直接设置 vm.$parentvm.$root 的值,跳过 if 语句块的结果将导致该抽象实例不会被添加到父实例的 $children 中。

​ 如果 !options.abstract 为不真,说明说明当前实例不是抽象组件,则执行 if 语句,接下来是一个 while 循环目的就是沿着父实例链逐层向上寻找到第一个不抽象的实例作为 parent(父级),并且在找到父级之后将当前实例添加到父实例的 $children 属性中,这样最终的目的就达成了。

​ 我们继续往下看,代码如下:

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

vm._watcher = null
vm._inactive = null 
vm._directInactive = false
vm._isMounted = false 
vm._isDestroyed = false 
vm._isBeingDestroyed = false
1
2
3
4
5
6
7
8
9

​ 这段代码的作用是在当前实例上添加一系列属性。其中 $children$refs 都是我们熟悉的实例属性,他们都在 initLifecycle 函数中被初始化,其中 $children 被初始化为一个数组,$refs 被初始化为一个空 json 对象,除此之外,还定义了一些以 _ 开始的属性供内部使用,需要注意的是这些属性是在 initLifecycle 函数中定义的,那么属性会与生命周期有关。这样 initLifecycle 函数我们就分析完毕了。

​ 说明:关于抽象组件我们会在组件化章节具体分析,不了解的同学可以先参考 这里 (opens new window)

# 4. initEvents

​ 我们回到 _init 函数中,继续往下看,代码如下:

​ 源码目录:src/core/instance/init.js

initEvents(vm)
1

​ 然后是执行 initEvents(vm) ,作用是初始化事件,接下来我们来看看 initEvents 的定义,如下:

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

​ 首先在 vm 实例对象上添加两个实例属性 _events_hasHookEvent,其中 _events 被初始化为一个空对象,_hasHookEvent 的初始值为 false

​ 接下来是定义常量 listeners ,它是 vm.$options._parentListeners 的引用。 _parentListeners 是在函数 createComponentInstanceForVnode 中添加到 options 上的,它在 core/vdom/create-component.js 文件中,也就是说在创建子组件实例的时候才会有这个参数选项,由于在我们当前的案例中获取到的 _parentListenersundefined,所以现在我们不做深入讨论,在组件化章节再来分析。

# 5. initRender

​ 我们回到 _init 函数中,继续往下看,代码如下:

​ 源码目录:src/core/instance/init.js

initRender(vm) 
1

​ 然后是执行 initRender(vm) ,作用是初始化渲染,接下来我们来看看 initRender 的定义,如下:

源码目录:`scr/core/instance/events.js` 
export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options 
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  const parentData = parentVnode && parentVnode.data

  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

​ 由于这部分代码比较多,接下来我们将 initRender 函数中的代码拆开来分析一下,首先看一下如下代码:

vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
1
2

​ 首先在 Vue 实例对象上添加两个实例属性,即 _vnode_staticTrees,并初始化为 null

​ 我们继续往下看,代码如下:

const options = vm.$options 
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
1
2
3
4
5

​ 这段代码是在 Vue 实例对象上添加三个实例属性,即 $vnode$slots$scopedSlots

​ 说明:关于上面代码的作用我们在后面插槽的章节再来分析。

​ 关于$slots 请到 这里 (opens new window) 学习。

​ 关于$scopedSlots 请到 这里 (opens new window) 学习。

​ 我们继续往下看,代码如下:

// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
1
2
3
4
5
6
7
8

​ 这段代码在 Vue 实例对象上添加了两个方法:vm._cvm.$createElement,这两个方法实际上是对内部函数 createElement 的包装。

​ 对于以上两个属性的理解我们举个例子来看看,如下代码:

render: function (createElement) {
  return createElement('h2', 'Title')
}
1
2
3

​ 我们知道,渲染函数的第一个参数是 createElement 函数,该函数用来创建虚拟节点,实际上你也完全可以这么做:

render: function () {
  return this.$createElement('h2', 'Title')
}
1
2
3

​ 上面两段代码是完全等价的。而对于 vm._c 方法,则用于编译器根据模板字符串生成的渲染函数的。vm._cvm.$createElement 的不同之处就在于调用 createElement 函数时传递的第六个参数不同,至于这么做的原因,我们放到后面章节讲解。有一点需要注意,即 $createElement 看上去像对外暴露的接口,但其实文档上并没有体现。

​ 我们继续往下看,代码如下:

// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data

/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
  defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
    !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
  }, true)
  defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
    !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
  }, true)
} else {
  defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
  defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

​ 这段代码的主要作用就是在 Vue 实例对象上定义两个属性:vm.$attrs 以及 vm.$listeners。在开发环境中调用 defineReactive 函数时传递的第四个参数是一个函数,实际上这个函数是一个自定义的 setter,这个 setter 会在你设置 $attrs$listeners 属性时触发并执行。

​ 自定义的 setter中,当 !isUpdatingChildComponent 成立时,会提示你 $attrs$listeners 是只读属性,你不应该手动设置它的值。

​ 说明:关于$attrs 请到 这里 (opens new window) 学习。

​ 关于$listeners 请到 这里 (opens new window) 学习。

# 6. initInjections

​ 我们回到 _init 函数中,继续往下看,代码如下:

​ 源码目录:src/core/instance/init.js

initInjections(vm) 
1

​ 我们继续往下看,代码如下:

​ 源码目录 src/core/instance/inject.js

export function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm) 
  // 省略...
}
1
2
3
4

initInjections 函数接收组件实例对象作为参数,在 initInjections 函数内部首先定义了 result 常量,并且我们能够注意到接下来的 if 条件语句的判断条件就是 result 常量,只有 result 为真的情况下才会执行 if 语句块内的代码。我们首先来看一下 result 常量的值是什么,可以看到它是 resolveInject 函数的返回值。我们知道子组件中通过 inject 选项注入的数据其实是存放在其父代组件实例的 vm._provided 属性中,实际上 resolveInject 函数的作用就是根据当前组件的 inject 选项去父代组件中寻找注入的数据,并将最终的数据返回。

​ 我们接下来先看看 resolveInject 函数,代码如下:

​ 源码目录 src/core/instance/inject.js

export function resolveInject (inject: any, vm: Component): ?Object {
  // 省略...
}
1
2
3

resolveInject 函数接收两个参数,分别是 inject 选项以及组件实例对象。我们可以看到在 initInjections 函数中调用 resolveInject 函数时所传递的参数分别是 vm.$options.inject 以及 vm

​ 我们继续往下看,代码如下:

​ 源码目录 src/core/instance/inject.js

export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    const result = Object.create(null)
    // 省略...
    return result
  }
}
1
2
3
4
5
6
7
8

​ 我们能够看到 if 语句块内首先定义一个常量,值为一个空对象,的最后一句代码将 result 返回,该 result 就是最终寻找到的注入的数据。如果 inject 选项不存在则返回 undefined

​ 我们继续往下看,代码如下:

​ 源码目录 src/core/instance/inject.js

export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    // 省略...
    const keys = hasSymbol 
      ? Reflect.ownKeys(inject) 
      : Object.keys(inject)
    // 省略...
  }
}
1
2
3
4
5
6
7
8
9

​ 接着定义了 keys 常量,它的值是一个数组,即由 inject 选项对象所有键名组成的数组,我们在前面章节规范化中分析过 inject 选项被规范化后将会是一个对象,并且该对象必然会包含 from 属性。例如 inject 选项是一个字符串数组:

inject: ['data1', 'data2']

// 规范化后
{
  'data1': { from: 'data1' },
  'data2': { from: 'data2' }
}
1
2
3
4
5
6
7

​ 如果 inject 选项是一个对象,那么这个对象你可以有好几种写法:

inject: {
  // 第一种写法
  data1: 'd1',
  // 第二种写法
  data2: {
    someProperty: 'someValue'
  }
}

// 规范化后
inject: {
  'data1': { from: 'd1' },
  'data2': { from: 'data2', someProperty: 'someValue' }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

​ 可以看到被规范化后的每个 inject 选项值也都是一个对象,并且都包含 from 属性。同时我们注意到 someProperty 属性被保留了,所以你完全可以把 someProperty 属性替换成 default 属性:

inject: {
  data1: {
    default: 'defaultValue'
  }
}
1
2
3
4
5

​ 这就是 Vue 文档中提到的可以使用 default 属性为注入的值指定默认值。

​ 现在我们知道 keys 常量中保存 inject 选项对象的每一个键名,但我们注意到这里有一个对 hasSymbol的判断,其目的是保证 Symbol 类型与 Reflect.ownKeys 可用且为宿主环境原生提供,如果 hasSymbol 为真,则说明可用,此时会使用 Reflect.ownKeys 获取 inject 对象中所有可枚举的键名,否则使用 Object.keys 作为降级处理。实际上 Reflect.ownKeys 配合可枚举过滤等价于 Object.keysObject.getOwnPropertySymbols 配合可枚举过滤之和,其好处是支持 Symbol 类型作为键名,当然了这一切都建立在宿主环境的支持之上,所以 Vue 官网中提到了**inject 选项对象的属性可以使用 ES2015 Symbols 作为 key,但是只在原生支持 SymbolReflect.ownKeys 的环境下可工作**。

​ 我们继续往下看,代码如下:

​ 源码目录 src/core/instance/inject.js

export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    // 省略...
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i] 
      // #6574 in case the inject object is observed...
      if (key === '__ob__') continue
      const provideKey = inject[key].from 
      let source = vm
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey] 
          break
        }
        source = source.$parent
      }
      if (!source) {
        if ('default' in inject[key]) {
          const provideDefault = inject[key].default
          result[key] = typeof provideDefault === 'function'
            ? provideDefault.call(vm)
            : provideDefault
        } else if (process.env.NODE_ENV !== 'production') {
          warn(`Injection "${key}" not found`, vm)
        }
      }
    }
    // 省略...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

​ 接下来的代码使用 for 循环,用来遍历刚刚获取到的 keys 数组,在循环内部首先定义了两个常量以及一个变量,其中 key 常量就是 keys 数组中的每一个值,即 inject 选项的每一个键值,provideKey 常量保存的是每一个 inject 选项内所定义的注入对象的 from 属性的值,我们知道 from 属性的值代表着 vm._provided 数据中的每个数据的键名,所以 provideKey 常量将用来查找所注入的数据。最后定义了 source 变量,它的初始值是当前组件实例对象。

​ 接下来将开启一个 while 循环,用来查找注入数据的工作,我们知道 source 是当前组件实例对象,在循环内部有一个 if 条件语句,该条件检测了 source._provided 属性是否存在,并且 source._provided 对象自身是否拥有 provideKey 键,如果有则说明找到了注入的数据:source._provided[provideKey],并将它赋值给 result 对象的同名属性。有的同学会问:“source 变量的初始值为当前组件实例对象,那么如果在当前对象下找到了通过 provide 选项提供的值,那岂不是自身给自身注入数据?”。大家不要忘了 inject 选项的初始化是在 provide 选项初始化之前的,也就是说即使该组件通过 provide 选项提供的数据中的确存在 inject 选项注入的数据,也不会有任何影响,因为在 inject 选项查找数据时 provide 提供的数据还没有被初始化,所以当一个组件使用 provide 提供数据时,该数据只有子代组件可用。

​ 那么如果 if 判断条件为假,重新赋值 source 变量,使其引用父组件,以及类推就完成了向父代组件查找数据的需求,直到找到数据为止。但是如果一直找到了根组件,但依然没有找到数据,将会执行下面代码,如下:

​ 我们继续往下看,代码如下:

​ 源码目录 src/core/instance/inject.js

export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    // 省略...
    for (let i = 0; i < keys.length; i++) {
      // 省略...
      if (!source) {
        if ('default' in inject[key]) {
          const provideDefault = inject[key].default
          result[key] = typeof provideDefault === 'function'
            ? provideDefault.call(vm)
            : provideDefault
        } else if (process.env.NODE_ENV !== 'production') {
          warn(`Injection "${key}" not found`, vm)
        }
      }
    }
    // 省略...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

​ 我们知道根组件实例对象的 vm.$parent 属性为 null,所以如上 if 条件语句的判断条件如果成立,说明一直寻找到根组件也没有找到要的数据,此时需要查看 inject[key] 对象中是否定义了 default 选项,如果定义了 default 选项则使用 default 选项提供的数据作为注入的数据,否则在非生产环境下会提示开发者未找到注入的数据。另外我们可以看到 default 选项可以是一个函数,此时会通过执行该函数来获取注入的数据。

​ 最后如果查询到了数据,resolveInject 函数会将 result 作为返回值返回,并且 result 对象的键就是注入数据的名字,result 对象每个键的值就是注入的数据。

​ 我们回到 initInjections 函数继续往下看,代码如下:

​ 源码目录 src/core/instance/inject.js

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

initInjections 函数通过 resolveInject 函数取得了注入的数据,并赋值给 result 常量,我们知道 result 常量的值有可能是不存在的,所以需要一个 if 条件语句对 result 进行判断,当条件为真时说明成功取得注入的数据,此时会执行 if 语句块内的代码。

​ 在 if 语句块内,通过遍历 result 常量并调用 defineReactive 函数在当前组件实例对象 vm 上定义与注入名称相同的变量,并赋予取得的值。这里有一个对环境的判断,在非生产环境下调用 defineReactive 函数时会多传递一个参数,即 customSetter,当你尝试设置注入的数据时会提示你不要这么做。

​ 另外大家也注意到了在使用 defineReactive 函数为组件实例对象定义属性之前,调用了 toggleObserving(false) 函数关闭了响应式定义的开关,之后又将开关开启:toggleObserving(true)关于toggleObserving我们将在初始化props中详细分析,这么做将会导致使用 defineReactive 定义属性时不会将该属性的值转换为响应式的,所以 Vue 文档中提到了:

提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

​ 当然啦,如果父代组件提供的数据本身就是响应式的,即使 defineReactive 不转,那么最终这个数据也还是响应式的。

​ 说明:更多内容查看 provide/inject (opens new window)

# 7. initState

​ 我们回到 _init 函数中,继续往下看,代码如下:

​ 源码目录:src/core/instance/init.js

initState(vm) 
1

​ 接下来执行 initState(vm) ,作用是初始化 props 属性、data 属性、methods 属性、computed 属性、watch 属性,接下来我们来看看 initState 的定义,如下:

源码目录:`scr/core/instance/state.js` 
export function initState (vm: Component) {
  vm._watchers = [] 
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed) 
  // Firefox has a "watch" function on Object.prototype
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch) 
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

​ 首先在 Vue 实例对象添加一个属性 vm._watchers = [],其初始值是一个数组,这个数组将用来存储所有该组件实例的 watcher 对象。随后定义了常量 opts,它是 vm.$options 的引用。

​ 如果 opts.props 存在,即选项中有 props,那么就调用 initProps 初始化 props 选项。

​ 如果 opts.methods 存在,则调用 initMethods 初始化 methods 选项。

​ 接下来判断 data 选项是否存在,如果存在则调用 initData 初始化 data 选项,如果不存在则直接调用 observe 函数观测一个空对象:{}

​ 如果 opts.computed 存在,即选项中有 computed,那么就调用 initComputed 初始化 computed 选项。

​ 最后判断 opts.watch 是否存在,并且 opts.watch 不是原生的 watch 对象,即选项中有 watch,那么就调用 initWatch 初始化 watch 选项。

​ 说明:前面的章节中我们提到过,这是因为在 Firefox 中原生提供了 Object.prototype.watch 函数,所以即使没有 opts.watch 选项,如果在火狐浏览器中依然能够通过原型链访问到原生的 Object.prototype.watch。但这其实不是我们想要的结果,所以这里加了一层判断避免把原生 watch 函数误认为是我们预期的 opts.watch 选项。

# 8. initProvide

​ 我们回到 _init 函数中,继续往下看,代码如下:

​ 源码目录:src/core/instance/init.js

initProvide(vm) 
1

​ 可以发现 initInjections 函数在 initProvide 函数之前被调用,这说明对于任何一个组件来讲,总是要优先初始化 inject 选项,再初始化 provide 选项。但是我们知道 inject 选项的数据需要从父代组件中的 provide 获取,所以我们优先来了解 provide 选项的实现,然后再查看 inject 选项的实现。

​ 源码目录 src/core/instance/inject.js

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}
1
2
3
4
5
6
7
8

​ 如上是 initProvide 函数的全部代码,它接收组件实例对象作为参数。在 initProvide 函数内部首先定义了 provide 常量,它的值是 vm.$options.provide 选项的引用,接着是一个 if 条件语句,只有在 provide 选项存在的情况下才会执行 if 语句块内的代码,我们知道 provide 选项可以是对象,也可以是一个返回对象的函数。所以在 if 语句块内使用 typeof 操作符检测 provide 常量的类型,如果是函数则执行该函数获取数据,否则直接将 provide 本身作为数据。最后将数据复制给组件实例对象的 vm._provided 属性,后面我们可以看到当组件初始化 inject 选项时,其注入的数据就是从父代组件实例的 vm._provided 属性中获取的。

​ 以上就是 provide 选项的初始化及实现,它本质上就是在组件实例对象上添加了 vm._provided 属性,并保存了用于子代组件的数据。

​ 说明:更多内容查看 provide/inject (opens new window)

# 9. callHook

​ 我们回到 _init 函数中,继续往下看,代码如下:

​ 源码目录:src/core/instance/init.js

callHook(vm, 'beforeCreate')
// 初始化Injections、State、Provide...
callHook(vm, 'created')
1
2
3

callHook 函数的作用是调用生命周期钩子函数。接下来我们来看看 callHook 的定义,如下:

源码目录:`scr/core/instance/lifecycle.js` 
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)
  }
  pushTarget()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

​ 首先我们看一下 callHook 函数的参数列表,callHook 接收两个参数:实例对象和要调用的生命周期钩子的名称

​ 接下来我们再来分析 callHook 内部的实现逻辑,首先 pushTarget()pushTarget() 的作用是为了避免在某些生命周期钩子中使用 props 数据导致收集冗余的依赖,我们这里不展开讲解中,在后面章节中我们会详细介绍。

​ 接下来是获取要调用的生命周期钩子,我们在 vue源码分析(十六) 选项合并之合并策略 (opens new window) 中分析知道生命周期钩子选项最终会被合并处理成一个数组,所以这里通过 vm.$options[hook] 获取的也是一个数组即 handlers 是一个数组。

​ 接下来是定义一个常量,值为当前要调用的生命周期的名称和 字符串 hook 组成的一个新的字符串。然后判断 handlers 是否在实例选项中写了生命周期钩子,如果写了则执行 if 语句块。if 语句块中通过 for 循环遍历获取到的生命周期钩子的数组,调用 invokeWithErrorHandling 函数处理钩子函数。

​ 我们再来看看 invokeWithErrorHandling 定义,如下:

export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

invokeWithErrorHandling 函数中使用 try...catch 语句块,为了捕获可能出现的错误, 并在 catch 语句块内使用 handleError 处理错误信息。 通过判断参数是否存在来确定使用 .apply(context, args) 还是使用 .call(context),如果存在则使用 .apply(context, args) 执行钩子函数,如果不存在则使用 .call(context) 执行钩子函数,需要注意的是为了保证生命周期钩子函数内可以通过 this 访问实例对象,此处的 contextvm 实例。

​ 我们再回到 callHook 函数中,代码分析到这里还有一个 if 语句没有分析,如下:

if (vm._hasHookEvent) {
  vm.$emit('hook:' + hook)
}
1
2
3

​ 其中 vm._hasHookEvent 是在 initEvents 函数中定义的,它的作用是判断是否存在生命周期钩子的事件侦听器,初始化值为 false 代表没有,当组件检测到存在生命周期钩子的事件侦听器时,会将 vm._hasHookEvent 设置为 true。那这个逻辑什么时候执行呢?我们举个例子来说明,如下:

<child
  @hook:beforeCreate="handleChildBeforeCreate"
  @hook:created="handleChildCreated"
  @hook:mounted="handleChildMounted"
  @hook:生命周期钩子
 />
1
2
3
4
5
6

​ 如上代码可以使用 hook:生命周期钩子名称 的方式来监听组件相应的生命周期事件。这是 Vue 官方文档上没有体现的,但你确实可以这么用,不过除非你对 Vue 非常了解,否则不建议使用。

​ 通过上面分析我们知道其中 initInjectionsinitStateinitProvidecallHook(vm, 'beforeCreate')callHook(vm, 'created') 两个钩子函数之间,其中 initState 包括了:initPropsinitMethodsinitDatainitComputed 以及 initWatch。所以当 beforeCreate 钩子被调用时,所有与 propsmethodsdatacomputed 以及 watch 相关的内容都不能使用,当然了 inject/provide 也是不可用的。而在 created 中这些数据都可以使用。