07 | 【阅读Vue2源码】Provide/Inject实现原理

前言

provide/inject在面试中,偶尔会问到,它通常在回答数据通信的内容中提到,那么它是怎么实现的呢?一起来分析源码吧。

官方定义:provide/inject这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。

provide/inject基本使用

在分析源码之前,先看下其基本使用,其实一般业务开发代码中比较少用到provide/inject,在写一些库的时候才会用到,那么看看基本使用方法吧:

index.html

html 复制代码
<section id="app">
  <button @click="plus">+1</button>
  <div id="count">count:{{ count }}</div>
  <display-count></display-count>
</section>

<script src="../../dist/vue.js"></script>
<script src="app.js"></script>

app.js

js 复制代码
const store = {}

const DisplayCount = {
  name: 'DisplayCount',
  template: `
    <div>
      DisplayCount: {{$store.state.count}}
    </div>
  `,
  inject: ['$store'],
  data() {
    return {
      
    }
  },
}

var app = new Vue({
  name: 'SimpleDemoAPI_Provide_Inject',
  components: {
    'display-count': DisplayCount
  },
  provide: {
    $store: store
  },
  data() {
    return {
      count: 0
    }
  },
  created() {
    store.state = this.$data
  },
  methods: {
    plus() {
      this.count += 1;
    }
  }
})

效果:

思维导图

provide/inject链路图:

分析源码

初始化vue的代码片段

源码位置:src\core\instance\init.js

js 复制代码
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
  	// ...
    initInjections(vm) // 初始化inject,读取provide的值,绑定到组件实例中
    // ...
    initProvide(vm) // 初始化 provide,原理:把options.provide挂载到vm._provide,inject时读取值,绑定到组件实例中
    // ...
  }
}

provide/inject的实现代码片段

源码位置:src\core\instance\inject.js

js 复制代码
/* @flow */

import { hasOwn } from 'shared/util'
import { warn, hasSymbol } from '../util/index'
import { defineReactive, toggleObserving } from '../observer/index'

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

export function initInjections (vm: Component) {
  // 根据inject的提供的key,寻找provide中对应的值
  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 {
        // 将inject的key对应的provide的值,绑定到vue/vue组件实例上
        defineReactive(vm, key, result[key])
      }
    })
    toggleObserving(true)
  }
}

// 根据inject的提供的key,寻找provide中对应的值
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)
    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
      // 遍历找父级provide
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          // 找到父级的provide的属性,放到result,再返回
          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
  }
}

从源码上看,可以看出provide/inject的实现很简单,代码量也很少,几十行就搞定了。

下面我们来分析代码逻辑,结合上面的链路图一起看,

  1. 初始化Vue,首先初始化inject,如果是根组件,则是没有inject的,子组件才会有inject,继续往下走
  2. 初始化provide,调用initProvide(),很简单,直接把provide的值赋值给组件的_provide属性,保存起来,等待子组件inject读取其值
  3. 子组件开始初始化inject,调用initInjections(),先把调用resolveInject()inject解析出来,再调用解析出来的值,根据对应的key取到值,再调用defineReactive()inject的key定义到组件实例中(defineReactive()就是调用Object.defineProperty()),至此provide/inject就实现了。
  4. 那么resolveInject()是怎么解析inject的呢?很简单,遍历inject的key,同时遍历当前组件实例中父级组件的provide属性,如果就返回就ok了,查找provide的过程其实就是数据结构中链表的查找的操作。

总结

provide/inject实现原理很简单:

组件初始化时先初始化inject注入的值,初始化的过程就是去查找父组件中的provide的值,再把对应的键和值通过Object.defineProperty把键值定义到当前组件中,所以组件可以通过this访问到inject注入的值。

相关推荐
weixin_382395232 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__2 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.3 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~4 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华4 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子7 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅7 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni7 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学8 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端