本帖最后由 大蓝鲸小蟀锅 于 2020-2-28 17:42 编辑
vue2.x和vue3.0中的响应式机制对比 首先,官网已明确指出追踪数据变化的机制,如果官网没有看过超过三遍的童鞋请再次查看一下: 如何追踪变化先明确一点:vue2.x中,让数据具有“响应式变化”,即:“数据改变影响视图,视图改变影响数据”,使用的是Object.defineProperty. 这是一个es5提供的api,这也是Vue不支持 IE8 以及更低浏览器的原因。
该api中有三个参数:Object.defineProperty(obj, propKey, attributes) 1、obj表示要在其上定义属性的对象,通常是一个空对象 其中描述符实现的是一个叫PropertyDescriptor接口,具体参数有如下: interface PropertyDescriptor { - 其中configurable,属性单词比较长,可以这样记忆。这个单词名词形式是 config 表示配置,后面加了able结尾是形容词,表示可配置。在该情境下表示对该对象添加或修改的属性是否可配置。默认是false不可配置。依次类推:enumerable表是否可枚举、writable表是否可写
如下代码:configurable为false时,尽管使用了 delete 命令删除对象obj下面的name属性,还是能输出内容:'heima'
[JavaScript] 纯文本查看 复制代码 let obj = {}[/align][align=left] Object.defineProperty(obj, 'name', {
value: 'heima',
configurable: false
})
delete obj.name
console.log(obj.name) //此处输出内容: heima
请看如下代码,将configurable换成true时,是如何表现的。 [AppleScript] 纯文本查看 复制代码 let obj = {}
Object.defineProperty(obj, 'name', {
value: 'heima',
configurable: true
})
delete obj.name
console.log(obj.name) //此处输出内容: undefined 此时同样使用delete命令,再去查看name属性时,已经是undefined了。表示configurable属性为true时表示该对象的属性能被配置(如删除属性操作)。
如下代码:enumerable为false时,使用 Object.keys 获取该对象obj下面的所有key的集合,发现是一个空数组 [JavaScript] 纯文本查看 复制代码 let obj = {}
Object.defineProperty(obj, 'name', {
value: 'heima',
enumerable: false
})
console.log(Object.keys(obj)) //此处输出内容: [] 请看如下代码,将enumerable换成true时,是如何表现的。 [JavaScript] 纯文本查看 复制代码 let obj = {}
Object.defineProperty(obj, 'name', {
value: 'heima',
enumerable: true
})
console.log(Object.keys(obj)) //此处输出内容: [ 'name' ] 同样使用Object.keys 获取该对象obj下面的所有key的集合,发现是有name属性的。表示enumerable属性为true时,name属性可被枚举出来
如下代码:writeable为false时,查看obj,发现仍然是一个空对象
[JavaScript] 纯文本查看 复制代码 let obj = {}
Object.defineProperty(obj, 'name', {
value: 'heima',
writable: false
})
obj.name = '传智播客'
console.log(obj) //此处输出内容: {}
请看如下代码,将writeable换成true时,是如何表现的。
[JavaScript] 纯文本查看 复制代码 let obj = {}
Object.defineProperty(obj, 'name', {
value: 'heima',
writable: true
})
obj.name = '传智播客'
console.log(obj.name) //此处输出内容: 传智播客
可以看出当writable属性为true时,可以对obj下面的name属性做重新赋值操作。
还剩下两个方法:get和set
get方法表示当获取该属性时,会调用的方法
set方法表示当给该属性重新赋值时,会调用的方法并且会有一个形参,表示用户给的新值
下面给出一份示例代码:使用Object.defineProperty 劫持一个普通对象
[JavaScript] 纯文本查看 复制代码 function isObject(target) {
return Object.prototype.toString.call(target).slice(8, -1).toLowerCase() === 'object'
}
function observe(target) {
if (isObject(target)) {
for (let key in target) {
defineProerties(target, key, target[key])
}
}
}
function defineProerties(obj, key, value) {
observe(value)
Object.defineProperty(obj, key, {
get() {
return value
},
set: (newVal) => {
if (newVal !== value) {
observe(newVal)
trigger()
value = newVal
}
}
})
}
function trigger() {
console.log('数据触发更新了')
}
其中:
isObject方法是确保所要劫持的参数是一个对象
observe方法是劫持对象,对对象内的key全部加上get set方法
defineProperties方法是使用Object.defineProperty对各个key添加getter和setter
trigger方法是用来查看给属性重新赋值时给出的提示信息
执行如下代码时:可以看到有触发更新的日志打出。说明给该对象下的普通属性进行重新赋值时可以进行拦截
[JavaScript] 纯文本查看 复制代码
let obj = {
school: '传智播客',
info: {
address: '三鸿路'
}
}
// 进行数据劫持
observe(obj)
// 给school重新赋值成 '黑马'
obj.school = '黑马'
console.log(obj)
执行如下代码时:可以看到有触发更新的日志打出。说明给该对象下的复杂属性(也是一个对象类型)进行重新赋值时可以也可以进行拦截
[JavaScript] 纯文本查看 复制代码 let obj = {
school: '传智播客',
info: {
address: '三鸿路'
}
}
// 进行数据劫持
observe(obj)
// 给info下面的address属性重新赋值成 '四鸿路'
obj.info.address = '四鸿路'
console.log(obj.info)
执行如下代码时:可以看到有触发更新的日志打出。说明给该对象下的复杂属性进行重新赋值另外一个对象时可以也可以进行拦截
[JavaScript] 纯文本查看 复制代码 let obj = {
school: '传智播客',
info: {
address: '三鸿路'
}
}
// 进行数据劫持
observe(obj)
// 给obj的info属性重新赋值一个对象时
obj.info = {a: 1}
console.log(obj.info)
同样,劫持数组也是可以的。[JavaScript] 纯文本查看 复制代码 let arr = [1, 2, 3]
arr.forEach((item, index) => defineProerties(arr, index, item))
arr[0] = 100
但是,官网上明确提到不允许通过改变数组索引的形式来更改数组。官网解释是性能问题,这里有一份比较详细的解释Vue为什么不能检测数组变动
以上三种类型代码可以直接置于浏览器的 Console 面板中执行,并且展开属性时能够发现具有get和set方法,表示已经被劫持。当然,仅仅具有劫持是不够的。
(这里只是数据劫持过程,模板编译阶段不是本节重点,后续会单单给出一节解释编译过程)
二、vue3.0
可以自己到github官网上搜索:vue-next,把代码down到本地后,执行npm run dev可以生成一个vue/dist/vue.global.js文件,这便是打包后的最新源码。
其中官网上已经明确说明,vue3的数据变更检测的机制使用的是proxy。
下面简单认识一下proxy:这是es6给出的api,用于修改某些操作的默认行为。再明确一点,就是在真正访问某个对象之前架设了一层“拦截”。
可以直接通过new的形式创建一个proxy实例,其中第一个参数表示你所要拦截的对象;第二个参数是一个对象,用来定制拦截行为,拦截行为里可以添加get,set,deleteProperty方法,分别表示获取属性时、给属性重新设置时、删除某个属性时会被“拦截一次”
如下代码,声明了一个obj对象,里面有一个name属性,属性值为字符串类型 'heima'
[JavaScript] 纯文本查看 复制代码 let obj = { name: 'heima' }
let proxy = new Proxy(obj, {
get() {
console.log('--get--')
return obj.name
}
})
console.log(proxy.name) // --get-- care
从输出结果可以看出,当通过proxy这个实例获取name属性时,确实执行了拦截行为里面的get方法
再看如下代码:
[JavaScript] 纯文本查看 复制代码
let toProxy = new WeakMap()
let toRaw = new WeakMap()
function trigger() {
console.log('数据更新了')
}
function isObject(target) {
return typeof target === 'object' && target !== null
}
function observer(target) {
if (!isObject(target)) {
return target
} else {
const proxy = toProxy.get(target)
if (proxy) {
return proxy
}
if (toRaw.has(target)) {
return target
}
const handler = {
set(target, key, value, receiver) {
// 过滤私有属性 length
if (target.hasOwnProperty(key)) {
trigger()
}
return Reflect.set(target, key, value, receiver)
},
get(target, key, receiver) {
if (isObject(target[key])) {
return observer(target[key])
}
return Reflect.get(target, key, receiver)
},
deleteProperty(target, key, receiver) {
return Reflect.deleteProperty(target, key, receiver)
}
}
let observed = new Proxy(target, handler)
console.log('proxy')
toProxy.set(target, observed) // 源对象和代理过后的对象做一个hash表
toRaw.set(observed, target) // 已经代理过后的对象和原对象做一个hash表
return observed
}
}
其中:
toProxy和toRow都是一个WeakMap的实例(好处是既可以在该对象上添加数据,又不干扰垃圾回收机制,可防止内存泄漏。命令参考源码,前者表示已经被代理的对象,后面表示已经被代理过后的源对象)
trigger方法用于输出数据更新的日志
isObject方法是用于判断所传入的参数是否是一个对象(和上面的xxx.call的方式一样,只不过这里的isObject是vue源码的推荐的写法)
observe方法里主要是使用proxy的形式添加get,set,deleteProperty三种方法的拦截
并且里面用到了Reflect,这个是es6推荐写法。因为某些场景下用Reflect会更合理些。如果没有用过的童鞋可以去先看一下 es6中的Reflect用法简介
执行以下代码:
[JavaScript] 纯文本查看 复制代码 let obj = {
name: 'heima',
a: [1, 2, 3]
}
let proxy = observer(obj)
proxy.name = '传智播客'
console.log(proxy) // proxy 数据更新了 { name: '传智播客', a: [ 1, 2, 3] }
观察输出内容,即可看出拦截行为里的get方法确实被拦截了,拦截的同时,我们把观察后的对象缓存到map集合中。方便下次使用不用重复再存。
再看如下代码:
[JavaScript] 纯文本查看 复制代码 let arr = [1, 2, 3]
let proxy = observer(arr)
proxy.push(4)
console.log(proxy) // proxy 数据更新了 [ 1, 2, 3, 4]
观察输出内容,get方法也被拦截,并且根据上述代码的过滤私有属性length操作,可以看出有一次数据更新日志打出(如果不过滤的话会有多次日志打出,具体可以执行代码,一见分晓)
再看如下代码:
[JavaScript] 纯文本查看 复制代码
let obj = {
name: 'heima',
a: [1, 2, 3]
}
let proxy = observer(obj)
proxy = observer(proxy)
proxy = observer(proxy)
proxy = observer(proxy)
console.log(proxy) // proxy { name: 'heima', a: [1, 2, 3] }
观察输出内容:对已观察的对象进行多次拦截时,也仅仅输出一次更新日志
再看如下代码:
[JavaScript] 纯文本查看 复制代码 let obj = {
name: 'heima',
a: [1, 2, 3]
}
let p1 = observer(obj)
p2 = observer(p1)
p3 = observer(p2)
p4 = observer(p3)
console.log(p4) // proxy { name: 'heima', a: [ 1, 2, 3] }
观察输出内容:对被观察后的观察对象,进行多次拦截已被观察后的对象是仅仅也是输出一次更新日志
再看如下代码:
[JavaScript] 纯文本查看 复制代码 let obj = {
name: 'heima',
a: [1, 2, 3]
}
let proxy = observer(obj)
proxy.a.push(4)
console.log(proxy) // proxy proxy 数据更新了 { name: 'heima', a: [ 1, 2, 3, 4] }
观察输出内容:有两次proxy输出的原因是proxy先取了a属性,再进行的push操作。这里可以轻松对数组进行拦截。很显然这是Object.defineProperty所不及之处。
好啦,以上便是两种数据检测方式的不同之处。总体来说是有proxy更具有全面性。当然,像 IE 一类,vue已经不再持观望态度了。
|