vue的数据响应式

写在前面

我相信很多同学对Vue的数据响应式是通过Vue.js文档来了解的,但毕竟文档的篇幅有限,你能知道自己理解了多少吗?先来看看下面几道题目,来看看你的理解是否到位。

题目一

//HTML
<div id="app">
  <span class=span-a>
    {{obj.a}} 
  </span>
  <span class=span-b>
    {{obj.b}}
  </span>
</div>

//JS
var app = new Vue({
  el: '#app',
  data: {
    obj: {
      a: 'a',
    }
  },
})
app.obj.a = 'a2'

问:请问最终 span-a 和 span-b 中分别展示什么字符串?

题目二

//HTML
<div id="app">
  <span class=span-a>
    {{obj.a}} 
  </span>
  <span class=span-b>
    {{obj.b}}
  </span>
</div>

//JS
var app = new Vue({
  el: '#app',
  data: {
    obj: {
      a: 'a',
    }
  },
})
app.obj.b = 'b'

问:请问最终 span-a 和 span-b 中分别展示什么字符串?

题目三

//HTML
<div id="app">
  <span class=span-a>
    {{obj.a}} 
  </span>
  <span class=span-b>
    {{obj.b}}
  </span>
</div>

//JS
var app = new Vue({
  el: '#app',
  data: {
    obj: {
      a: 'a',
    }
  },
})
app.obj.a = 'a2'
app.obj.b = 'b'

问:请问最终 span-a 和 span-b 中分别展示什么字符串?

好了,以上三题你都能想明白吗?带着这些疑问我们来深入了解一下Vue的数据响应式。

深入理解

什么是数据响应式

先聊一聊,什么是响应
“响应”,中文的意思也就是“回应”。比如,别人叫你一声或者给你发消息,你回复了他,这个过程就叫响应

那什么是数据响应式呢?
Vue的官方文档已经很明确的告诉我们了。对于数据data,只要你修改了,视图就会自动更新,不需要你自己再去操作DOM。这也是Vue 最独特的特性之一 —— 非侵入性的响应式系统

怎么理解“非侵入性”呢,我觉得的可以理解为“不可篡改的”,即Vue做到了只要你修改了data中的任意数据,对应的组件实例的watcher实例都会收到消息,并重新渲染与其关联的组件。

通过一个例子理解内部原理

接下来我们一起来了解一下,这一机理的背后是怎样进行的,我们来看下面这个例子:
进入实例前,大家需要了解以下前置知识:

假设有这样一个需求:我们需要存储一个n的值,有一个条件就是n不能小于0。
请问我们怎样保证,不管用户怎么修改n的值,都可以满足我们的要求。

let myData = {n:0}  
let data = proxy({ data:myData }) // 括号里是匿名对象,无法访问

function proxy({data}){
  //监听data的变化 
  let value = data.n      // 声明一个新的 value 来获取 data.n 这样就可以监听 data.n 的变化
  Object.defineProperty(data, 'n', {   
    get(){ 
      return value
    },
    set(newValue){
      if(newValue<0)return
      value = newValue
    }
  })

 //添加代理
  const obj = {}// 声明一个新的对象,把 data.n 全权负责给 obj,这样无轮怎样篡改,
                // 都不会影响 n 的变化
  Object.defineProperty(obj, 'n', {  
    get(){
      return data.n
    },
    set(value){
      if(value<0)return
      data.n = value
    }
  })
  return obj // obj 就是代理
}

代码分析

  • 首先,给我们要存储n的值的对象命名为myData
  • 对这个对象的操作,我们交给一个代理来完成,这个代理就是data
  • 为了防止用户修改myData,我们先对代理data进行监听
  • 这里我们声明了一个新的value来存储data.n的原始值
  • 接着定义一个新的(虚拟)n来覆盖原来的data.n,如果你要读取n的值,就调用get函数;如果你要设置新的值,就把新的值给value
  • 下一步,添加代理
  • 声明一个新的对象,把 data.n 全权负责给 obj,这样无轮怎样篡改,都不会影响 n 的变化
  • 这里返回了obj,所以data就是代理

好了,说了这么多,请问和vue的数据响应式有什么联系呢?
别急,我们再来看一个例子。

//引用完整版 Vue
import Vue from "vue/dist/vue.js"; 

Vue.config.productionTip = false;

const myData = {
  n: 0
}
console.log(myData) // 重点一

new Vue({
  data: myData,
  template: `
    <div>{{n}}</div>
  `
}).$mount("#app");

setTimeout(()=>{
  myData.n += 10
  console.log(myData) //重点二
},3000)

打印结果为:

image.png

是不是很奇怪。
为什么把 data 在外部创建,在 Vue 里引用,然后在创建后引用后分别打印一次,两次打印出来的n会不一样。

这是因为,Vue 对 data 里的数据进行了一些处理。

那又是什么样的处理?
我在第一个例子就已经告诉你了。

有没有觉得 let data = proxy({ data:myData }) 看着很眼熟。
没错,Vue在创建实例的时候, const vm = new Vue({data:myData}) ,也是如此。

用文档里的话来解释就是:

  • 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。
  • 这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。
  • 每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

