上一篇 Vue解析原理(二): 初始化时beforeCreate之前做了什么?

我们继续this._init() 的初始化相关操作, 接着又会执行如下三个初始化方法:

initInjections(vm)
initState(vm)
initProvide(vm)

5. initInjections(vm): 主要作用是初始化inject, 可以访问到对应的依赖。

injectprovide 这里需要简单的提一下, 这是vue@2.2 版本添加的一对需要一起使用的API, 它 允许父级组件向它之后的子孙组件提供依赖,让子孙组件无论嵌套多深都可以访问到.

  • provide : 提供一个对象或是返回一个对象的函数。
  • inject : 是一个字符串数组或对象。

这一对APIVue 官网有给出两条实用提示:

provideinject 主要为高阶插件/组件库提供用例。 并不推荐直接用于应用程序代码中。

  • 大概是因为会让组件数据层级关系变的混乱的缘故, 但在开发组件库时会很好使。

provideinject 绑定并不是可响应的。 这是刻意为之的。 然而,如果你传入一个可以监听的对象,那么其对象的属性还是可响应的。

  • 有个小技巧, 这里可以将根组件data 内定义的属性提供给子孙组件, 这样在不借助vuex 的情况下就可以实现简单的全局状态管理。
app.vue 根组件

export default {
  provide() {
    return {
      app: this
    }
  },
  data() {
    return {
      info: 'hello world!'
    }
  }
}

child.vue 子孙组件

export default {
  inject: ['app'],
  methods: {
    handleClick() {
      this.app.info = 'hello vue!'
    }
  }
}

一但触发handleClick 事件之后, 无论嵌套多深的子孙组件只要是使用了inject注入this.app.info 变量的地方都会被响应, 这就是完成了简易的vuex。 接下来我们来看下这功能究竟怎么实现的~

虽然injectprovide 是成对使用的, 但是二者内部是分开初始化的。 从上面三个初始化方法就能看出, 先初始化inject, 然后初始化props/data状态相关, 最后初始化provide 。 这样做的目的是可以在 props/data 中使用 inject 内所注入的内容。

我们首先来看一下初始化inject 时的方法定义:

export function initInjections(vm) {
  const result = resolveInject(vm.$options.inject, vm) // 找结果
  
  ...
}

vm.$options.inject 为之前合并后得到的用户自定义的inject, 然后使用resolveInject 方法找到我们想要的结果, 我们看下resolveInject 方法的定义:

export function resolveInject (inject, vm) {
  if (inject) {
    const result = Object.create(null)
    const keys = Object.keys(inject)  //省略Symbol情况

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      const provideKey = inject[key].from
      let source = vm
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) { //hasOwn为是否有
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
    ... vue@2.5后新增设置inject默认参数相关逻辑
    }
    return result
  }
}

首先定义一个result 返回找到的结果。 接下来使用双循环查找,外层的for 循环会遍历inject 的每一项, 然后再内层使用while 循环向上查找inject 该项的父级是否有提供对应的依赖。

PS: 这里有个疑问, 之前inject 的定义明明是数组, 这里怎么可以通过Object.keys 取值? 这是因为上一章再做options 合并时, 也会对参数进行格式化, 如:props 的格式, 定义为数组也会被转为对象格式, inject 被定义时是这样的:

定义时:
{
  inject: ['app']
}

格式化后:
{
  inject: {
    app: {
      from: 'app'
    }
  }
}

接上文, source 就是当前的实例, 而source._provided 内保存的就是当前provide 提供的值。 首先从当前实例查找,接着将它的父组件实例赋值给source,
在它的父组件查找。 找到后使用break 跳出循环,将搜索的结果赋值给result, 接着查找下一个。

PS: 可能有人会有疑问, 这个时候是先初始化inject 再初始化provide , 怎么访问父级的provide? 它根本就没有初始呀, 这个时候需要再思考下, 因为Vue是组件式的, 首先就会初始化父组件, 然后才是初始化子组件, 所以这个时候是有source._provided 属性的。

梳理了想到的结果之后, 补全之前initInjections的定义:

export function initInjections(vm) {
  const result = resolveInject(vm.$options.inject, vm)

  if(result) { // 如果有结果
    toggleObserving(false)  // 刻意为之不被响应式
    Object.keys(result).forEach(key => {
      ...
      defineReactive(vm, key, result[key])
    })
    toggleObserving(true)
  }
}

如果有搜索结果,首先会调用toggleObserving(false), 具体实现先不理会, 这个方法的作用是设置一个标志位, 将决定defineReactive()方法是否将它的第三个参数设置为响应式数据, 也就是决定result[key] 这个值是否会被设置为响应式数据, 这里的参数为false, 只是在 vm 下挂载 key 对应普通的值, 不过这样做可以在当前实例使用this 访问到 inject 内对应的依赖项了, 设置完毕之后再调用toggleObserving(true) , 改变标志位, 让defineReactive() 可以设置第三个参数为响应式数据(defineReactive 是响应式原理很重要的方法,这里了解即可), 也就是他该有的样子。 以上是inject实现相关原理, 一句话来说就是, 先遍历每个项, 然后挨个遍历每一项父级是否有依赖。

6. initState(vm): 初始化会被使用到的状态, 状态包括: ==props, methods, data, computed, watch == 五个选项。

先看下 initState(vm) 方法的定义:

export function initState(vm) {
  ...
  const opts = vm.$options
  if(opts.props) initProps(vm, opts.props)
  if(opts.methods) initMethods(vm, opts.methods)
  if(opts.data) initData(vm)
  ...
  if(opts.computed) initComputed(vm, opts.computed)
  if(opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

现在这里的话只介绍前面三类状态的初始化做了什么, 也就是props, methods, data, 因为computed 和 watch == 会涉及到响应式相关的watcher==, 这里先略过。 接下来我们依次介绍上面三个初始化方法的实现及原理:

6.1 initProps(vm, propsOptions):

  • 主要作用是检测子组件接收的值是否符合规则, 以及让对应的值可以用this直接访问。
function initProps(vm, propsOptions) {  // 第二个参数为验证规则
  const propsData = vm.$options.propsData || {}  // props具体的值
  const props = vm._props = {}  // 存放props
  const isRoot = !vm.$parent // 是否是根节点
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    const value = validateProp(key, propsOptions, propsData, vm)
    defineReactive(props, key, value)
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

我们知道 props是作为父组件向子组件通信的重要方式, 而initProps 内的第二个参数propsOptions , 就是当前实例也就是通信角色里的子组件, 它所定义的接收参数的规则。子组件的props 规则是可以使用数组形式定义的, 不过在经过合并options之后会被格式化为对象的形式:

定义时:
{
  props: ['name', 'age']
}

格式化后:
{
  name: {
    type: null
  },
  age: {
    type: null
  }
}

所以在定义props 规则时, 直接使用对象格式吧,这也是更好的书写规范。

知道了规则之后, 接下来需要知道父组件传递给子组件具体的值, 它以对象的格式被放在 vm.$options.propsData 内, 这也是合并options 时得到的。 接下来在实例下定义了一个空对象 vm._props , 它的作用是将符合规则的值挂载到它下面。 isRoot 的作用是判断当前组件是否是根组件, 如果不是就不将props 转为响应式数据。

接下来遍历格式后的props 验证规则, 通过validateProp 方法验证规则并得到相应的值, 将得到的值挂载到vm._props下。 这个时候就可以通过this._props 访问到props 内定义的值了:

props: ['name'],
methods: {
  handleClick() {
    console.log(this._props.name)
  }
}

不过直接访问内部的私有变量这种方式并不友好, 所以vue 内部做了一层代理, 将对this.name 的访问转而为对this._props.name 的访问。 这里的proxy 需要介绍下,因为之后的data也会使用到,看下它的定义:

格式化了一下:
export function proxy(target, sourceKey, key) {
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      return this[sourceKey][key]
    },
    set: function () {
      this[sourceKey][key] = val
    }
  })
}

其实很简单, 只是定义一个对象值的get方法, 读取时让其返回另外一个值,这里就完成了props 的初始化。

6.2 initMethods(vm, methods):

  • 主要作用是将methods内的方法挂载到this下。
function initMethods(vm, methods) {
  const props = vm.$options.props
  for(const key in methods) {
    
    if(methods[key] == null) {  // methods[key] === null || methods[key] === undefined 的简写
      warn(`只定义了key而没有相应的value`)
    }
    
    if(props && hasOwn(props, key)) {
      warn(`方法名和props的key重名了`)
    }
    
    if((key in vm) && isReserved(key)) {
      warn(`方法名已经存在而且以_或$开头`)
    }
    
    vm[key] = methods[key] == null
      ? noop  // 空函数
      : bind(methods[key], vm)  //  相当于methods[key].bind(vm)
  }
}

methods 的初始化比较简单。 不过它也有很多边界情况, 如只定义了key 而没有方法具体的实现, keyprops 重名了, key 已经存在且命名不规范,以==_== 或者 $ 开头。 最后将methods 内的方法挂载到this 下, 就完成了methods 的初始化。

6.3 initData( vm )

  • 主要作用是初始化 data, 还是老套路,挂载到this下。 有个重要的点, 之所以data 内的数据是响应式的, 是在这里初始化的, 这个我们得有个印象~
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm) // 通过data.call(vm, vm)得到返回的对象
    : data || {}
  if (!isPlainObject(data)) { // 如果不是一个对象格式
    data = {}
    warn(`data得是一个对象`)
  }
  const keys = Object.keys(data)
  const props = vm.$options.props  // 得到props
  const methods = vm.$options.methods  // 得到methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (methods && hasOwn(methods, key)) {
      warn(`和methods内的方法重名了`)
    }
    
    if (props && hasOwn(props, key)) {
      warn(`和props内的key重名了`)
    } else if (!isReserved(key)) { // key不能以_或$开头
      proxy(vm, `_data`, key)
    }
  }
  observe(data, true)
}

首先通过 vm.$options.data 得到用户定义的 data, 如果是function 格式就执行它,并返回执行之后的结果, 否则返回 data{}, 将结果赋值给vm._data 这个私有属性。 和 props 一样的套路, 最后用来做一层代理, 如果得到的结果不是对象格式就是报错了。

然后遍历data 内的每一项, 不能和methods 以及 props 内的key 重名, 然后使用 proxy 做一层代理。 注意最后会执行一个方法 observe(data, true), 它的作用是递归的让 data 内的每一项数据都变成响应式的。

其实不难发现他们三个主要做的事情差不多, 首先不要互相之间有重名, 然后可以被this 直接访问到。

7. initProvide(vm): 主要作用是初始化provide 为子组件提供依赖。

provide 选项应该是一个对象或者函数, 所以对他取值即可, 就像取data 内的值类似, 看下它的定义:

export function initProvide (vm) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

首先通过 vm.$options.provide 取得用户定义的provide 选项, 如果是一个function类型就执行以下, 得到返回后结果,将其赋值给了 vm._provided 私有属性, 所以子组件在初始化 inject 时就可以访问到父组件提供的依赖了; 如果不是function 类型就直接返回定义的provide

8. callHook(vm, ‘created’): 执行用户定义的created 钩子函数, 有mixin混入的也一并执行。

终于我们越过了created钩子函数, 还是分别用一句话来介绍它们主要干了什么事:

  • initInjections(vm): 让子组件inject的项可以访问到正确的值。
  • initState(vm): 将组件定义的状态挂载到this下。
  • initProvide (vm): 初始化父组件提供的provide依赖。
  • created: 执行组件的 created 钩子函数。

初始化阶段算是告一段落了, 接下来我们会进入组件的挂载阶段。
本章还是以一个问题来结尾:

  • 请问methods 内的方法可以使用箭头函数么? 会产生怎样的结果?
    解答:

  • 是不可以使用箭头函数的, 因为箭头函数的this是在定义时就绑定的。 在vue的内部, methods 内每个方法的上下文是当前的vm组件实例, methods[key].bind(vm)。 如果使用箭头函数,函数的上下文就变成了父级的上下文, 也就是undefined了, 结果就是通过undefined 访问任何变量都会报错。

下一篇: Vue原理解释(四): 虚拟Dom是怎么生成的 (上)

Logo

前往低代码交流专区

更多推荐