vue源码provide/inject原理解析

1,126 阅读1分钟

依赖注入

当深层嵌套的子孙组件想要拿到父组件的数据时,我们可以使用provide和inject。官网原理图: 依赖注入

使用案例: 我们假设组件嵌套结构如下:

Root
└─ TodoList
   ├─ TodoItem
   └─ TodoListFooter
      ├─ ClearTodosButton
      └─ TodoListStatistics

我们通过provide和inject将组件TodoList的属性直接传给组件TodoListStatistics:

const app = Vue.createApp({})

app.component('todo-list', {
  data() {
    return {
      todos: ['张三', '李四']
    }
  },
  provide: { // provide是一个对象
    name: '王五'
  },
  template: `
    <div>
      {{ todos.length }}
    </div>
  `
})

app.component('todo-list-statistics', {
  inject: ['name'],
  created() {
    console.log(`Injected 属性: ${this.name}`) // > Injected 属性: 王五
  }
})

上例中的provide定义为一个对象。如果需要在provide里使用data中的属性,需要把provide定义成一个方法,否则会报错。

app.component('todo-list', {
  data() {
    return {
      todos: ['张三', '李四']
    }
  },
  provide() { // provide是一个function
    return {
      todoLength: this.todos.length
    }
  },
  template: `
    ...
  `
})

依赖注入的优缺点如下:

优点:

  • 祖先组件不需要知道哪些后代组件使用它提供的数据;
  • 后代组件不需要知道被注入的数据来自哪里; 缺点:
  • 组件间的耦合较为紧密,不易重构;
  • 提供的属性是非响应式的;解决方案见官方文档

源码解读

了解了inject和provide的使用后,我们来深层解读下源码实现。废话不多说,直接上源码:

组件实例初始化的时候会调用Vue.prototype._init,该文件中,可以了解到:

  • inject、provide的初始化时间在生命周期钩子函数beforeCreate之后,created之前;
  • initInjections(vm) 解析inject是在初始化data/props之前;
  • initProvide(vm) 解析provide是在初始化data/props之后;

这也符合数据初始化的一个处理逻辑。

Vue.prototype._init源码:

//初始化inject和provide
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

initInjections函数,功能是获取组件注册的inject属性合集,然后遍历合集进行响应式监听。 源码如下:

export function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm) // 1、根据注册的inject,通过$parent向上查找对应的provide
  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]) // 2、进行响应式监听
      }
    })
    toggleObserving(true)
  }
}

resolveInject函数,功能是通过$parent一层层向上查找祖先节点的数据,直到找到对应于inject的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++) { // 遍历所有inject为其赋值
      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) { // 核心原理:通过$parent一层一层向上查找祖先节点的provide,找到则对inject进行赋值
        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
  }
}

initProvide函数,该方法单纯把组件注册的provide值,赋值给vm._provided,resolveInject中有使用到。

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

总结

依赖注入,其核心原理就是通过$parent向上查找祖先组件中的provide,找到则赋值给对应的inject即可。 仔细一思量,老铁们会发想,依赖注入原理和JavaScript中的instanceof操作符原理有异曲同工之处。在instanceof中,通过__proto__向原型链中查找,如果__proto__与构造函数的prototype相等则返回true。哈哈哈哈哈,这就是研究原理的有趣之处。