响应式原理
# 关于 Vue.js
Vue.js 是一款 MVVM 框架,上手快速简单易用,通过响应式在修改数据的时候更新视图。Vue.js 的响应式原理依赖于Object.defineProperty (opens new window),尤大大在Vue.js 文档 (opens new window)中就已经提到过,这也是 Vue.js 不支持 IE8 以及更低版本浏览器的原因。Vue 通过设定对象属性的 setter/getter 方法来监听数据的变化,通过 getter 进行依赖收集,而每个 setter 方法就是一个观察者,在数据变更的时候通知订阅者更新视图。
# 将数据 data 变成可观察(observable)的
那么 Vue 是如何将所有 data 下面的所有属性变成可观察的(observable)呢?
function observe(value, cb) {
Object.keys(value).forEach((key) =>
defineReactive(value, key, value[key], cb)
);
}
function defineReactive(obj, key, val, cb) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
/*....依赖收集等....*/
/*Github:https://github.com/answershuto*/
return val;
},
set: (newVal) => {
val = newVal;
cb(); /*订阅者收到消息的回调*/
},
});
}
class Vue {
constructor(options) {
this._data = options.data;
observe(this._data, options.render);
}
}
let app = new Vue({
el: "#app",
data: {
text: "text",
text2: "text2",
},
render() {
console.log("render");
},
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
为了便于理解,首先考虑一种最简单的情况,不考虑数组等情况,代码如上所示。在initData (opens new window)中会调用observe (opens new window)这个函数将 Vue 的数据设置成 observable 的。当_data 数据发生改变的时候就会触发 set,对订阅者进行回调(在这里是 render)。
那么问题来了,需要对 app._data.text 操作才会触发 set。为了偷懒,我们需要一种方便的方法通过 app.text 直接设置就能触发 set 对视图进行重绘。那么就需要用到代理。
# 代理
我们可以在 Vue 的构造函数 constructor 中为 data 执行一个代理proxy (opens new window)。这样我们就把 data 上面的属性代理到了 vm 实例上。
_proxy.call(this, options.data); /*构造函数中*/
/*代理*/
function _proxy(data) {
const that = this;
Object.keys(data).forEach((key) => {
Object.defineProperty(that, key, {
configurable: true,
enumerable: true,
get: function proxyGetter() {
return that._data[key];
},
set: function proxySetter(val) {
that._data[key] = val;
},
});
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
我们就可以用 app.text 代替 app._data.text 了。
# Vue2 和 Vue3 响应式的区别
vue2 中使用 Object.defineProperty(target,key,handle) 通过循环遍历对象属性的方式来进行监听 Object.defineProperty 不能监听对象新增属性,Proxy 可以 Object.defineProperty 不能监听对象删除属性,Proxy 可以 Object.defineProperty 不能监听数组下标的变化, 因为数组的 push、pop、shift、unshift、splice、sort,reverse 方法是无法触发 set 的,所以需要重写这几个方法(其实可以监控到数组,考虑到性能问题,最后重写了数组的方法)
vue3 响应式对数组的处理 vue3 使用 Proxy 可以代理整个对象 Proxy 可以监听数组下标的变化 当代理过后的数组想要使用 indexOf lastIndexOf includes 查找原数组中的对象时,这时候因为代理过后的数组和原数组不一样,所以这几个方法都会返回 false,故需要重写这 3 个方法,查找时先在代理过后的数组上去查找,找不到再去原数组上找
隐式修改数组长度的方法 push、pop、shift、unshift、splice 也重写了,有一个场景
const obj = {};
const reactObj = Ref(obj);
watchEffect(() => {
reactObj.push(1);
});
watchEffect(() => {
reactObj.push(2);
});
2
3
4
5
6
7
8
9
10
11
上面这种场景,第一个 effect 往数组添加数据,会获取数组的长度,并且和自己绑定关系,然后去修改数组长度,这时候在依赖 trigger 函数中会去通知所有跟 length 相关的 effect 全部执行一遍
因此在第二个 effect 还没执行完的时候,就要执行第一个 effect,第二个 effect 执行完之后又会修改 length,再去执行第一个 effect,造成死循环,导致栈溢出,
问题的原因就是 push 操作会读取 length,进而绑定联系,为了解决这问题,就需要关闭这种绑定联系
执行这些方法之前,先要停止依赖收集和停止副作用,待方法执行完之后,再恢复依赖收集和副作用