听说 Vue3 数据绑定要切换到 Proxy,为什么?
这就是这篇文章的原因,来源于某个牛逼公司的面试。我真的应该学会怎么清楚表达观点…
defineProperty
和 Proxy
使用方式和效果看起来是差不多的,但如果翻译成中文的话,一个是定义属性,一个是代理,并且 MDN 上描述有所不同。
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。 —— MDN
Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。 —— MDN
defineProperty
这是一个 ES5
的方法。一个 defineProperty
需要三个参数,都是 require
Object.defineProperty(obj, prop, descriptor)
obj: object, 定义的对象(我理解为附着于哪个对象)
prop: 定义的对象名称(key)
descriptor: 将被定义或修改的属性描述符。
const obj = {}
Object.defineProperty(obj, 'name', {
value: 'colmugx',
});
descriptor
存在可选项:
key | value |
---|---|
configurable | 该属性为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。 |
enumerable | 该属性为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。 |
value | 该属性对应的值。 |
writable | 该属性为 true 时,value才能被赋值运算符改变。默认为 false。 |
get | getter 方法 |
set | setter 方法 |
除了 boolean
类型,值都默认为 undefined
Proxy
这是一个 ES6
的方法,Proxy
参数比较简单
new Proxy(target, handler)
target: 目标对象
handler: 属性/操作对象,当执行一个操作时定义代理的行为的函数
const obj = new Proxy({}, {
get(target, name) {
return name in target ? target[name] : target
}
})
?
数据拦截
先看看 Vue
那种数据拦截是怎样的,做一个简单实现。
const obj = {}
let value = undefined
Object.defineProperty(obj, 'key', {
get() {
return value
}
set(val) {
value = val
}
})
很明显,我可以劫持一个对象的 getter
和 setter
,同时也很明显,貌似需要一个缓存量。
如果说和 Proxy
比较呢?因为 Proxy
实现的是代理下整个对象,那么
const obj = new Proxy({key: undefined}, {
get(target, prop) {
// 由于代理了所有操作,那么返回 404 纯属意愿
return prop in target ? target[prop] : '404 Not Found'
}
set(val) {
target[prop] = val
}
})
写法有点像computed
那么,为什么defineProperty
不直接target[prop] = val
?因为劫持关系,你会看到狗咬尾巴的奇观。也就是说,defineProperty
对原对象操作就会触发劫持,而Proxy
操作的是实例对象,每个实例对象相对独立。
所以通过两次面试,闭环了这个知识。另外一篇:vue watch存在永动吗?
然而Vue
换方式,显然不止因为这个,或许连这个都谈不上。
只需要 defineProperty?怎么可能?
首先,上面例子这种方式可以看出,我每次操作都只能监听一个值,但一个应用不可能只有一个属性,而且没办法及时知道哪一个属性获得了更新。在深入了解之后,Vue
好像用了订阅的方式在做这些事情。
vue/src/core/observer/index.js#L109
除了简单(没有深度)数据,其他一概遍历进观察者。
为什么 Proxy?
defineProperty
对数组有硬伤
因为设定关系,defineProperty
不能观察到数组内部,如果直接修改数组而不是返回新数组的话,无法触发劫持。Vue
文档以一个简单的方式解释了这个问题,解决这个问题的方法有点骚,相关源码位置。但这只是让数组方法可以“正常使用”,万一有人arr[0] = 0
呢?
- 一个方法只能监听一个属性
如果我需要监听这个对象里所有键,我需要把所有键都defineProperty
一次。需要创建一个缓存变量倒不是什么“难事”,封装成一个方法就成了,但:
const obj = { a: { b: { c: { d: { e: '???' }}}}} // ???
Proxy
可以做到上面所有事情
上面提到,Proxy
是代理了整个对象,而且是以根据 target 创建实例来进行接下来的工作,每一个都相对独立。
第一个问题,因为我们有这个“对象”的所有操作权,而且每次set
都能返回新的“对象”,并且我们可以自己定义“数据如何改变”
第二个问题,因为Proxy
实现的是观察到整个对象而不是对象属性,那自然不存在这个问题了。
- 除了
getter
,setter
,Proxy
还有其他用法
比如apply
,可以劫持对象的函数(我的理解是把对象可以执行,当然JavaScript中,一切都是对象)
const obj = new Proxy({}, {
apply(target, context, args) {}
})
所以你可以执行一些东西,甚至是通过方法创建/改变得到一个对象(对 target 直接修改)
感觉,如果Vue3
用Proxy
改写之后,代码会简洁非常多。而且性能可能会比现在提高好几倍(?),毕竟看目前得到的信息,对每一个数据创建Observer
+ defineProperty
,性能挺要命的……
补充
Proxy
在这里的用法只能说是「能当对象使用的对象」吧,毕竟它还是一个实例(Proxy(…),而defineProperty
操作的是意义上的对象。
而且我本以为我应该会先读React
的源码,没想到…
为什么 Vue2 不直接用 Proxy
Vue
最早出现于 2013 年,ES2015
规范确定于 2015年,目前找不到 Vue2
第一个 commit 是什么时候(懒),推算了一下,不应该是赶不上,所以有第二种猜想:兼容性问题。
Vue
可以支持到 IE9+,目前的兼容性是 IE10+。首先Proxy
就已经把 IE 完整的抛弃掉了,一点点都不支持。就算上 polyfill
,也仅可以使用get
, set
, apply
, construct
,而 Proxy
的钩子(陷阱)达到十几种,显然物尽其能是不可能。
时代不同了,现在是现代浏览器时代,IE 淡出,连 Edge 都投靠敌台了。话说我最喜欢的就是最后一代斯巴达,渲染很快又开始支持插件。
Vue & React
18 年我经常说的就是,如果技术参差不齐的团队,显然Vue
更合适。反正你照着说明书一行一行抄肯定不会抄出问题。所以我不喜欢 Vue 就是因为不够自由,但如果无法驾驭自由,React 性能会更差。
其中一个原因就是 Vue
跟 React
对数据的操作是不一样的。React
单向,通过对比来更新数据,所以今日会有两种组件创建方式:Component
, PureComponent
。
为什么 React 不学 Vue 搞这手,表单验证之类工作不是很好实现
大家的想法都不一样为什么要强比较?所以我的回答是:
这是 React 的设计原因,React 的做法是数据流单向,利用函数式的思想,像管道一样的操作使得副作用更加可控。
只是回答得不完美,我有面试就紧张的坏心态。我的本意是我可以清楚数据的流向,同时采用数据不可变,这样根本不用担心数据在中途突然被什么做了修改。因为前面的原因,第二个问题忘记回答了:因为用函数式的思想,那么表单验证我可以用高阶函数呀。
但并不是说Vue
的做法不是高阶函数,其实也是的。只是我们在用React
的时候,感觉React
只提供了把代码转成视图的功能,就没了,什么都没有什么都是自己实现。而Vue
已经实装了非常多的操作使得开发过程不用想多一些问题。举一个小栗子:React
渲染一个列表,靠的是自己用JavaScript
的方法生成一个装着ReactNode
的数组,而Vue
只需要在模板中标记列表数据和在哪个节点渲染,把要渲染的内容写在其中即可。