vue新特性provide/inject深入学习

provide/inject深入学习

本文深入探究provide,inject

在官网porivide, inject的介绍中, 有这样的一段话:

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

本文将深入探究这句话的意思.

父引用和子引用

在无provide和inject时代, 一般通过组件中$parent(父引用)和$children(子组件列表)来实现在组件树上进行跨组件状态或者属性的引用, 在子组件中通过递归或者循环获取$parent的方式的得到祖先组件的数据, 由于获取到的数据是可响应的,使用时自然也就是响应式的。如在子组件中有代码:


computed: {

    count() {

      return (this.$parent || {}).count;

    },

    user() {

      return (this.$parent || {}).user;

    },

  },

当父组件的count(基本类型)或者user(引用类型)发生改变时, 会自动更新子组件的computed。这在组合组件(类似select和option)中非常有用, 可以把一些子组件共有的特性抽取到父组件中去进行统一设置, 如el-form中的label-width就可以为所有的子组件el-form-item设置一个默认的label-width

实际使用时,不会直接对this.$parent获取目标组件, 因为这样耦合性太高, 一般通过循环或者递归查找某种特征的组件,具体可以查看element-uiemitter中冒泡中是如何查找目标组件触发事件.

$children是不支持响应式的.

provide/inject传递数据

利用provide/inject机制也可以在组件树中数据自上而下进行传递: 在祖先组件中通过provide中暴露一些数据, 子组件获取这些数据. 如祖先组件中:


  props: {

    count: Number,

    user: Object,

  },

  data() {

    return {

      grade: {

        number: 12,

      },

    };

  },

  provide() {

    return {

      count: this.count,

      user: this.user,

      grade: this.grade,

    };

  },

其中, user是外部组件通过props传递给父组件的数据, 结构为:


{

    name: ''.

    age: 0,

    address: {

        number: 0,

    }

}

在子组件中, 通过inject获取这些数据:


{

  inject: ['count', 'user', 'grade'],

  created () {

    console.log(this.count);

  }

  // ...

}

provide/inject响应式特性深入探究

对上章节中provide/inject的案例进行响应式研究, 可以发现user,grade都是响应式数据结构,count不是。接着依次改变grade.number,user.address.number,user.age,user.address, 会发现子组件能接受到的响应式数据,但当改变了grade,user,count时, 子组件不能响应式渲染页面, 且改变grade,user`的属性也会发现不会响应式渲染。

其中count由于是一个不可监听的对象, 没有在子组件中动态渲染, 符合期望。其后在改变grade.number, user.address.number, user.age, user.address时, 子组件可以动态渲染改变值,这是由于这些属性都在一个可监听对象中。改变grade, user的后,发现子组件没有动态渲染, 证明grade, user是不可响应的,这是由于grade, user不在一个可监听的对象里面。 grade, user在哪呢? 在provide的声明中, 返回了一个对象:


 provide() {

    return {

      count: this.count,

      user: this.user,

      grade: this.grade,

    };

  }

grade, user就在这个对象里面, 该对象不是可监听对象, 所以导致grade, user不是响应式的. 首先的解决方案就是, 将这个对象作为一个响应式的,可以在data中声明一个容器字段, 用于包装需要传递的数据, 如:


  data() {

    return {

      context: {

        user: '',

        count: 0,

        grade: '',

      },

      grade: {

        number: 12,

      },

    };

  },

  watch: {

    user(val) {

      this.context.user = val;

    },

    count(val) {

      this.context.count = val;

    },

    grade(val) {

      this.context.grade = val;

    },

  },

  created() {

    this.context.user = this.user;

    this.context.count = this.count;

    this.context.grade = this.grade;

  },

为什么这样写, 是由于provide/inject机制不支持响应式编程的,后续对provide返回的对象直接修改不会重新刷新provide/inject机制, 也就是provide返回的对象的最顶层的响应机制会失效,且无法对对象顶层属性进行操作。这个机制会导致以下三种方式不能实现响应式传递:

  • 上文中的context不能在computed中声明。因为每次computed都会返回一个新的值(引用),而provide只会记录一开始的context的那个引用, 后续数据发生变更, 新的context不会被刷新到provide中去。

  • 上文中的context就算data中声明的, 但如果在某个地方执行了this.context = {...}, 新的context也不会被更新在provide, provide中的context永远是在初始化时复制给他的那个引用。这会导致在父组件中context可以动态刷新, 但是子组件中的context不会动态刷新。

  • 直接在provide函数中返回上文中的context,那么user, grade就会成为顶层属性,在created中进行的重新赋值操作和后续的重新赋值操作都不会响应到provide中, 将会失去响应式。

按照上面的写法, 发现grade, user已经能够动态在子组件中渲染了。由上得到结论:要想provide传递的数据一直是可响应的, 需要provide传递的每一个属性的值都是一个引用不变的可监听对象。

每次维护context太麻烦了, 有没有简便方法? 可以这样写:


 provide() {

    return {

      group: this,

    };

  }

直接将父组件的引用放在provide返回对象的一个属性中, this代表当前实例, 引用实不会发生变化的, 且this中大多数属性都是响应式的。但需要注意带$前缀的属性大多数都不是响应式属性,如$el,子组件在使用这些属性时, 不会动态渲染。如果父组件有更大的作用域时,比如同时为多种类型子组件服务,或者允许第三方子组件inject使用时, 建议不要直接传递this, 而是在provide中暴露特定的api。但是按照上文中维护一个特定的context对象太繁琐了,可以使用函数来保证引用不变(推荐使用该方式):


provide() {

    return {

      getContext: () => ({

          user: this.user,

          grade: this.grade,

      })

    };

  }

在子组件中:


inject: ['getContext'],

computed: {

    context() {

        return this.getContext();

    }

}

原理

本章节从源码的角度来解密provide为什么有如此怪异的特性.

provide/inject为什么具备响应式, 因为它的实现机制也是通过$parent实现的。provide会在初始化时放到一个_provided的属性中,子组件在访问inject的值时, 会通过$parent属性向上查找所有祖先节点的_provided获取第一个有目标属性的值, 因此子组件跟父组件是共享一个变量,如果这个变量是引用类型的话, 在子组件中改变这个值, 父组件中该值也会发生了变化。

为什么provide返回的对象的最顶层的响应机制会失效? 在查找inject的值时, 会将目标_provide的单个属性拿出来, 放在一个不是响应式的对象中。从设计上也是合理的,provide的是一个数据集, 而不是一个数据,子组件inject到的数据可能从多个provide中挑选的集合, 故父组件的provide没有必要维持这个数据集的响应式 。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,050评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,538评论 1 306
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,673评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,622评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,047评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,974评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,129评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,893评论 0 209
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,654评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,828评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,297评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,619评论 3 262
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,326评论 3 243
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,176评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,975评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,118评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,909评论 2 278