vue data数据修改_你真的理解Vue的数据响应式吗??

作者:Paula Hu
博客: 掘金主页知乎主页GitHub主页
点赞是对原创者最大的肯定,我希望在这里看到你努力的身影~~~

写在前面

我相信很多同学对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实例都会收到消息,并重新渲染与其关联的组件。

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

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

  • getter/setter
  • Object.defineProperty()


假设有这样一个需求:我们需要存储一个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"; 
import App from "./App.vue";

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)

打印结果为:

c38797505d9a262f396585ff78b1a521.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");

结果为:

b26fcf027267f7ed7e83dd7d2763299a.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");

我们查看一下控制台:

1ca853db63aa185ec51dd1b051f513ca.png


我们发现尤雨溪对着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来验证自己的猜想,这样你可以不用看源码就能知道这个技术的一些细节。
同样,以上是我的一些学习思路和总结,欢迎大家和我多多交流,如有错误烦请指正~~~

最后,你们的点赞是我继续分享的最大动力哦~~~❤️❤️❤️

参考链接

  • Vue的官方文档
  • getter/setter
  • Object.defineProperty()
  • 《Vue 自测题》中答错率最高的题的解释
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值