一探 Vue 数据响应式原理

本文写于 2020 年 8 月 5 日

相信在很多新人第一次使用 Vue 这种框架的时候,就会被其修改数据便自动更新视图的操作所震撼。

Vue 的文档中也这么写道:

Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。

单看这句话,像我这种菜鸟程序员必然是看不懂的。我只知道,在 new Vue() 时传入的 data 属性一旦产生变化,那么在视图里的变量也会随之而变。

但这个变化是如何实现的呢?接下来让我们,一探究竟。

1 偷偷变化的 data

我们先来新建一个变量:let data = { msg: 'hello world' }

接着我们将这个 data 传给 Vue 的 data:

  1. let data = { msg: 'hello world' }
  2. /*****留空处*****/
  3. new Vue({
  4. data,
  5. methods: {
  6. showData() {
  7. console.log(data)
  8. }
  9. }
  10. })

这看似是非常平常的操作,但是我们在触发 showData 的时候,会发现打出来 data 不太对劲:

  1. msg: (...)
  2. __ob__: Observer {value: {…}, dep: Dep, vmCount: 1}
  3. get msg: ƒ reactiveGetter()
  4. set msg: ƒ reactiveSetter(newVal)
  5. __proto__: Object

它不仅多了很多没见过的属性,还把里面的 msg: hello world 变成了 msg: (...)

接下来我们尝试在留空处打印出 data,即在定义完 data 之后立即将其打印。

但是很不幸,打印出来依然是上面这个不对劲的值。

可是很明显,当我们不去 new Vue(),并且传入 data 的时候,data 的打印结果绝对不是这样。

所以我们可以尝试利用 setTimeout()new Vue() 延迟 3 秒执行。

这个时候我们就会惊讶的发现:

  1. 当我们在 3s 内点开 console 的结果时,data 是普通的形式;
  2. 当我们在 3s 后点开 console 的结果时,data 又变成了奇怪的形式。

这说明就是 new Vue() 的过程中,Vue 偷偷的对 data 进行了修改!正是这个修改,让 data 的数据,变成了响应式数据。

2 (...) 的由来

为什么好好的一个 msg 属性会变成 (...) 呢?

这就涉及到了 ES6 中的 getter 和 setter。(如果理解 getter/setter,可跳至下一节)

一般我们如果需要计算后的值,会定义一个函数,例如:

  1. const obj = {
  2. number: 5,
  3. double() {
  4. return this.number * 2;
  5. }
  6. };

在使用的时候,我们写上 obj.double(obj.number) 即可。

但是函数是需要加括号的,我太懒了,以至于括号都不想要了。

于是就有了 getter 方法:

  1. const obj = {
  2. number: 5,
  3. get double() {
  4. return this.number * 2;
  5. }
  6. };
  7. const newNumber = obj.double;

这样一来,就能够不需要括号,就可以得到 return 的值。

setter 同理:

  1. const obj = {
  2. number: 5,
  3. set double(value) {
  4. if(this.number * 2 != value;)
  5. this.number = value;
  6. }
  7. };
  8. obj.double = obj.number * 2;

由此我们可以看出:通过 setter,我们可以达到给赋值设限的效果,例如这里我就要求新值必须是原值的两倍才可以。

但经常的,我们会用 getter/setter 来隐藏一个变量

比如:

  1. const obj = {
  2. _number: 5,
  3. get number() {
  4. return this._number;
  5. },
  6. set number(value) {
  7. this._number = value;
  8. }
  9. };

这个时候我们打印出 obj,就会惊讶的发现 (...) 出现了:

  1. number: (...)
  2. _number: 5

现在我们明白了,Vue 偷偷做的事情,就是把 data 里面的数据全变成了 getter/setter。

3 利用 Object.defineProperty() 实现代理

这个时候我们想一个问题,原来我们可以通过 obj.c = 'c'; 来定义 c 的值——即使 c 本身不在 obj 中。

但如何定义一个 getter/setter 呢?答:使用 Object.defineProperty()

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

