听说 Vue3 数据绑定要切换到 Proxy,为什么?

这就是这篇文章的原因,来源于某个牛逼公司的面试。我真的应该学会怎么清楚表达观点…

definePropertyProxy 使用方式和效果看起来是差不多的,但如果翻译成中文的话,一个是定义属性,一个是代理,并且 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 存在可选项:

keyvalue
configurable该属性为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
enumerable该属性为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。
value该属性对应的值。
writable该属性为 true 时,value才能被赋值运算符改变。默认为 false。
getgetter 方法
setsetter 方法

除了 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
  }
})

很明显,我可以劫持一个对象的 gettersetter,同时也很明显,貌似需要一个缓存量。

如果说和 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?

  1. defineProperty 对数组有硬伤

因为设定关系,defineProperty不能观察到数组内部,如果直接修改数组而不是返回新数组的话,无法触发劫持。Vue文档以一个简单的方式解释了这个问题,解决这个问题的方法有点骚,相关源码位置。但这只是让数组方法可以“正常使用”,万一有人arr[0] = 0呢?

  1. 一个方法只能监听一个属性

如果我需要监听这个对象里所有键,我需要把所有键都defineProperty一次。需要创建一个缓存变量倒不是什么“难事”,封装成一个方法就成了,但:

const obj = { a: { b: { c: { d: { e: '???' }}}}}  // ???
  1. Proxy 可以做到上面所有事情

上面提到,Proxy是代理了整个对象,而且是以根据 target 创建实例来进行接下来的工作,每一个都相对独立。

第一个问题,因为我们有这个“对象”的所有操作权,而且每次set都能返回新的“对象”,并且我们可以自己定义“数据如何改变”

第二个问题,因为Proxy实现的是观察到整个对象而不是对象属性,那自然不存在这个问题了。

  1. 除了 getter, setterProxy还有其他用法

比如apply,可以劫持对象的函数(我的理解是把对象可以执行,当然JavaScript中,一切都是对象)

const obj = new Proxy({}, {
  apply(target, context, args) {}
})

所以你可以执行一些东西,甚至是通过方法创建/改变得到一个对象(对 target 直接修改)

感觉,如果Vue3Proxy改写之后,代码会简洁非常多。而且性能可能会比现在提高好几倍(?),毕竟看目前得到的信息,对每一个数据创建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 性能会更差。

其中一个原因就是 VueReact 对数据的操作是不一样的。React单向,通过对比来更新数据,所以今日会有两种组件创建方式:Component, PureComponent

为什么 React 不学 Vue 搞这手,表单验证之类工作不是很好实现

大家的想法都不一样为什么要强比较?所以我的回答是:

这是 React 的设计原因,React 的做法是数据流单向,利用函数式的思想,像管道一样的操作使得副作用更加可控。

只是回答得不完美,我有面试就紧张的坏心态。我的本意是我可以清楚数据的流向,同时采用数据不可变,这样根本不用担心数据在中途突然被什么做了修改。因为前面的原因,第二个问题忘记回答了:因为用函数式的思想,那么表单验证我可以用高阶函数呀。

但并不是说Vue的做法不是高阶函数,其实也是的。只是我们在用React的时候,感觉React只提供了把代码转成视图的功能,就没了,什么都没有什么都是自己实现。而Vue已经实装了非常多的操作使得开发过程不用想多一些问题。举一个小栗子:React渲染一个列表,靠的是自己用JavaScript的方法生成一个装着ReactNode的数组,而Vue只需要在模板中标记列表数据和在哪个节点渲染,把要渲染的内容写在其中即可。