本次想讨论一下Vue的provide / inject,之前其实在业务中一直没有用过这个属性或者很少使用。因为一直找不到这个属性的使用场景,再加上之前忘了哪里看的说这个属性最好少在业务中使用,因为可能会造成数据的来源不清晰,容易混乱数据流。但是我看好多组件开发里面用的这个属性却比较多。再加上前一段时间看到了它们的使用,感觉有点陌生所以就打算在这里跟大家讨论一下。具体使用大家请移步官方文档,其实本文主要讨论两个方面
provide / inject
是如何初始化的,整个数据流程是怎么样的?- 为什么
provide
和inject
绑定并不是可响应的?(这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。)
那么就不多啰嗦了,开始吧!
以下源码基于vue 2.6.14版本
1. 初始化数据流程
-
Inject
- 首先当我们初始化Vue的时候会初始化
initMixin
function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } initMixin(Vue)
- 在
initMixin
里面
export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { // 删除一大堆代码 const vm: Component = this vm._self = vm callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') if (vm.$options.el) { vm.$mount(vm.$options.el) } } }
从上面我们可以看到在初始化data前后分别调用了
initInjections
和initProvide
两个函数,其实它们就是初始化provide / inject
的数据用的。我们可以考虑一下为什么Vue初始化各个数据的顺序是这样的,欢迎评论区留言
initInjections -> initProps -> initMethods -> initData -> initComputed -> initWatch -> initProvide
- 下面我们看一下
initInjections
做了什么
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) } }
其实就是把每个组件(实例)的inject对象取出来然后分别
defineReactive
,下面我们看一下resolveInject
做了什么// inject: { // foo: { // from: 'bar', // default: 'foo' // } // } 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) /** 在该对象中你可以使用 ES2015 Symbols 作为 key,但是只在原生支持 Symbol 和 Reflect.ownKeys 的环境下可工作。 */ 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... // 如果key是__ob__那么就跳过该属性 if (key === '__ob__') continue const provideKey = inject[key].from let source = vm // 因为provide的属性是每个”子级“都可以取到的,所以需要每个父级去查找最近的provide的值 while (source) { if (source._provided && hasOwn(source._provided, provideKey)) { result[key] = source._provided[provideKey] break } source = source.$parent } // 如果找不到 if (!source) { // 取默认值或者warn 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 } }
上面的代码其实很好理解,就是根据该组件的inject去相应的“父级”去查到对应provide的值是什么,如果找不到的话就取默认值,还没有的话就warn
/** * Check whether an object has the property. */ const hasOwnProperty = Object.prototype.hasOwnProperty export function hasOwn (obj: Object | Array<*>, key: string): boolean { return hasOwnProperty.call(obj, key) } export const hasSymbol = typeof Symbol !== 'undefined' && isNative(Symbol) && typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys)
- 下面我们看一下
defineReactive
做了什么
/** * In some cases we may want to disable observation inside a component's * update computation. */ export let shouldObserve: boolean = true export function toggleObserving (value: boolean) { shouldObserve = value }
/** * Define a reactive property on an Object. */ export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() // configurable:当且仅当该属性为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。 const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set // 如果没有提供val就自己去取 if ((!getter || setter) && arguments.length === 2) { val = obj[key] } // 如果不是浅监听就observe这个val let childOb = !shallow && observe(val) // 响应式 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val // 放入dep if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { // 如果值是一个数组,则将数组的每一项放入到dep中 dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } // observe新设置的值 childOb = !shallow && observe(newVal) // dep通知watcher数据更新了,进而更新视图 dep.notify() } }) }
其实
defineReactive
是很好理解的,就是把inject的所有值变成响应式的。这里不懂的人可以去看一下Vue的响应式原理(后期如果有时间的话再写一篇文章介绍Vue的响应式)。/** * Collect dependencies on array elements when the array is touched, since * we cannot intercept array element access like property getters. */ function dependArray (value: Array<any>) { for (let e, i = 0, l = value.length; i < l; i++) { e = value[i] e && e.__ob__ && e.__ob__.dep.depend() if (Array.isArray(e)) { dependArray(e) } } }
将数组的每一项都加入到dep中,下面看一下observe的具体实现
/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */ export function observe (value: any, asRootData: ?boolean): Observer | void { // 不是对象或者是组件就return if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void // 响应式对象都会加上__ob__属性 // 如果已经是响应式的了就直接取响应式对象,不是的话在重新Observer if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( // 这里的shouldObserve,初始化inject的时候设置为了false shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } // 如果是响应式对象则返回,否则返回原始对象 return ob; }
其实到这整个初始化inject的过程就结束了。
-
不知道大家看到这里有没有疑惑,就是上面的inject的key取值,是从from属性取的
const provideKey = inject[key].from
,但是我们根本就没有设置from属性,那不就是undefined了吗?那还怎么取呢?其实在一开始初始化的时候Vue都帮你做好了// 在Vue调用init的时候会进行mergeOptions操作,里面会调用 normalizeInject(child, vm) /** * Normalize all injections into Object-based format */ function normalizeInject (options: Object, vm: ?Component) { const inject = options.inject if (!inject) return // 置为空对象 const normalized = options.inject = {} // 这种形式的inject: ['item', 'test', 'user'], if (Array.isArray(inject)) { for (let i = 0; i < inject.length; i++) { normalized[inject[i]] = { from: inject[i] } } } else if (isPlainObject(inject)) { // 这种形式的 // inject: { // foo: { // from: 'bar', // default: 'foo' // } // } for (const key in inject) { const val = inject[key] normalized[key] = isPlainObject(val) // 有就用原来的没有就设置 ? extend({ from: key }, val) : { from: val } } } } /** * Mix properties into target object. */ export function extend (to: Object, _from: ?Object): Object { for (const key in _from) { to[key] = _from[key] } return to }
在上面Vue会为每一个inject的key添加from属性
小结
- 总体的流程就是根据组件配置的inject,去它的所有“父级”组件去查找,如果找到了就通过defineReactive将该属性增加到vm里面,然后observe这个值。如果没有找到就使用default的值,再没有就warn
- 首先当我们初始化Vue的时候会初始化
-
Provide
-
初始化provide的过程其实很简单
export function initProvide (vm: Component) { const provide = vm.$options.provide if (provide) { // provide 选项应该是一个对象或返回一个对象的函数。 // inject取值也是从_provided里面 vm._provided = typeof provide === 'function' ? provide.call(vm) : provide } }
流程就是查看当前组件有没有provide属性,如果有的话就执行provide
-
2. 为什么provide 和 inject 绑定并不是可响应的?
我的理解是每个Inject在初始化的时候都是一个新的响应式对象和之前的是分开的,相当于把inject的对象作为新的数据放到这个组件上面。但是对于对象类型的数据,因为inject的数据在defineReactive的时候并没有深层的observe所有的key,所以对象形式的修改其实还是“响应式的”。
3. Issue
我们来看一下关于上面的两个issue
-
#6574:其实这个说的是Vue一开始在查找的时候没有忽略
__ob__
这个属性,导致每次都会报Injection"__ob__" not found
的warnning -
#7981:看了一下评论,好像一开始是因为一个人提了一个issue说在组件初始化时会调用getter,他期望不要一开始就立即调用而是希望自己在需要的时候调用getter(他好像有个延迟加载并调用后端接口的场景)。然后尤大说什么时候调用getter是没有保证的并且getter不应该有副作用。然后提这个PR(链接)的人就在下面评论了一个方案。感兴趣的可以自己看一下,我只看明白了个大概。了解的可以在评论区科普一下。
以下源码基于Vue 3.2.29版本
1. 初始化数据流程
-
Inject
- 其实整体流程都差不多,我们直接看具体的实现吧
export function inject( key: InjectionKey<any> | string, defaultValue?: unknown, treatDefaultAsFactory = false ) { // fallback to `currentRenderingInstance` so that this can be called in // a functional component const instance = currentInstance || currentRenderingInstance if (instance) { // #2400 // to support `app.use` plugins, // fallback to appContext's `provides` if the instance is at root // 跟vue 2.x同理 const provides = instance.parent == null ? instance.vnode.appContext && instance.vnode.appContext.provides : instance.parent.provides if (provides && (key as string | symbol) in provides) { return provides[key as string] } else if (arguments.length > 1) { return treatDefaultAsFactory && isFunction(defaultValue) ? defaultValue.call(instance.proxy) : defaultValue } } }
企业也是根据inject的值在组件的“父级”去查找,有的话就赋值,没有的话就取默认值,再没有就warn
-
Provide
export function provide<T>(key: InjectionKey<T> | string | number, value: T) { let provides = currentInstance.provides // by default an instance inherits its parent's provides object // but when it needs to provide values of its own, it creates its // own provides object using parent provides object as prototype. // this way in `inject` we can simply look up injections from direct // parent and let the prototype chain do the work. const parentProvides = currentInstance.parent && currentInstance.parent.provides if (parentProvides === provides) { provides = currentInstance.provides = Object.create(parentProvides) } // TS doesn't allow symbol as index type provides[key as string] = value }
-
Issue
#2400:这个主要的原因是使用者的组件inject使用了自己provide的数据,此修复程序确保组件中的注入来自父级。对于根组件,可以注入通过插件提供的属性。