一探 Vue 数据响应式原理

本文写于 2020 年 8 月 5 日

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

Vue 的文档中也这么写道:

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

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

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

1 偷偷变化的 data

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

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

let data = { msg: 'hello world' }

/*****留空处*****/

new Vue({
data,
methods: {
showData() {
console.log(data)
}
}
})

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

msg: (...)
__ob__: Observer {value: {…}, dep: Dep, vmCount: 1}
get msg: ƒ reactiveGetter()
set msg: ƒ reactiveSetter(newVal)
__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,可跳至下一节)

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

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

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

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

于是就有了 getter 方法:

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

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

setter 同理:

const obj = {
number: 5,
set double(value) {
if(this.number * 2 != value;)
this.number = value;
}
}; obj.double = obj.number * 2;

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

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

比如:

const obj = {
_number: 5,
get number() {
return this._number;
},
set number(value) {
this._number = value;
}
};

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

number: (...)
_number: 5

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

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

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

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

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

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

const obj = {
a: 'a',
b: 'b'
}
Object.defineProperty(obj, 'c', {
value: 'c'
})

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

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

这里我们只看 getset

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

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

那么我们怎么办呢?

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

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

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

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

proxy({ a: 'a' })

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

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

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

let data = proxy({
a: 'a',
b: 'b'
}); function proxy(data) {
const obj = {};
const keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
Object.defineProperty(obj, keys[i], {
get() {
return data[keys[i]];
},
set(value) {
if (value < 0) return;
data[keys[i]] = value;
}
});
}
return obj;
}

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

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

const sourceData = {
a: 'a',
b: 'b'
}; let data = proxy(sourceData);

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

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

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

for(/*......*/) {
const value = data[keys[i]];
Object.defineProperty(data, keys[i], {
get() {
return value;
},
set(newValue) {
if (newValue < 0) return;
value = newValue;
}
});
/*......*/
}

这是什么意思呢?

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

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

4 回到 Vue

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

let data = proxy({
a: 'a'
}) function proxy(data) { }

那如果我改成这样呢:

let data = proxy({
data: {
a: 'a'
}
}) function proxy({ data }) {
// 结构赋值
}

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

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

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

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

5 Vue 数据响应式的 Bug

如果我们的数据是这样:

data: {
obj: {
a: 'a'
}
}

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

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

写法一:

const vm = new Vue({
data: {
obj: {
a: 'a'
}
},
methods: {
changeObj() {
this.obj.b = 'b';
}
}
})

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

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

写法 2:

const vm = new Vue({
data: {
obj: {
a: 'a'
}
},
methods: {
changeObj() {
this.obj.a = 'a2'
this.obj.b = 'b';
}
}
})

我们仅仅只是新增了一行代码,在改变 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. Java 面试问题列表包含的主题?

    多线程,并发及线程基础 数据类型转换的基本原则 垃圾回收(GC) Java 集合框架 数组 字符串 GOF 设计模式 SOLID 抽象类与接口 Java 基础,如 equals 和 hashcode ...

  2. 学习Kvm(七)

    六,管理虚拟网络 [x] Linux网桥基本概念 [x] qemu-kvm支持的网络 [x] 向虚拟机添加虚拟网络连接 [x] 基于NAT的虚拟网络 [x] 基于网桥的虚拟网络 [x] 用户自定义的隔 ...

  3. Effective Java —— 用私有构造器或枚举类型强化单例属性

    本文参考 本篇文章参考自<Effective Java>第三版第三条"Enforce the singleton property with a private construc ...

  4. 关于CDN那些事

    对于前端性能优化我们不得不了解的几个知识点:CDN.HTTP header信息 今天我就来谈谈我对cdn的理解 1.CDN是什么:CDN全称是Content Delivery Network,即内容分 ...

  5. 小程序wx.previewImage查看图片再次点击返回时重新加载页面问题

    wx.previewImage预览图片这个过程到底发生了什么? 首先我们点击图片预览,附上查看图片代码: <image class="headImg" data-src=&q ...

  6. 基于Apache组件,分析对象池原理

    池塘里养:Object: 一.设计与原理 1.基础案例 首先看一个基于common-pool2对象池组件的应用案例,主要有工厂类.对象池.对象三个核心角色,以及池化对象的使用流程: import or ...

  7. 服务器jupyter连接不上主机

    首先安装jupyter pip3 install jupyter -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com 然后 ...

  8. Java重载容易引发的错误—返回类型

    方法的签名仅仅与方法名和参数类型相关,而与访问控制符.返回类型无关,以及方法体中的内容都没有关系,下面用一个例子说明; 如果Student类两种签名,myStudent(int,int)返回int 类 ...

  9. gin框架使用【4.请求参数】

    GET url: http://127.0.0.1:8080/users?id=1&name=卷毛狒狒 package mainimport ( "github.com/gin-go ...

  10. el-table高度问题

    1. 外层容器flex=1,el-table设置属性height="calc(100% - 60px),有时一刷新页面表格高度无限增加,滚动条一直变短 试验过几次后发现是因为el-table ...