例如我们上面写的 obj.c = 'c';,就可以通过

  1. const obj = {
  2. a: 'a',
  3. b: 'b'
  4. }
  5. Object.defineProperty(obj, 'c', {
  6. value: 'c'
  7. })

Object.defineProperty() 接收三个参数:第一个是要定义属性的对象;第二个是要定义或修改的属性的名称或 Symbol;第三个则是要定义或修改的属性描述符。

在第三个参数中,可以接收多个属性,value 代表「值」,除此之外还有 configurable, enumerable, writable, get, set 一共六个属性。

这里我们只看 getset

之前我们说了,通过 getter/setter 我们可以把不想让别人直接操作的数据“藏起来”。

可是本质上,我们只是在前面加了一个 _ 而已,直接访问是可以绕过我们的 getter/setter 的!

那么我们怎么办呢?

利用代理。这个代理不是 ES6 新增的 Proxy,而是设计模式的一种。

我们刚刚为什么可以去修改我们“藏起来”的属性值?

因为我们知道它的名字呀!如果我不给他名字,自然别人就不可能修改了。

例如我们写一个函数,然后把数据传进去:

  1. proxy({ a: 'a' })

这样一来我们的 { a: 'a' } 就根本没有名字了,无从改起!

接下来我们在定义 proxy 函数时,可以新建一个空对象,然后遍历传入的值,分别进行 Object.defineProperty()将传入的对象的 keys 作为 getter/setter 赋给新建的空对象

最后,我们 return 这个对象即可。

  1. let data = proxy({
  2. a: 'a',
  3. b: 'b'
  4. });
  5. function proxy(data) {
  6. const obj = {};
  7. const keys = Object.keys(data);
  8. for (let i = 0; i < keys.length; i++) {
  9. Object.defineProperty(obj, keys[i], {
  10. get() {
  11. return data[keys[i]];
  12. },
  13. set(value) {
  14. if (value < 0) return;
  15. data[keys[i]] = value;
  16. }
  17. });
  18. }
  19. return obj;
  20. }

这样一来,我们一开始声明的 data,就是我们 return 的对象了。在这个对象里,没有原始的数据,别人无法绕过 getter/setter 进行操作!

但是往往并没有这么简单,如果我一定需要一个变量名呢?

  1. const sourceData = {
  2. a: 'a',
  3. b: 'b'
  4. };
  5. let data = proxy(sourceData);

如此一来,通过直接操作 sourceData.a,时可以直接绕过我们在 proxy 中设置的 set a 进行赋值的。这个时候我们怎么处理?

很简单嘛,当我们遍历传入的数据时,我们可以对传入的数据新增 getter/setter,此后原始的数据就会被 getter/setter 所替代

在刚刚的代码中,我们在循环的刚开始添加这样一段代码:

  1. for(/*......*/) {
  2. const value = data[keys[i]];
  3. Object.defineProperty(data, keys[i], {
  4. get() {
  5. return value;
  6. },
  7. set(newValue) {
  8. if (newValue < 0) return;
  9. value = newValue;
  10. }
  11. });
  12. /*......*/
  13. }

这是什么意思呢?

我们利用了闭包,将原始值单独拎出来,每一次对原始属性进行读写,其实都是 get 和 set 在读取闭包时被拎出来的值。

那么不管别人是操作我们的 let data = proxy(sourceData); 的 data,还是操作 sourceData,都会被我们的 getter/setter 所拦截。

4 回到 Vue

我们刚刚写的代码是这样的:

  1. let data = proxy({
  2. a: 'a'
  3. })
  4. function proxy(data) {
  5. }

那如果我改成这样呢:

  1. let data = proxy({
  2. data: {
  3. a: 'a'
  4. }
  5. })
  6. function proxy({ data }) {
  7. // 结构赋值
  8. }

是不是和 Vue 就非常非常像了!

const vm = new Vue({ data: {} }) 也是让 vm 成为 data 的代理,并且就算你从外部将数据传给 data,也会被 Vue 所捕捉。

而在每一次捕获到你操作数据之后,就会对需要改变的 UI 进行重新渲染。

同理,Vue 对 computed 和 watch 也存在着各种偷偷的处理。

