Vue provide与inject的实现

190 阅读3分钟

简介:

Vue中project与inject的使用比较简单,父级或祖级使用provide提供,子级孙级使用inject接收,这里不做介绍了,简单给个例子。

var Provider = {
  provide:{
    foo:'bar'
  },
  //....
}
//子
var Child = {
  inject:['foo'],
  created(){
    console.log(this.foo);
  }
}

背景:

在hummer/tenon-vue这个多端框架中,目标为实现换肤这一简单功能,由于编译最终为客户端代码,故不存在css继承、属性选择器、伪类,所以传统在根级标签中添加属性选择器的方案不可行。

所以我想到的方案为将主题theme使用变量通过祖级组件provide提供给所有子孙级组件,再通过把theme作为函数返回值的形式,将函数传给子孙级,子孙级即可获取。

方案比较简单,实用性也还好,但我无意间听到同事的实现方案:

相同点是:theme依然存在根级,区别他每个子孙级容器都会查询自己是否有theme,如果没有去查询自己的父级,若父级也没有,则查找爷爷级,直到查到存在或查到根级为止。

先不考虑他实现方案是否更优,但给我打开了一扇新的大门,这是我常年做业务,少做设计所思考的事情。然后我思考了下,这与我的方案有什么不同,优劣在哪?仔细一想我甚至不知道我的实现原理。知道通过provide/inject实现跨层级数据传输了,故此,我要好好了解下provide/inject的实现了。

源码:

  1. 入口位置:src/core/instance/init.js 50行左右

    initInjections(vm)//子组件将数据注入到自己身上
    initProvide(vm);//父组件在初始化的时候提供好数据
    
  2. provide源码:src/core/inject.js:第8行

    • 说明:其实就是在vm上增加_provide vm._provide.foo = 'bar'
    export function initProvide (vm: Component) {
       const provide = vm.$options.provide
       if (provide) {
         vm._provided = typeof provide === 'function'
           ? provide.call(vm)
           : provide
       }
     }
    
  3. inject源码

    • 说明:不停的去从实例上查找source._provided属性,一层层向上查找,找到为止
    export function initInjections (vm: Component) {
       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 {
             defineReactive(vm, key, result[key])
           }
         })
         toggleObserving(true)
       }
     }
    
     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
           while (source) {
             if (source._provided && hasOwn(source._provided, provideKey)) {
               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
       }
     }
    
  4. 总结: 简单来说就是全局vm上添加一个属性provide存我们定义的变量,inject一层层向上查找目标属性,找到为止!

结束语:

这么看的话,同事的实现方式实质是与provide/inject的实现一致,甚至连缺点也一致,都是无法知道数据流的来源与流向;当然,这里我们不考虑重名的问题,但重名是依然是这个设计的存在的问题。

我没有看他的代码,但感觉实现方式是构造一个类去自动向上级去取,封装后应该代码也不多,相对比,没有明显优劣,我的主要就是比较容易理解了,但我依然要加强关于程序设计的思考,不能简单局限在业务中。