【源码解析】挖一挖provide/inject是如何实现与后代组件交互的

1,088 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情

前言

大家好,在前面的vue2组件的交互方式分享中我们梳理了vue中组件间的几种交互方式,其中有一种交互方式是父组件与后代组件的交互 - proivde/inject。那么它是如何实现在父组件中定义就能在所有后代组件都能访问的呢,它的是实现原理又是什么呢,接下来我们以vue2为例从源码层面来梳理一下。

原理解析

在provide/inject的源码中总共只有七十多行代码,主要分为三个函数实现:initProvideinitInjectionsresolveInject,下面我们分别来看下这三个函数都做了什么!

initProvide

initProvide方法非常简单只有8行代码,其核心思想就是给Vue实例添加一个_provided属性,其值就是父组件中提供都provide值。

// 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
    }
}

如上是initProvide函数的源码,该函数接收一个vm参数,vm实际就是Vue的实例。在Vue实例上有个options属性,如果父组件提供了provide,则该provide会挂在options属性,如果父组件提供了provide,则该provide会挂在options属性上。

  • 因此在该函数的第一行首先获取$options中的proivde并保存到provide变量中。
  • 然后判断,如果provide存在,首先检测该provide是否是一个函数,
    • 如果provide是一个函数, 则通过call执行该函数,并将函数的执行结果作为值挂着vm的_provided属性上
    • 如果proivde不是函数,则直接将proivde赋值给vm的_provided

initInjections

initInjections方法也非常简单,其主要功能就是给inject上的每个属性做数据劫持(使其成为响应式数据)。

// src/core/instance/inject.js
export function initInjections(vm: Component){
    const result = resolveInject(vm.$options.iniject, vm)
    if(result){
        toggleObserving(false)
        Object.keys(result).forEach(key => {
            /* istabbul 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)
    }
}

该方法同样接收一个vm参数(Vue的实例),如果inject属性在后代组件中被定义,也会挂到Vue实例的$options属性上

  • 首先还是在Vue实例的$options属性中获取inject属性,与provide不同,获取到该属性后不是直接赋值给某个变量,而是将其作为参数传递给另外一个函数resolveInject进行处理,并将处理结果返回保存在result中
  • 如果result存在,则对result中的每个属性进行响应式处理
    • toggleObserving方法是一个是否监听的开关,在处理响应式前先将开关关闭
    • 利用Object.keys方法获取到result中到所有属性,并遍历result中到所有属性进行响应式处理
    • 在调用defineReactive进行响应式处理时会区分一下是否是生产环境,如果是非生产环境会给出一个warning消息提示(关于defineReactive我们在另一篇分享中有详细说明,这里就不再展开了)
    • 最后响应式处理完成后再通过toggleObserving将监听开关打开

resolveInject

resolveInject算是本次分享中相对较复杂的一个函数,也是能够实现父组件与后代组件进行数据交互的核心。下面我们先来看下它的源码

// src/core/instance/inject.js
export function resolveInject(
  inject: any,
  vm: Component
): Record<string, any> | undefined | null {
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    const result = Object.create(null)
    const keys = hasSymbol ? Reflect.ownKeys(inject) : Object.keys(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
        }
        // @ts-expect-error
        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 as string}" not found`, vm)
        }
      }
    }
    return result
  }
}

如上代码,该函数接收两个参数,一个是外面传进来的任意类型的inject,另一个参数仍然是Vue的实例vm。

  • 首先检测inject是否存在,如果存在先创建一个空对象保存在变量result中
  • 然后取出inject中的所有的key保存在变量keys中,并利用for循环遍历所有的key
  • 将Vue的实例vm赋值给source变量并用while循环去查找source(当前实例)的_provided中是否存在key
    • 也就是说只要当前实例存在,则while条件就一直成立
    • 如果在当前实例的_provided找到了inject中的key,就将该key对应的值保存到前面创建的result对象中,并跳出while循环
    • 如果没有在当前实例的provided中找到key,则将当前实例的$parent再重新赋值给source,也就是说如果当前实例中没有找到key则继续到当前实例的父组件实例中继续查找,依此类推。
  • 如果source不存在,则查看是否有默认的inject,如果有则使用默认值,否则就是没有inject 以上便是resolveInject的基本流程,之所以能够实现父组件与后代组件交互主要就是借助while循环加$parent,也就是说通过while循环一层一层向上查找,直到找到或者到根组件为止。

总结

今天的分享中我们根据vue中的源码简单分析梳理了project/inject的实现原理。通过对源码的学习我们知道了provide/inject是如何实现父组件与后代组件交互的。本次分享就到这里了。