5 Vue 数据响应式的 Bug

如果我们的数据是这样:

  1. data: {
  2. obj: {
  3. a: 'a'
  4. }
  5. }

我们在 Vue 的 template 里却写了 <div>{{ obj.b }}<div> 会怎样?

Vue 对于不存在或者为 undefined 和 null 的数据是不予以显示的。但是当我们往 obj 中新增 b 的时候,他会显示吗?

写法一:

  1. const vm = new Vue({
  2. data: {
  3. obj: {
  4. a: 'a'
  5. }
  6. },
  7. methods: {
  8. changeObj() {
  9. this.obj.b = 'b';
  10. }
  11. }
  12. })

我们可以给一个按钮绑定 changeObj 事件,但是很遗憾,这样并不能使视图中的 obj.b 显示出来。

回想一下刚刚我们对于数据的处理,是不是只遍历了外层?这就是因为 Vue 并没有对 b 进行监听,他根本不知道你的 b 是如何变化的,自然也就不会去更新视图层了。

写法 2:

  1. const vm = new Vue({
  2. data: {
  3. obj: {
  4. a: 'a'
  5. }
  6. },
  7. methods: {
  8. changeObj() {
  9. this.obj.a = 'a2'
  10. this.obj.b = 'b';
  11. }
  12. }
  13. })

我们仅仅只是新增了一行代码,在改变 b 之前先改变了 a,居然就让 b 实现了更新!

这是为什么?

因为视图更新其实是异步的。

当我们让 a'a' 变成 'a2' 时,Vue 会监听到这个变化,但是 Vue 并不能马上更新视图,因为 Vue 是使用 Object.defineProperty() 这样的方式来监听变化的,监听到变化后会创建一个视图更新任务到任务队列里。

所以在视图更新之前,要先把余下的代码运行完才行,也就是会运行 b = 'b'

最后等到视图更新的时候,由于 Vue 会去做 diff 算法,于是 Vue 就会发现 a 和 b 都变了,自然会去更新相对应的视图。

但是这并不是我们解决问题的办法,写法 2 充其量只能算是“副作用”。

Vue 其实提供了方法让我们来新增以前没有生命的属性:Vue.set() 或者 this.$set()

Vue.set(this.obj, 'b', 'b'); 会代替我们进行 obj.b = 'b';,然后监听 b 的变化,触发视图更新。

那数组怎么响应呢?

每当我们往数组里新增元素的时候,数组就在不断的变长。对于没有声明的数组下标,很明显 Vue 不会给予监听呀。

比如 a: [1, 2, 3],当我新增一个元素,让 a === [1, 2, 3, 4] 的时候,a[3] 是不会被监听的

总不能每次 push 数组,都要手写刚刚说的 Vue.set 方法吧。

可实际操作中,我们发现并没有呀,Vue 监听了新增的数据。

这是因为 Vue 又偷偷的干了一件事儿,它把你原本的数组方法给改了一些。

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

在 Vue 中的数组所带的这七个方法都不是原生的方法了。Vue 考虑到这些操作极为常用,所在中间为我们添加了监听。

讲到这里,相信大家对 Vue 的响应式原理应该有了更深的认识了。

(完)

