让 Vue.js 中的 provide / inject 支持响应式

Posted by Yinode on Friday, January 11, 2019

TOC

provide / inject

provide / inject Vue.js 2.2.0 版本后新增的 API.

还发出了这样的警告provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。

provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

我们这次的目标就是让该 API 实现响应式。为了了解这个 API 为什么无法响应式呢,我们先来看看源码

走进初始化

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) {
  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)
  }
}

接着我们来看看初始化的顺序

vm._self = vm
initLifecycle(vm) // 初始化一些实例上的值
initEvents(vm) // 将父组件的事件监听回到绑定子组件的_events上
initRender(vm) // 给实例增加上关于render的属性
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) // 对Props进行迁移,并对方法 计算属性 data 进行解剖和响应绑定
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

从以上的信息我们可以了解到,Provide无法响应式的真正原因其实就是没有加依赖收集,所以无论你如何调用$watch,都是无效的,你都无法以任何的形式进行监听。接着我们发现Provide的初始化发生在state之后,并且Provide支持函数的方式进行导入,所以我们可以在这里做一些文章

利用Data

我来演示一下我的方法

首先是父组件,利用data来为foo对象增加依赖收集机制,紧接着在provide完成初始化之后,对_provide.foo进行监听,这时候data中的foo中的dep就会增加依赖到这个新生成的watcher中。接下来,我们需要在子组件中使用他

export default {
  data() {
    return {
      foo: {
        a: 1
      }
    };
  },
  provide() {
    return {
      foo: this.foo
    };
  },
  created() {
    this.$watch(
      "_provide.foo",
      function() {
        console.log("changne");
      },
      {
        deep: true
      }
    );
  },
}
<template>
  <div class="hello">
    {{foo.a}}
  </div>
</template>

export default {
  name: "HelloWorld",
  inject: ["foo"],
  components: {
    anode
  },

  created() {
    setInterval(() => {
      this.foo.a = Date.now();
    }, 1000);
  }
};

其实在完成这一步之后,我们的foo正在被两个watcher所监听,分别是父组件中触发的$watch,已经子组件中的render watcher . 这样一但子组件修改foo.a 这两个watcher都会接收到更新。

如此,我们的目的就达到了,但是在这里提醒大家,使用这个东西前,你最好清楚的明白你需要什么,以及你到底做了什么。

我想到用这个东西的需求是,我需要写一个需要保存用户进度,角色等级的一个H5小游戏,如此,在父组件中声明一个比较大的笔记本对象,然后子组件修改这个笔记本对象中的内容,在父组件中进行观察者模式监听,并且利用防抖函数不停的存到后端。在局势可以掌控的情况下,我认为这种模式会带来一定的便利。

tip: 这里提醒一下各位 这种方法实际用起来其实并不好用 我更加推荐直接provide父对象的实例 在子组件中直接修改父对象实例上的属性,主要原因还是上面这种方法一但遇到异步,就需要在data里建立一个引用嵌套层。