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注入的值。

相关推荐
道不尽世间的沧桑2 小时前
第17篇:网络请求与Axios集成
开发语言·前端·javascript
diemeng11193 小时前
AI前端开发技能变革时代:效率与创新的新范式
前端·人工智能
bin91535 小时前
DeepSeek 助力 Vue 开发:打造丝滑的复制到剪贴板(Copy to Clipboard)
前端·javascript·vue.js·ecmascript·deepseek
晴空万里藏片云6 小时前
elment Table多级表头固定列后,合计行错位显示问题解决
前端·javascript·vue.js
曦月合一6 小时前
html中iframe标签 隐藏滚动条
前端·html·iframe
奶球不是球6 小时前
el-button按钮的loading状态设置
前端·javascript
kidding7237 小时前
前端VUE3的面试题
前端·typescript·compositionapi·fragment·teleport·suspense
无责任此方_修行中8 小时前
每周见闻分享:杂谈AI取代程序员
javascript·资讯
Σίσυφος19009 小时前
halcon 条形码、二维码识别、opencv识别
前端·数据库
学代码的小前端9 小时前
0基础学前端-----CSS DAY13
前端·css