一探 Vue 数据响应式原理
一探 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 秒执行。
这个时候我们就会惊讶的发现:
- 当我们在 3s 内点开 console 的结果时,data 是普通的形式;
- 当我们在 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
一共六个属性。
这里我们只看 get
与 set
。
之前我们说了,通过 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 数据响应式原理的更多相关文章
- Vue 数据响应式原理
Vue 数据响应式原理 Vue.js 的核心包括一套“响应式系统”.“响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码.例如,视图渲染中使用了数据,数据改变后,视图也会自动更新. 举个简单 ...
- vue.js响应式原理解析与实现
vue.js响应式原理解析与实现 从很久之前就已经接触过了angularjs了,当时就已经了解到,angularjs是通过脏检查来实现数据监测以及页面更新渲染.之后,再接触了vue.js,当时也一度很 ...
- vue深入响应式原理
vue深入响应式原理 深入响应式原理 — Vue.jshttps://cn.vuejs.org/v2/guide/reactivity.html 注意:这里说的响应式不是bootsharp那种前端UI ...
- 深入解析vue.js响应式原理与实现
vue.js响应式原理解析与实现.angularjs是通过脏检查来实现数据监测以及页面更新渲染.之后,再接触了vue.js,当时也一度很好奇vue.js是如何监测数据更新并且重新渲染页面.vue.js ...
- Vue的响应式原理
Vue的响应式原理 一.响应式的底层实现 1.Vue与MVVM Vue是一个 MVVM框架,其各层的对应关系如下 View层:在Vue中是绑定dom对象的HTML ViewModel层:在Vue中是实 ...
- Vue.js响应式原理
写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出. 文章的原地址:answershuto/learnV ...
- Vue的响应式原理---(v-model中的双向绑定原理)
Vue响应式原理 不要认为数据发生改变,界面跟着更新是理所当然. 具体代码实现:https://gitee.com/ahaMOMO/Vue-Responsive-Principle.git 看下图: ...
- vue系列---响应式原理实现及Observer源码解析(一)
_ 阅读目录 一. 什么是响应式? 二:如何侦测数据的变化? 2.1 Object.defineProperty() 侦测对象属性值变化 2.2 如何侦测数组的索引值的变化 2.3 如何监听数组内容的 ...
- Vue.js 响应式原理
1. Vue2.x 基于 Object.defineProperty 方法实现响应式(Vue3 将采用 Proxy) Object.defineProperty(obj, prop, descript ...
随机推荐
- vue3在组件上使用v-model
v-model用于在元素上创建双向数据绑定,负责监听用户输入事件来更新数据. v-model应用于输入框 <input v-model="searchText" /> ...
- Cookie与HttpSession对象
Cookie与HttpSession对象的作用 维护客户端浏览器与服务端会话状态的两个对象. 由于HTTP协议是一个无状态的协议,因此服务端不会记录当前客户端浏览器的访问状态 有些时候需要服务端能够记 ...
- 关于CPU、指令集、架构、芯片的一些科普
作者:王强链接:https://zhuanlan.zhihu.com/p/19893066来源:知乎 随着智能设备的广泛普及,这几年媒体上越来越多的出现关于"架构""AR ...
- (stm32f103学习总结)—待机唤醒实验
一.STM32待机模式介绍 1.1 STM32低功耗模式介绍 很多单片机具有低功耗模式,比如MSP430.STM8L等,我们的STM32 也不例外.默认情况下,系统复位或上电复位后,微控制器进入运行模 ...
- 一文读懂充电宝usb接口电路及制作原理详细
转自:http://www.elecfans.com/dianlutu/dianyuandianlu/20180511675801.html USB充电器套件,又名MP3/MP4充电器,输入AC160 ...
- CSS详细解读定位
一 前言 CSS定位是CSS布局只能够重要的一环.本篇文章带你解读定位属性,可以让你更加深入的理解定位带来的一些特性,熟练使用CSS布局. 二 正文 1.文档流布局的概念 将窗体自上而下分成一行行, ...
- vue全家桶+axios+jsonp+es6 仿肤君试用小程序
vue全家桶+axios+jsonp+es6 仿肤君试用小程序 把自己写的一个小程序项目用vue来实现的,代码里面有一些注释,主要使用了vue-cli,vue,vuex,vue-router,axoi ...
- 将base64转成File文件对象
function dataURLtoFile(dataurl, filename) { //将base64转换为文件 var arr = dataurl.split(','), ...
- 入行数字IC验证的一些建议
0x00 首先,推荐你看两本书,<"胡"说IC菜鸟工程师完美进阶>(pdf版本就行)本书介绍整个流程都有哪些岗位,充分了解IC行业的职业发展方向.<SoC设计方法 ...
- Spring配置数据源(连接池)
1.数据源(连接池)的作用:为了提高程序的性能而出现的 2.数据源的原理: *事先实例化数据源,初始化部分连接资源 *使用连接资源时从数据源中获取 *使用完毕后将连接资源归还给数据源 使用c3p0的步 ...