说人话就是:

  • Vue 让 vm 成为了 myData 的代理,并且会对 myData 的所有属性进行监控
  • 什么是代理? 就是当你对myData对象的属性进行读写的时候,全权由另一个对象vm来负责,那么vm就是myData的代理。比如,不用myData.n,偏要用vm.n来操作myData.n。
  • 为什么要监控? 就是为了防止myData的属性变了,vm不知道
  • vm知道了又如何? 知道属性变了就可以调用render(data)啊~~~
  • 这样,就实现了只要修改了data.n,Vue就会自动帮你实现视图的重新渲染,即UI里的n就会响应我,这就是响应式的过程。

扩展

Vue似乎有个bug

好了,经过上面的分析,我相信同学们都能理解Vue数据响应式的原理了。
但是还没结束,这里有几个有意思的例子,带大家看一下。

  • 例一

我们知道,Vue是通过 Object.defineProperty(obj, 'n', {...}) 来实现监听和代理obj.n的,如果我们忘记给 'n' 了会怎么样?

//引用完整版 Vue
import Vue from "vue/dist/vue.js";

Vue.config.productionTip = false;

new Vue({
  data: {},
  template: `
    <div>{{n}}</div>
  `
}).$mount("#app");

结果为:

image.png

浏览器会抛出一个警告

  • 例二
//引用完整版 Vue
import Vue from "vue/dist/vue.js";

Vue.config.productionTip = false;

new Vue({
  data: {
    obj: {
      a: 0 // obj.a 会被 Vue 监听 & 代理
    }
  },
  template: `
    <div>
      {{obj.b}}
      <button @click="setB">set b</button>
    </div>
  `,
  methods: {
    setB() {
      this.obj.b = 1; //请问,页面中会显示 1 吗?
    }
  }
}).$mount("#app");

请问:此时我点击set n,视图会显示1吗
答案是:不会
为啥:因为Vue没法监听一开始不存在的obj.b

解决方案

方案一:提前声明好所有的key,后面再加属性

举例:

//引用完整版 Vue
import Vue from "vue/dist/vue.js";

Vue.config.productionTip = false;

new Vue({
  data: {
   n:undefined
  },
  template: `
    <div>{{n}}</div>
  `
}).$mount("#app");

方案二:使用Vue.set或this.$set

作用:

  • 新增key
  • 如果没有创建过,自动创建代理和监听
  • 触发视图更新(异步的,不会立刻更新)

举例:

//引用完整版 Vue
import Vue from "vue/dist/vue.js";

Vue.config.productionTip = false;

new Vue({
  data: {
    obj: {
      a: 0 // obj.a 会被 Vue 监听 & 代理
    }
  },
  template: `
    <div>
      {{obj.b}}
      <button @click="setB">set b</button>
    </div>
  `,
  methods: {
    setB() {
      Vue.set(this.obj,'b',1)
      // this.$set(this.obj,'b',1)  //这句和上面等同
    }
  }
}).$mount("#app");

针对数组的解决方案

我们很容易想到,如果是数组的话,没有办法提前就把后面要设置的属性就写好,那要怎么解决呢。

方案一:可以用Vue.set或this.$set(不推荐)

方案二:尤雨溪为我们提供了一种数组的变异方法

举例:

//引用完整版 Vue
import Vue from "vue/dist/vue.js";

Vue.config.productionTip = false;

new Vue({
  data: {
    array: ["a", "b", "c"]
  },
  template: `
    <div>
      {{array}}
      <button @click="setD">set d</button>
    </div>
  `,
  methods: {
    setD() {
      // this.$set(this.array,3,'d')  //可以实现,不推荐
      this.array.push('d')  //可以实现,推荐
    }
  }
}).$mount("#app");

我们查看一下控制台:

image

我们发现尤雨溪对着7个API进行了改造,方便你虽数组进行增删,这7个API会自动处理监听和代理,并更新视图。具体内容可以参考文档中的变异方法章节

回答一下最开始的问题

好了,现在我们可以回答最开始的题目了。

【题目一】
分析:

  • 由于obj.a提前定义了,后面又被改写为a2,所以会被监听;
  • 由于obj.b没有事先定义,所以不会被监听。

结论: span-a 中显示a2,span-b 中不显示。

【题目二】
分析:

  • 由于obj.a提前定义了,所以会被监听;
  • 由于obj.b没有事先定义,所以不会被监听。

结论: span-a 中显示a,span-b 中不显示。

【题目三】
分析:

  • 由于obj.a提前定义了,后面又被改写为a2,所以会被监听。这个没什么问题,但是后面的就有问题了
  • 大家会不会以为,obj.b没有事先定义,所以不会被监听。
  • 如果是这样想的话,那就错了,实际上span-b 中会显示b

深入分析:

  • 要理解为什么 span-b 会更新,要点是理解视图更新其实是异步的。
  • 当我们让 a 从 'a1' 变成 'a2' 时,Vue 会监听到这个变化,但是 Vue 并不能马上更新视图,因为 Vue 是使用 Object.defineProperty 这样的方式来监听变化的,监听到变化后会创建一个视图更新任务到任务队列里。
  • 所以在视图更新之前,要先把余下的代码运行完才行,也就是会运行 b = 'b'。
  • 等到视图更新的时候,由于 Vue 会去做 diff(文档中有写),于是 Vue 就会发现 a 和 b 都变了,自然会去更新 span-a 和 span-b。

结论: span-a 中显示a2,span-b 中显示b。

小结

通过以上分析,相信大家对Vue的数据响应式原理已经有了一定的理解。我觉得,关键是要多看文档,多思考、多总结,通过写一些demo来验证自己的猜想,这样你可以不用看源码就能知道这个技术的一些细节。

知乎:Paula Hu

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