Vue源码系列讲解——生命周期篇【五】(initInjections)

目录

[1. 前言](#1. 前言)

[2. initInjections函数分析](#2. initInjections函数分析)

resolveInject函数分析

[3. 总结](#3. 总结)


1. 前言

本篇文章介绍生命周期初始化阶段所调用的第四个初始化函数------initInjections。从函数名字上来看,该函数是用来初始化实例中的inject选项的。说到inject选项,那必然离不开provide选项,这两个选项都是成对出现的,它们的作用是:允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。并且

provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。在该对象中你可以使用 ES2015 Symbols 作为 key,但是只在原生支持 SymbolReflect.ownKeys 的环境下可工作。

inject 选项应该是:

  • 一个字符串数组,或
  • 一个对象,对象的 key 是本地的绑定名,value 是:
    • 在可用的注入内容中搜索用的 key (字符串或 Symbol),或
    • 一个对象,该对象的:
      • from 属性是在可用的注入内容中搜索用的 key (字符串或 Symbol)
      • default 属性是降级情况下使用的 value

这两个选项在我们日常开发中使用的频率不是很高,但是在一些组件库中使用的很频繁,官方文档给出了使用示例,如下:

javascript 复制代码
// 父级组件提供 'foo'
var Parent = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子组件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}

利用 ES2015 Symbols、函数 provide 和对象 inject

javascript 复制代码
const s = Symbol()

const Provider = {
  provide () {
    return {
      [s]: 'foo'
    }
  }
}

const Child = {
  inject: { s },
  // ...
}

接下来 2 个例子只工作在 Vue 2.2.1 或更高版本。低于这个版本时,注入的值会在 propsdata 初始化之后得到。

使用一个注入的值作为一个属性的默认值:

javascript 复制代码
const Child = {
  inject: ['foo'],
  props: {
    bar: {
      default () {
        return this.foo
      }
    }
  }
}

使用一个注入的值作为数据入口:

javascript 复制代码
const Child = {
  inject: ['foo'],
  data () {
    return {
      bar: this.foo
    }
  }
}

在 2.5.0+ 的注入可以通过设置默认值使其变成可选项:

javascript 复制代码
const Child = {
  inject: {
    foo: { default: 'foo' }
  }
}

如果它需要从一个不同名字的属性注入,则使用 from 来表示其源属性:

javascript 复制代码
const Child = {
  inject: {
    foo: {
      from: 'bar',
      default: 'foo'
    }
  }
}

与 prop 的默认值类似,你需要对非原始值使用一个工厂方法:

javascript 复制代码
const Child = {
  inject: {
    foo: {
      from: 'bar',
      default: () => [1, 2, 3]
    }
  }
}

总结起来一句话就是:父组件可以使用provide选项给自己的下游子孙组件内注入一些数据,在下游子孙组件中可以使用inject选项来接收这些数据以便为自己所用。

另外,这里有一点需要注意:provideinject 选项绑定的数据不是响应式的。

了解了他们的作用及使用方法后,我们就来看下initInjections函数是如何来初始化inject选项的。

2. initInjections函数分析

分析之前,我们先说一个问题,细心的同学可能会发现,既然inject选项和provide选项都是成对出现的,那为什么在初始化的时候不一起初始化呢?为什么在init函数中调用initInjections函数和initProvide函数之间穿插一个initState函数呢?

其实不然,在官方文档示例中说了,provide选项注入的值作为数据入口,如下:

javascript 复制代码
const Child = {
  inject: ['foo'],
  data () {
    return {
      bar: this.foo
    }
  }
}

这里所说的数据就是我们通常所写datapropswatchcomputedmethod,所以inject选项接收到注入的值有可能被以上这些数据所使用到,所以在初始化完inject后需要先初始化这些数据,然后才能再初始化provide,所以在调用initInjections函数对inject初始化完之后需要先调用initState函数对数据进行初始化,最后再调用initProvide函数对provide进行初始化。

OK,接下来我们就来分析initInjections函数的具体原理,该函数定义位于源码的src/core/instance/inject.js中,如下:

javascript 复制代码
export function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      defineReactive(vm, key, result[key])
    }
    toggleObserving(true)
  }
}

export let shouldObserve: boolean = true
export function toggleObserving (value: boolean) {
  shouldObserve = value
}

可以看到,initInjections函数的逻辑并不复杂,首先调用resolveInjectinject选项中的数据转化成键值对的形式赋给result,如官方文档给出的例子,那么result应为如下样子:

javascript 复制代码
// 父级组件提供 'foo'
var Parent = {
  provide: {
    foo: 'bar'
  }
}

// 子组件注入 'foo'
var Child = {
  inject: ['foo'],
}

// result
result = {
    'foo':'bar'
}

然后遍历result中的每一对键值,调用defineReactive函数将其添加当前实例上,如下:

javascript 复制代码
if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
        defineReactive(vm, key, result[key])
    }
    toggleObserving(true)
}

此处有一个地方需要注意,在把result中的键值添加到当前实例上之前,会先调用toggleObserving(false),而这个函数内部是把shouldObserve = false,这是为了告诉defineReactive函数仅仅是把键值添加到当前实例上而不需要将其转换成响应式,这个就呼应了官方文档在介绍provideinject 选项用法的时候所提示的:

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

