刚才回头看了看,发现昨天说今天要看的是vm.$nextTick & Vue.nextTick,我记错了,emm....
这篇文章主要想模拟proxy实现Vue的监控数据改变,所以我先来复习一下proxy。 想模拟proxy实现Vue的监控数据改变 先来复习一下proxy
reflect
不是说复习proxy吗!干嘛给我看reflect?当然是因为他们有关联啦。
Reflect是什么?
Reflect是一个内置的js对象(会有一些兼容性问题)它提供了一系列方法,可以让开发者通过调用这些方法来访问一些JS底层功能。由于它类似于其他语言的"反射",因此取名为Reflect。
他可以做什么?
使用Reflect可以实现属性的赋值与取值、调用普通函数、调用构造函数、判断属性是否在对象中等等功能。当然我们依然可以使用符号去编程,也可以使用这些api去编程。 他只是给我们提供了一些操作底层的方法。
这些功能不是已经存在了吗 为什么还需要用Reflect实现一次?
有一个重要的理念,就是在ES5被提出减少魔法(也就是暴露底层方法)让代码更加纯粹,这种理念很大程度上是受到函数式编程的影响。
ES6进一步贯彻了这种理念,他认为对属性内存的控制、原型链的修改、函数的调用等等这些都属于底层实现,属于一种魔法。因此要把他们提取出来,形成一个正常的API。
它里面到底提供了哪些API呢
-
Reflect.set(target, propertyKey, value): 设置对象target的属性propertyKey的值为value 等同于给对象的属性赋值
const obj = { a: 1, b: 2 } Reflect.set(obj, "a", 10) //相当于obj.a = 10 -
Reflect.get(target, propertyKey):读取对象target的属性propertyKey 等同于读取对象的属性值
Reflect.get(obj, "a") //相当于obj.a -
Reflect.apply(target, thisArgument, argumentsList):调用一个指定的函数 并绑定this和参数列表 等同于函数调用
function method(a, b){ console.log("method", a, b) } //相当于method(3,4) Reflect.apply(method, null, [3, 4]) -
Reflect.deleteProperty(target, propertyKey):删除一个对象的属性
const obj = { a: 1, b: 2 } //相当于 delete obj.a = 10 Reflect.deleteProperty(obj, "a") console.log(obj) -
Reflect.defineProperty(target,propertyKey,attributes): 类似于Object.defineProperty,不同的是如果配置出现问题,返回false而不会报错。
-
Reflect.consruct(target, argumentsList):用构造函数的方式创建一个对象。
function Test(a, b){ this.a = a this.b = b } //相当于const t = new Test(1, 3) const t = Reflect.construct(Test, [1, 3]) console.log(t) -
Reflect.has(target, propertyKey):判断一个对象是否拥有一个属性。
const obj = { a: 1, b: 2 } //相当于console.log("a" in obj) console.log(Reflect.has(obj, "a"))
proxy代理
代理是啥?
把我们的目标对象当成霸道总裁,代理就是霸道总裁的小助理。
我们不能直接接触到霸道总裁(目标对象),我们要是想和霸道总裁(目标对象)打交道 那就必须要通过小助理(代理)。我们交互的时候是和小助理进行交互,有什么事情要让小助理去找霸道总裁,霸道总裁处理事件后就把结果交给小助理,小助理再交给我们(卑微)。
那如果让代理去做我们平常就可以实现的事情是没有意义的:如果要设置属性值就给属性值,要读取属性值就读取属性值,有啥意义,我们也会啊!那我们既然使用了代理,代理一定有一些权力可以修改我们做不到的事(也就是底层实现)。比如说在修改一个属性的时候,代理要有方法,有能力去修改底层的实现。
这就要求底层实现必须做成api的形式:因为修改赋值符号或是修改new是很难实现的,但是修改一个函数就比较简单。我们只要定义一个函数再覆盖原来的函数就可以了,这也是出现reflect的原因。
使用代理
我们先来创建一个代理:
//代理一个目标对象
//target: 目标对象
//handler: 是一个普通对象
// 其中可以重写代理的底层实现 参数和reflect是一样的
// 也就是说里面放了我们之前学习的reflect的api
//最后会返回一个被代理的对象
new Proxy(target, handler)
简单的使用一下试试
const obj = {
a: 1,
b: 2
}
const proxy = new Proxy(obj, {
//代理偷懒 什么活也没干
})
proxy.a = 1
console.log(proxy.a)//1
让代理干点活!不能偷懒
const obj = {
a: 1,
b: 2
}
const proxy = new Proxy(obj, {
//赋值的时候输出一下
set(target, propertyKey, value){
console.log(target, propertyKey, value)
}
})
proxy.a = 10 //Object "a" 10
console.log(proxy.a) //输出1 因为赋值函数里面什么都没做 根本就没有赋值嘛!
干点有意义的活!
const obj = {
a: 1,
b: 2
}
const proxy = new Proxy(obj, {
set(target, propertyKey, value){
//我虽然重写了方法 但是还是想先调用底层的方法 再进行重写
Reflect.set(target, propertyKey, value)
//当然也可以写成target[propertyKey] = value
//但是这样更有点内种味道 懂吧 就是这里是底层的操作 你整一个target[propertyKey] = value看起来就不是很像在操作底层了
...//当然还可以写一些其他操作 自由发挥吧 这里就不操作了
}
})
proxy.a = 10
console.log(proxy.a)//10
不知道有没有人好奇proxy里面是什么东西,反正我挺好奇的,于是在上面的代码中输出了一下,但是也没看出个啥.. 博客JavaScript 深入理解proxy指出,new Proxy返回的实例对象proxy的原型就是目标对象obj的原型。
console.log(proxy)
/*稍微看看proxy到底是个啥东西
Proxy {a: 10, b: 2}
//我们重写的方法
[[Handler]]: Object
set: ƒ set(target, propertyKey, value)
__proto__: Object
//目标对象和它里面的属性
[[Target]]: Object
a: 10
b: 2
__proto__: Object
//未撤销
[[IsRevoked]]: false
*/
有两点需要注意:
- 创建了代理之后,后续使用这个对象要记得通过代理来使用 虽然不通过代理也不会报错。
- 其实set有第四个参数,是代理对象本身。
//重写get has属性
const obj = {
a: 1,
b: 2
}
const proxy = new Proxy(obj, {
get(target, propertyKey){
//这里是直接操作目标对象的 不是通过代理 所以不会被下面改写过的has影响
if(Reflect.has(target, propertyKey)){
return Reflect.get(target, propertyKey)
} else {
return -1
}
}
has(target, propertyKey){
return false
}
}
//相当于Reflect.get(obj, "d")
console.log(proxy.d)//-1
//相当于Reflect.has(obj, "a")
console.log("a" in proxy)//false
用proxy实现数据监听
复习完之后,我们就直接上手吧!
const data = {
name: "小饼",
blog: {
name: "快点吃饼"
}
}
//设置重写方法的对象
let handlerObj = {
set(target, propertyKey, value) {
Reflect.set(target, propertyKey, value)
render()
}
}
const proxy = new Proxy(data, handlerObj)
proxy.name = "仙女" //数据改变 输出"页面渲染了"
proxy.blog.name = "仙女" //数据改变 但是没有输出"页面渲染了"
所以说,该来的递归还是跑不掉的..
//递归 只要是对象就创建proxy
function observer(data) {
//如果当前数据是一个数组
if (Array.isArray(data)) {
//直接返回代理
return new Proxy(data, handlerObj)
}
//如果当前数据是一个对象
if (typeof data === "object") {
//遍历里面的所有属性
for (let key in data) {
//递归 如果有哪个属性的是对象就要让proxy代理之后再返回
data[key] = observer(data[key]) || data
}
//返回proxy代理之后的数据
return new Proxy(data, handlerObj)
}
}
// handlerObj没有改动 放在这里只是为了方便看
let handlerObj = {
set(target, propertyKey, value) {
Reflect.set(target, propertyKey, value)
render()
}
}
let proxy = observer(data)
proxy.name = "仙女"
proxy.blog.name = "仙女"
啊,原来proxy也要递归啊,好像也不是很厉害嘛...
然后我又测试了一下那几个defineProperty的硬伤 测试结果如下:
//所有测试数据
const data = {
name: "小饼",
blog: {
name: "快点吃饼"
},
like: ["蜡笔小新", "小葵", "野原美伢", "野原广志"]
}
//能否监听到新增对象
proxy.sex = "仙女" //输出"页面渲染了" 说明能监听到
//能否监听到数组不存在的索引的改变
proxy.like[4] = "小白" //输出"页面渲染了" 说明能监听到
//能否监听到数组长度的改变
proxy.like.length = 0 //输出"页面渲染了" 说明能监听到
//能否监听到删除对象
delete proxy.like //啥都没输出 但是确实被删除了
proxy就是牛!
为什么defineProxy为什么会有那些缺陷?
defineProperty只能监听对象中的某一个属性。
从前,有一个对象,他辛辛苦苦遍历完所有的属性,每一个属性都上一个defineProperty监听器,搞了大半天总算能管好自己的每个属性了,这时候你还要给他插进来一个属性,那他真的管不了啦。
他控诉道:"嘤嘤嘤,我好不容易搞完了,你又给我来一个,难道我还要重新遍历一次所有的属性,就为了给你这一个突然插进来的属性加一个监听器吗?呜呜呜呜,我不干了。就算你插进来我也不会给你加监听器的。"
由于新插进来的属性上没有侦听器,这个属性的变化当然就没办法被监听到了,也就不能重新渲染页面了。
而proxy之所以能监听到这些变化,都是因为proxy监听的是一整个对象而不是对象中的某一个属性。proxy是这样说的:我监听的是一整个对象!只要对象里的属性改变了就是对象被改变了!所以统统给老子刷新!
总结一下,proxy一些优点:
-
官方一点的说法就是:
Object.defineProperty只能劫持对象的属性,而Proxy是直接代理对象。
接地气一点的说法就是:
我们之前使用Object.defineProperty的时候,只能监听属性。所以我们需要对对象中的每一个属性进行遍历,然后加上监听器。
但是我们使用proxy之后,proxy管的就是整个对象,只要监听对象就可以监听到他的所有儿子属性的改动。
(但是孙子属性是监听不到滴,所以还是要递归。)
由于这个特性,我们得到了proxy的两个优点:
-
不需要遍历添加监听器
(这是我有点纠结的地方,因为到处都说是不需要遍历。但是不遍历怎么去监听对象里面的对象呢?所以我也不敢乱说话,但是有篇文章的观点是和我一样的,我贴在文章后面了。)
-
不需要使用$set方法
-
-
Proxy支持13种拦截操作,这是defineProperty所不具有的
就是reflect支持的方法他都支持了.
-
新标准性能红利
Proxy作为新标准,长远来看,JS引擎会继续优化Proxy,但getter和setter基本不会再有针对性优化。(这句话是引用的,出处放在文章最后了)
为什么vue2.x不使用Proxy呢?兼容性不好