本文写于 2020 年 8 月 5 日html
相信在不少新人第一次使用 Vue 这种框架的时候,就会被其修改数据便自动更新视图的操做所震撼。react
Vue 的文档中也这么写道:程序员
Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。算法
单看这句话,像我这种菜鸟程序员必然是看不懂的。我只知道,在 new Vue()
时传入的 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: (...)
。ide
接下来咱们尝试在留空处打印出 data,即在定义完 data 以后当即将其打印。
可是很不幸,打印出来依然是上面这个不对劲的值。
但是很明显,当咱们不去 new Vue()
,而且传入 data 的时候,data 的打印结果绝对不是这样。
因此咱们能够尝试利用 setTimeout()
将 new Vue()
延迟 3 秒执行。
这个时候咱们就会惊讶的发现:
这说明就是 new Vue()
的过程当中,Vue 偷偷的对 data 进行了修改!正是这个修改,让 data 的数据,变成了响应式数据。
(...)
的由来为何好好的一个 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。
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 所拦截。
咱们刚刚写的代码是这样的:
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 也存在着各类偷偷的处理。
若是咱们的数据是这样:
data: { obj: { a: 'a' } }
咱们在 Vue 的 template 里却写了
{{ obj.b }}
会怎样?
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 又偷偷的干了一件事儿,它把你本来的数组方法给改了一些。
在 Vue 中的数组所带的这七个方法都不是原生的方法了。Vue 考虑到这些操做极为经常使用,所在中间为咱们添加了监听。
讲到这里,相信你们对 Vue 的响应式原理应该有了更深的认识了。
(完)