Vue 响应式原理

回顾以往的书签,原本是打算写一点 Vue 响应式原理相关的一点内容,但是 Vue 官网只是简单的描述了基本原理,并没有详细对整个过程进行阐述(毕竟相关 API MDN 上都可以查得到,也并非所有人都关心程序内部的细节),所以这算是一篇笔记。

Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用

语法: Object.defineProperty(obj, prop, descriptor)

    obj:要定义属性的对象。

    prop:要定义或修改的属性的名称或 Symbol 。

    descriptor:要定义或修改的属性描述符。

        configurable:当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。

        enumerable:当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。

        value:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。

        writable:当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。

        get:属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。

        set:属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。

如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常

以上只是对 Object.defineProperty 的介绍,今天的重点是自定义 Setters 和 Getters,这也是 Vue3 之前响应式原理的核心。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var obj = {};
var value = "";
Object.defineProperty(obj, "xx", {
get: function () {
console.log("get obj.xx");
return value;
},
set: function (val) {
console.log("set obj.xx");
value = val;
},
});

obj.xx; // => get obj.xx ""
obj.xx = 2; // => set obj.xx 2
obj.xx; // => get obj.xx 2

如果我们在自定义 Setters 中添加回调函数,就可以实现数据驱动界面了,我认为这才是 vue3 之前版本实现响应式原理的核心。

Object.defineProperties

既然都看到 Object.defineProperty, 也顺便看看 Object.defineProperties

Object.defineProperties() 方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。

语法: Object.defineProperty(obj, props)

看描述和语法,Object.defineProperties 和 Object.defineProperty 只是参入上的差别,区别在一一个属性和多个属性,直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var obj = {};
var a = "";
var b = "";
Object.defineProperties(obj, {
a: {
get: function () {
console.log("get obj.a");
return a;
},
set: function (val) {
console.log("set obj.a");
a = val;
},
},
b: {
get: function () {
console.log("get obj.b");
return b;
},
set: function (val) {
console.log("set obj.b");
b = val;
},
},
});

Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

语法: const p = new Proxy(target, handler)

    target: 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

    handler: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
1
2
3
4
5
6
7
8
9
10
11
12
let view = new Proxy(
{
userName: "",
},
{
set: function (obj, prop, newVal) {
console.log("set ", prop, newVal);
obj[prop] = newVal;
return true;
},
}
);

同理,如果我们在 set 方法内设置对应的回调函数,也实现了响应式更新。

对比

由于 Object.defineProperties 和 Object.defineProperty 基本可以算是相同的 api 的不同写法,此处仅对比 Object.defineProperty 和 Proxy。

监听范围

Object.defineProperty
Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。
Object.defineProperty 不能监听数组。是通过重写数据的那 7 个可以改变数据的方法来对数组进行监听的。
Object.defineProperty 也不能对 es6 新产生的 Map,Set 这些数据结构做出监听。
Object.defineProperty 也不能监听新增和删除操作,通过 Vue.set()和 Vue.delete 来实现响应式的。
Proxy
Proxy 可以直接监听整个对象而非属性。
Proxy 可以直接监听数组的变化。
Proxy 有 13 中拦截方法,如 ownKeys、deleteProperty、has 等是 Object.defineProperty 不具备的。
Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
Proxy 做为新标准将受到浏览器产商重点持续的性能优化,也就是传说中的新标准的性能红利。

兼容性

Object.defineProperty
兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平。
Proxy
不支持 IE

[越努力,越幸运!]