一探 Vue 数据响应式原理的更多相关文章

  1. Vue 数据响应式原理

    Vue 数据响应式原理 Vue.js 的核心包括一套“响应式系统”.“响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码.例如,视图渲染中使用了数据,数据改变后,视图也会自动更新. 举个简单 ...

  2. vue.js响应式原理解析与实现

    vue.js响应式原理解析与实现 从很久之前就已经接触过了angularjs了,当时就已经了解到,angularjs是通过脏检查来实现数据监测以及页面更新渲染.之后,再接触了vue.js,当时也一度很 ...

  3. vue深入响应式原理

    vue深入响应式原理 深入响应式原理 — Vue.jshttps://cn.vuejs.org/v2/guide/reactivity.html 注意:这里说的响应式不是bootsharp那种前端UI ...

  4. 深入解析vue.js响应式原理与实现

    vue.js响应式原理解析与实现.angularjs是通过脏检查来实现数据监测以及页面更新渲染.之后,再接触了vue.js,当时也一度很好奇vue.js是如何监测数据更新并且重新渲染页面.vue.js ...

  5. Vue的响应式原理

    Vue的响应式原理 一.响应式的底层实现 1.Vue与MVVM Vue是一个 MVVM框架,其各层的对应关系如下 View层:在Vue中是绑定dom对象的HTML ViewModel层:在Vue中是实 ...

  6. Vue.js响应式原理

      写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出. 文章的原地址:answershuto/learnV ...

  7. Vue的响应式原理---(v-model中的双向绑定原理)

    Vue响应式原理 不要认为数据发生改变,界面跟着更新是理所当然. 具体代码实现:https://gitee.com/ahaMOMO/Vue-Responsive-Principle.git 看下图: ...

  8. vue系列---响应式原理实现及Observer源码解析(一)

    _ 阅读目录 一. 什么是响应式? 二:如何侦测数据的变化? 2.1 Object.defineProperty() 侦测对象属性值变化 2.2 如何侦测数组的索引值的变化 2.3 如何监听数组内容的 ...

  9. Vue.js 响应式原理

    1. Vue2.x 基于 Object.defineProperty 方法实现响应式(Vue3 将采用 Proxy) Object.defineProperty(obj, prop, descript ...

随机推荐

  1. 用maven建立一个工程5

    在命令行里面输入cd myapp再按回车 再输入mvn compile再按回车 再输入 cd target按回车 再输入cd../按回车 再输入mvn package按回车 最后输入java -cla ...

  2. 规范之“用流中的Stream.Of(arr1,arr2)将两个集合合并”

    案例:用流中的Stream.Of(arr1,arr2)将两个集合合并 /** * 功能描述: * 两个对象集合添加到一起 * 在用flatMap扁平化改为Stream<User> * 这样 ...

  3. 印度下架54款中国APP,中东政策逐年收紧,伊拉克成蓝海市场

    2月14日,印度电子和信息技术部以"安全威胁"为由对有中国基因的54款App下达禁令,15日,印度税务部门对某些在印中资企业多个场所进行搜查. 本批下架的App中主要以应用类App ...

  4. python大佬养成计划----flask_bootstrap装饰网页

    flask_bootstrap Bootstrap 是 Twitter 开发的一个开源框架,它提供的用户界面组件可用于创建整洁且具有吸引力的网页,而且这些网页还能兼容所有现代 Web 浏览器. Boo ...

  5. Codepen 每日精选(2018-4-11)

    按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以打开原始页面. 纯 css 写行走的大象https://codepen.io/FabioG/ful... 纯 css 画的 ...

  6. android.content.res.Resources$NotFoundException: String resource ID #0x0报错

    报错:android.content.res.Resources$NotFoundException: String resource ID #0x0 原因:在setText()中使用了int型的参数 ...

  7. Spring理解1 ioc

    Spring Spring是一个轻量级的控制反转(IOC)和面向切面(AOP)的容器(框架).   需要了解 ioc容器 IOC底层原理 IOC接口 BeanFactory Bean的作用域 IOC操 ...

  8. DirectX11 With Windows SDK--38 级联阴影映射(CSM)

    前言 在31章我们曾经实现过阴影映射,但是受到阴影贴图精度的限制,只能在场景中相当有限的范围内投射阴影.本章我们将以微软提供的例子和博客作为切入点,学习如何解决阴影中出现的Atrifacts: 边缘闪 ...

  9. 安卓记账本开发学习day9之依赖导入失败

    根据上一篇文章导入依赖,在一些旧版本的AS上能正常完成,但是我下载最新的AS以后无法正常导入 同步的时候控制台报 Build file 'C:\CS\AndroidStudioProjects\tal ...

  10. 把图片存储 canvas原生API转成base64

    1.LocalStorage有什么用? 2.LocalStorage的普通用法以及如何存储图片. 首先介绍下什么是LocalStorage 它是HTML5的一种最新储存技术.但它只能储存字符串.以前的 ...