美文网首页
Vue 中 provide 和 inject 用法及源码解析

Vue 中 provide 和 inject 用法及源码解析

作者: 梦晓半夏_d68a | 来源:发表于2021-05-26 21:47 被阅读0次

provide/inject 官方文档说明
官网总结:

  1. 配对使用:provide 和 inject 需要成对使用
  2. 作用:祖先组件可向其所有子孙后代传递数据
  3. 优点:当孙组件想要访问祖先组件的 property 时,通过 provide/inject 可以轻松实现跨级访问数据
  4. 场景:provide/inject 主要在开发高阶插件/组件库时使用,并不推荐用于普通应用程序代码中

  从使用 provide/inject 优点来看,当我们的组件嵌套了多层,孙组件可轻松访问祖先组件的数据,可能大家会和我有同样的疑惑:为什么开发中却很少用到?

使用场景分析

为什么会不推荐用于普通应用程序代码呢?
  因为数据追踪比较困难,不知道是哪一个层级声明了这个或者不知道哪一层级或若干个层级使用了。如果数据在多个组件都需要使用到,可以使用 vueX 来进行状态管理。如果只是子组件想要使用父组件上的数据,可直接通过 props 来让父组件给子组件传值。

为什么推荐使用在高阶插件/组件库?
  因为在 vue 中父子组件可以用 props 传值,子组件也可以通过 $parent 访问父组件的 property(不推荐)。但父子组件 props 传值需要需要知道往哪一个子组件传值,而在组件库中的组件中引入的子组件是不确定的。而 provide 只需要将传递的值注入 ,不需要知道使用哪一个子组件,子组件通过 inject 获取注入的数据,也不需要知道父组件是谁,因此再进行封装组件库的时候很方便。

借用网上很火的 element-ui 组件库的案例来说明一下。
  在 element-ui 中,我们经常使用里面的表单,看一段熟悉的代码,DOM 结构: el-form > el-form-item > el-input:


element-ui 案例配图1

打开 el-form 这个组件,发现使用到了 provide:


element-ui 案例配图2

  这里的 el-form组件,提供给子孙组件的数据是当前的 vue 实例,这样在子孙组件中,都可以使用 inject 来接收 el-form 组件提供的 vue 实例。

  再打开 el-form 组件的子组件 el-form-item,果不其然,发现用到了 inject 注入数据:


element-ui 案例配图3

provide/inject 语法

  provide:是一个对象或返回一个对象的函数。该对象包含可注入其子孙组件的数据
  inject:一个字符串数组,或一个对象。如果是对象,对象的 key 是本地的绑定名,value 是,包含from和default默认值。

自制小案例

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <parent></parent>
  </div>
  <script src="../../dist/vue.js"></script>
  <script>
    Vue.component('parent', {
      template: `
        <div>
          <child></child>
        </div>
      `,
      provide: {
        A: 'ParentA'
      }
    })
    Vue.component('child', {
      template: `
        <div>
          <h2>child 组件中获取tA---{{ A }}</h2>
          <sunzi></sunzi>
        </div>
      `,
      provide: {
        A: 'ChildA',
        B: 'ChildB'
      },
      inject: ['A']
    })
    Vue.component('sunzi', {
      template: `
        <div>
          <h2>sunzi 组件中获取A ---{{ injectA }}</h2>
          <h2>sunzi 组件中获取B ---{{ injectB }}</h2>
        </div>
      `,
      inject: {
        injectA: {
          from: 'A',
          default: '默认信息1'
        },
        injectB: {
          from: 'B',
          default: '默认信息2'
        }
      }
    })
    const app = new Vue({
      el: '#app',
      data: {}
    })
  </script>
</body>
</html>

运行结果如下:


自制案例配图1

从上结果可以看出,组件在查找注入内容时是一级一级往上查找的,就近原则。

源码解析

  首先我们知道在使用 vue 的时候,先通过CDN方式 引入vue 或者 npm 下载Vue包,再通过 new Vue(options) 即可,其实在我们执行 new Vue(options) 代码的时候,vue内部是做了很多初始化操作的,比如现在要说的 initInjections(vm) 初始化组件的 inject 注入内容。直接上源码

initInjections 方法
文件路径:src/core/instance/inject.js

/**
 * 初始化 inject 选项
 * 获取 inject 里的所有 key 的值,得到 result[key] = val 对象,
 * 并做响应式处理:把 key 代理到当前 vue 实例上,方便通过 this.key 访问
 * @param {*} vm
 */
export function initInjections (vm: Component) {
  // 从配置项上解析 inject 选项,最后得到 result[key] = val
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    // 关闭观察者 observe 模式
    toggleObserving(false)
    // 将解析结果做响应式处理,将 key 代理到当前 vue 实例上,在组件里可以通过 this.key 来进行访问,响应式处理代码 defineReactive 后面过几天再更。
    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])
      }
    })
    // 开启观察者 observe 模式
    toggleObserving(true)
  }
}

resolveInject 方法
文件路径:src/core/instance/inject.js

/**
 * 
 * @param {*} inject = {
 *  key: {
 *    from: provideKey,
 *    default: xx
 *  }
 * } 或 inject = [key..]
 * @param {*} vm 
 * @returns { key: val }
 */
export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    const result = Object.create(null)
    // 创建一个数组 keys,保存 inject 的所有 key
    const keys = hasSymbol
      ? Reflect.ownKeys(inject)
      : Object.keys(inject)
    // 遍历 keys 数组项
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      if (key === '__ob__') continue
      // 获取 key 的 from 属性,并赋值给 provideKey
      const provideKey = inject[key].from
      // 一层一层地向祖代组件的配置项查找 provide 选项,并查找对应的 key 值
      let source = vm
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
      // 如果所有祖代组件都已查找完,则先去 inject 获取 key 时的设置的 deault 默认值
      // 如果没有默认值且不是生产环境,则直接警示 `Injection "${key}" not found`
      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
  }
}

toggleObserving 方法
文件路径:src/core/observe/index.js

/**
 * 控制是否使用观察者 observe 模式
 */
export function toggleObserving (value: boolean) {
  shouldObserve = value
}

相关文章