initInjections函数的逻辑就介绍完了,接下来我们看看resolveInject函数内部是如何把inject 选项中数据转换成键值对的。

resolveInject函数分析

我们知道,inject 选项中的每一个数据key都是由其上游父级组件提供的,所以我们应该把每一个数据key从当前组件起,不断的向上游父级组件中查找该数据key对应的值,直到找到为止。如果在上游所有父级组件中没找到,那么就看在inject 选项是否为该数据key设置了默认值,如果设置了就使用默认值,如果没有设置,则抛出异常。

OK,以上是我们的分析,下面我们就来看下resolveInject函数的源码,验证我们的分析,源码如下:

javascript 复制代码
export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    const result = Object.create(null)
    const keys =  Object.keys(inject)

    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)) {
          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)
        }
      }
    }
    return result
  }
}

在分析函数源码之前,我们对照着官网给出的示例,这样会比较好理解一些。

javascript 复制代码
var Parent = {
  provide: {
    foo: 'bar'
  },
  // ...
}
const Child = {
  inject: {
    foo: {
      from: 'bar',
      default: () => [1, 2, 3]
    }
  }
}

可以看到,在函数源码中,首先创建一个空对象result,用来存储inject 选项中的数据key及其对应的值,作为最后的返回结果。

然后获取当前inject 选项中的所有key,然后遍历每一个key,拿到每一个keyfrom属性记作provideKeyprovideKey就是上游父级组件提供的源属性,然后开启一个while循环,从当前组件起,不断的向上游父级组件的_provided属性中(父级组件使用provide选项注入数据时会将注入的数据存入自己的实例的_provided属性中)查找,直到查找到源属性的对应的值,将其存入result中,如下:

javascript 复制代码
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)) {
      result[key] = source._provided[provideKey]
      break
    }
    source = source.$parent
  }
}

如果没有找到,那么就看inject 选项中当前的数据key是否设置了默认值,即是否有default属性,如果有的话,则拿到这个默认值,官方文档示例中说了,默认值可以为一个工厂函数,所以当默认值是函数的时候,就去该函数的返回值,否则就取默认值本身。如果没有设置默认值,则抛出异常。如下:

javascript 复制代码
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)
  }
}

最后将result返回。这就是resolveInject函数的所有逻辑。

此时你可能会有个疑问,官方文档中说inject 选项可以是一个字符串数组,也可以是一个对象,在上面的代码中只看见了处理当为对象的情况,那如果是字符串数组呢?怎么没有处理呢?

其实在初始化阶段_init函数在合并属性的时候还调用了一个将inject 选项数据规范化的函数normalizeInject,该函数的作用是将以下这三种写法:

javascript 复制代码
// 写法一
var Child = {
  inject: ['foo']
}

// 写法二
const Child = {
  inject: {
    foo: { default: 'xxx' }
  }
}

// 写法三
const Child = {
  inject: {
    foo
  }
}

统统转换成以下规范化格式:

javascript 复制代码
const Child = {
  inject: {
    foo: {
      from: 'foo',
      default: 'xxx'  //如果有默认的值就有default属性
    }
  }
}

这样做的目的是,不管用户使用了何种写法,统统将其转化成一种便于集中处理的写法。

该函数的定义位于源码的src/core/util/options.js中,如下:

javascript 复制代码
function normalizeInject (options: Object, vm: ?Component) {
  const inject = options.inject
  if (!inject) return
  const normalized = options.inject = {}
  if (Array.isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] }
    }
  } else if (isPlainObject(inject)) {
    for (const key in inject) {
      const val = inject[key]
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)
        : { from: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
      `but got ${toRawType(inject)}.`,
      vm
    )
  }
}

该函数的逻辑并不复杂,如果用户给inject选项传入的是一个字符串数组(写法一),那么就遍历该数组,把数组的每一项变成

javascript 复制代码
inject:{
  foo:{
    from:'foo'
  }
}

如果给inject选项传入的是一个对象,那就遍历对象中的每一个key,给写法二形式的key对应的值扩展{ from: key },变成:

javascript 复制代码
inject:{
  foo:{
    from: 'foo',
    default: 'xxx'
  }
}

将写法三形式的key对应的值变成:

javascript 复制代码
inject:{
  foo:{
    from: 'foo'
  }
}

总之一句话就是把各种写法转换成一种规范化写法,便于集中处理。

3. 总结

本篇文章介绍生命周期初始化阶段所调用的第四个初始化函数------initInjections。该函数是用来初始化inject选项的。

由于inject选项在日常开发中使用频率不高,所以首先我们先根据官方文档回顾了该选项的作用及使用方法。

接着,我们分析了initInjections函数的内部实现原理,分析了是根据inject选项中的数据key是如何自底向上查找上游父级组件所注入的对应的值。

另外,对inject选项的规范化函数normalizeInject也进行了分析,Vue为用户提供了自由多种的写法,其内部是将各种写法最后进行统一规范化处理。

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax