下面这段代码很是简单,编写过Vue的同窗都能看懂它在干什么,可是你能准确的说出这段代码在第一秒,第二秒,第三秒页面上分别有什么变化吗?javascript
<!DOCTYPE html> <html> <script src="https://unpkg.com/vue/dist/vue.js"></script> <body> <div id="app"> <div>{{ list }}</div> </div> <script> new Vue({ el: '#app', data: { list: [], }, mounted() { setTimeout(()=>{ this.list[0] = 3 }, 1000) setTimeout(()=>{ this.list.length = 5 }, 2000) setTimeout(()=>{ this.$set(this.list, this.list) }, 3000) } }) </script> </body> </html>复制代码
你们最好能动手拷贝上面的代码,本地新建HTML文件保存后打开调试查看,我这里直接说一下结果。当执行这段代码后,页面在第一秒和第二秒无变化,直到第三秒时候才会发生变化,思考一下第一秒和第二秒改变了list的值,为何Vue的双向绑定在这里失效了呢?围绕这个问题下面开始一步一步看看Vue的数据变化监听实现机制。
html
这里由浅入深的去看,先从要监听普通数据类型看起。前端
监听普通数据类型,即要监听的对象属性的值为非对象的五种基本类型变化,这里不直接看源码,每一步都本身手动的去实现,更加便于理解。vue
<!DOCTYPE html> <html> <div> name: <input id="name" /> </div> </html> <script> // 监听Model下的name属性,当name属性有变化时要引发页面id=name的响应变化 const model = { name: 'vue', }; // 利用Object.defineProperty建立一个监听器 function observe(obj) { let val = obj.name; Object.defineProperty(obj, 'name', { get() { return val; }, set(newVal) { // 当有新值设置时,执行setter console.log(`name变化:从${val}到${newVal}`); // 解析到页面 compile(newVal); val = newVal; } }) } // 解析器,将变化的数据响应到页面上 function compile(val) { document.querySelector('#name').value = val; } // 调用监听器,对model开始监听 observe(model); </script>复制代码
在控制台调试过程。复制代码
上面的代码在调试的时候,我先查看了model.name初始值后,进行了从新设置,能够引发setter函数的触发执行,从而页面达到响应式效果。java
可是当给name属性赋值为对象类型后,再给新对象里插入key1一个属性后,接着改变这个key1的值,这时候页面并不能获得响应式触发。node
因此上面的observe的实现中,当name是普通数据类型的时候监听没有问题,而要监听的内容是对象的变化里的时候,上面的写法就有问题了。git
下面看看监听对象类型属性observe函数要怎么实现。github
从上面的例子里,检测属性值为对象时,不能知足监听需求,接下来进一步改造observe监听函数,解决思路很简单,若是是对象,只需再一次将当前对象下的全部普通类型的监听变化便可,若是该对象下还有对象属性,继续监听就能够了,若是你对递归很熟,立刻就知道该如何解决这个问题。面试
<!DOCTYPE html> <html> <div> name: <input id="name" /> val: <input id="val" /> list: <input id="list" /> </div> </html> <script> // 监听Model下的name属性,当name属性有变化时要引发页面id=name的响应变化 const model = { name: 'vue', data: { val: 1 }, list: [1] }; // 监听函数 function observe(obj) { // 遍历全部属性,各自监听 Object.keys(obj).map(key => { // 将object属性特殊处理 if (typeof obj[key] === 'object') { // 是对象属性的再次监听 observe(obj[key]); } else { // 非对象属性的作监听 defineReactive(obj, key, obj[key]); } }) } // 利用Object.defineProperty作对象属性的作监听 function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get() { return val; }, set(newVal) { // 当有新值设置时,执行setter console.log(`${key}变化:从${val}到${newVal}`); if (Array.isArray(obj)) { document.querySelector(`#list`).value = newVal; } else { document.querySelector(`#${key}`).value = newVal; } val = newVal; // 新增的属性再次进行监听 observe(newVal); } }) } // 监听model下的全部属性 observe(model); </script>复制代码
在控制台调试过程。复制代码
在上面的实际操做中,我先改变了属性name的值,触发了setter,页面收到响应,再次改变了model.data这个对象下的val属性,页面也获得响应式变化,这说明咱们在以前是想observe监听不到对象属性变化的问题在上面的改造下获得了解决。数组
接下来要注意,在最后我改变了数组属性list下的第一个下标里的值为5,页面也获得了监听结果,可是我改变了第二个下标后,没有触发setter,接着特地去改变list的length,或者push都没有触发数组的setter,页面没有变化响应。
这里抛出两个问题:
a、我修改了数组list的第二个下标的值,而且调用length、push改变数组list后页面也没有响应到变化,是怎么回事?
b、回到文章开始示例的那一段Vue代码里的实现,我改变了Vue的data下list的下标属性值,页面是没有响应变化的,可是这里我改了list的内的值从1到5,页面响应了,这又是怎么回事?
请带着a、b两个问题继续往下看。
这里分析一下a问题修改数组下标的值和调用length、push方法改变数组时不触发监听器的setter函数的缘由。我以前看到不少文章写Object.defineProperty不能监听到数组内的值变化,真的是这样么?
请看下面的例子,这里不绑定页面,只观察Object.defineProperty监听的数组元素,是否能监听到变化。
从上面代码里,首先监听了model数组里全部的属性,而后经过各类数组的方法来修改当前数组,得出如下几个结论。
一、直接修改数组中已有的元素是能够被监听的。
二、数组的操做方法若是是操做已经存在的被监听的元素也是能够触发setter被监听的。
三、只有push、length、pop一些特殊的方法确实不能触发setter,这跟方法的内部实现与Object.defineProperty的setter钩子的触发实现有关系,是语言层面的缘由。
四、改变超过数组长度的下标的值时,值变化是不能监听到的。这个其实很好理解,不存在的属性固然是不能监听到,由于绑定监听操做在以前已经执行过了,后添加的元素属性在绑定当时都尚未存在,固然没有办法提早去监听它了。
因此综上,Object.defineProperty不能监听到数组内的值变化的说法是错误的,同时也得出了a问题的答案,语言层面不支持用Object.defineProperty监听不存在的数组元素,而且经过一些能形成数组的方法形成数组改变也不能监听到。
对于b问题,则须要去看看Vue的源码里,为什么Object.defineProperty明明能监听到数组值的变化,而它却没有实现呢?
这里分享一下我看源码的技巧,若是直接打开github一行一行看看源码是很懵逼的,我这里是直接用Vue-cli在本地生成一个Vue项目,而后在安装的node_modules下的Vue包里进行断点查看的,你们能够尝试下。
测试代码很简单,以下;
import Vue from './node_modules/_vue@2.6.11@vue/dist/vue.runtime.common.dev'
// 实例化Vue,启动起来后直接
new Vue({
data () {
return {
list: [1, 3]
}
},
})复制代码
解释一下这一起的源码,下面的hasProto的源码是看是否有原型存在,arrayMethods是被重写的数组方法,代码流程是若是有原型,直接修改原型上的push,pop,shift,unshift,splice, sort,reverse七个方法,若是没有原型的状况下,走copyAugment去新增这七个属性后赋值这七个方法,并无监听。
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
// 监听数组元素
observe(items[i])
}
}复制代码
最后就是this.observeArray函数了,它的内部实现很是简单,它对数组元素进行了监听,什么意思呢,就是改变数组里的元素不能监听到,可是数组内的值是对象类型的,修改它依旧能获得监听响应,如改变list[0].val能够获得监听,可是改变list[0]不能,可是依旧没有对数组自己的变化进行监听。
再看看arrayMethods是如何重写数组的操做方法的。
// 记录原始Array未重写以前的API原型方法
const arrayProto = Array.prototype
// 拷贝一份上面的原型出来
const arrayMethods = Object.create(arrayProto)
// 将要重写的方法
const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]
/** * Intercept mutating methods and emit events */
methodsToPatch.forEach(function (method) {
def(arrayMethods, method, function mutator (...args) {
// 原有的数组方法调用执行
const result = arrayProto[method].apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 若是是插入的数据,将其再次监听起来
if (inserted) ob.observeArray(inserted)
// 触发订阅,像页面更新响应就在这里触发
ob.dep.notify()
return result
})
})复制代码
从上面的源码里能够完整的看到了Vue2.x中重写数组方法的思路,重写以后的数组会在每次在执行数组的原始方法以后手动触发响应页面的效果。
看完源码后,问题a也水落石出了,Vue2.x中并无实现将已存在的数组元素作监听,而是去监听形成数组变化的方法,触发这个方法的同时去调用挂载好的响应页面方法,达到页面响应式的效果。
可是也请注意并不是全部的数组方法都从新写了一遍,只有push,pop,shift,unshift,splice, sort,reverse这七个。至于为何不用Object.defineProperty去监听数组中已存在的元素变化。
做者尤雨溪的考虑是由于性能缘由,给每个数组元素绑定上监听,实际消耗很大,而受益并不大。
issue地址:https://github.com/vuejs/vue/issues/8562。
前一篇说了Vue3.0的监听采用的是ES6新的构造方法Proxy来代理原对象作变化检测,(对于Proxy不熟的同窗能够翻看上一篇内容)而Proxy做为代理的存在,当异步触发Model里的数据变化时,必须通过Proxy这一层,在这一层则能够监听数组以及各类数据类型的变化,看看下面的例子。
简直完美,不管是数组下标赋值引发变化仍是数组方法引发变化,均可以被监听到,并且既能够避开监听数组每一个属性下形成的性能问题,还能够解决像pop、push方法,length方法改变数组时监听不到数组变化的问题。
接下来使用Proxy和Reflect实现Vue3.0下的双向绑定。
<!DOCTYPE html>
<html>
<div>
name: <input id="name" />
val: <input id="val" />
list: <input id="list" />
</div>
</html>
<script>
let model = {
name: 'vue',
data: {
val: 1,
},
list: [1]
}
function isObj (obj) {
return typeof obj === 'object';
}
// 监控器
function observe(data) {
// 将属性都作监控
Object.keys(data).map(key => {
if (isObj(data[key])) {
// 对象类型的继续监听它的属性
data[key] = observe(data[key]);
}
})
return defineProxy(data);
}
// 生成Proxy代理
function defineProxy(obj) {
return new Proxy(obj, {
set(obj, key, val) {
console.log(`属性${key}变化为${val}`);
compile(obj, key, val);
return Reflect.set(...arguments);
}
})
}
// 解析器,响应页面变化
function compile(obj, id, val) {
if (Array.isArray(obj)) { // 数组变化
document.querySelector('#list').value = model.list;
} else {
document.querySelector(`#${id}`).value = val;
}
}
model= observe(model);
</script>复制代码
利用Proxy和Reflect实现以后,不用在考虑数组的操做是否触发setter,只要操做通过proxy代理层,各类操做都会被被捕获到,达到页面响应式的要求。
在Vue2.x中数组变化监听的问题,其实不是Object.definePropertype方法监听不到,而是为了性能和收益比例综合考虑之下,改变了监听方式,从本来的直接监听结果变化这种思路变换到监听会致使结果变化的方法上,也就上面所提到的对数组的重写。
而Vue3.0中利用Proxy的方式则完美解决了2.0中出现的问题,因此之后面试中若是遇到Vue中对于数组监听的处理的时候,必定要分清楚是哪个版本,本文完。