阅读之前附上之前写的Vue源码系列的几篇文章:
三个函数,让你搞懂Vue3.0的核心响应式原理
响应式原理对比
在面试中,可能会问你Vue1.0~Vue3.0三个版本的响应式原理是什么?你会说:
- Vue1.0、Vue2.0都是基于
Object.defineProperty()
实现的。 - Vue3.0是基于
Proxy
实现的。 以上回答都没有任何问题,核心原理就是Object.defineProperty()
、Proxy
这俩个Api。
Vue2.0
我们都知道Vue2.0核心响应式原理是通过Object.defineProperty()
实现的,所以我们现在看一下Object.defineProperty()
的用法。
Object.defineProperty(obj,key,attributes)
- obj:想要处理的对象
- key:属性名称
- attributes:属性描述符(get、set、configurable、enumerable、value、writable)
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`读取属性:${key}`);
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal
console.log(`修改属性:${key} —————— 修改后的值为:${val}`);
}
}
})
}
const data = {
bar: "bar",
foo: "foo"
}
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key]);
})
data.bar
data.bar = "newBar";
data.foo
data.foo = "newFoo";
控制台输出:
上面就是Object.defineProperty()
的基本用法。那为什么Vue3.0会改用Proxy
呢?接着看。
// 继续以上代码
// 新增属性name
data["name"] = "张三";
我们调试发现控制台跟上面代码输出一样,并没有检测到对象的新增了一个name
属性。
由此可见:
- 弃用原因:
Object.defineProperty()
在Vue中,无法检测到对象的新增操作,我们想要给对象新增响应式属性,必须使用Vue.$set()
。Object.defineProperty()
在Vue中,会完全遍历data中的所有属性。而Proxy
是一个懒加载的过程,只有被访问到才会做响应式处理,这也就是Vue3.0快的原因。
Vue3.0
从上面我们知道了Object.defineProperty()
的弊端,接下来我们看一下Proxy
的用法。
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
console.log(`读取属性:${key}`);
return target[key];
},
set(target, key, value) {
if (target[key] !== value) {
target[key] = value;
console.log(`设置属性:${key} —————— 设置后的值为:${target[key]}`);
}
},
deleteProperty(target, key, value) {
delete target[key];
console.log(`删除属性:${key} —————— 删除后的值为:${target}`);
},
})
}
const data = {
bar: "bar",
foo: "foo"
}
const instance = reactive(data);
// 正常访问就不测试了,我们直接新增属性
instance["name"] = "张三";
控制台输出:
从浏览器输出可以看出
Proxy
,可以直接检测到对象新增属性的操作。在Vue3.0中我们新增对象属性的时候,再也不用Vue.$set()
了。
Vue3.0响应式原理实现
先来看一个例子,这个例子跟我之前实现Vue1.0的时候一样
<div class="container">
</div>
<script>
const data = {
foo: "foo"
}
function update() {
document.querySelector(".container").innerHTML = data.foo;
}
update();
</script>
代码很简单,如果想让我们页面的内容跟着foo
发生改变,要怎么做?
// 继续以上代码
data.foo = "newFoo";
update();
是不是只要data.foo
发生改变的时候,调用一下update
函数就行。
这里顺便提一点题外话,
Vue1.0、Vue2.0
和Vue3.0
存储update
函数的位置不同:
- Vue1.0和Vue2.0是:将
update
存储在当前key
的闭包环境中,更新时,准确找到当前key的闭包环境,dep.notify()
进行更新。
- Vue3.0是:将
update
函数存储在一个叫WeakMap
对象中,更新时,因为Proxy
会给handler
传入target、key
,所以就可以从WeakMap
中查找到对应的cb(更新函数)
,从而完成更新。
思考
从上面的例子可以看出,当我们试图改变对象中某个属性的值,也就是data.foo = xxx
时,调用update
函数就可以完成更新。
既然如此,对于对象来说那是不是只要我们在:
getter
时:依赖收集(收集update函数)setter
时:调用收集的函数触发更新 就可以完成我们的响应式更新,而这也是Vue3.0的核心思想。
根据上面的想法,我画了一张图:
三句话描述一张图(这里的cb,就是update函数):
-
effect
将cb
函数存入effectStack
,并且调用cb
函数,因为cb
中读取了对象的某个key
,触发当前key
的getter
,执行完之后清空effectStack
。(注意:清空effectStack
之前,先执行track
) -
track
在getter
触发时调用,并且可以拿到target,key
,并且联合effectStack
中存储的cb
函数组成类似于{target:{key:[cb]}}
存入targetMap
中。(这一步不理解没关系,具体看下面实现) -
trigger
在setter
触发时调用,并且可以拿到target,key
,会从targetMap
找到当前target.key
的中cb
函数并执行,完成更新。
effect
作为响应式处理的起点,这个函数主要处理以下几件事:
- 执行传入的
cb
函数。 - 处理
cb
函数执行异常的情况 - 将函数临时存储到
effectStack
中(这一步的用途在track(依赖收集)
的时候会看到)
<div class="container">
</div>
<script>
const effectStack = [];
function effect(cb) {
const e = createEffectReactive(cb);
e();
}
function createEffectReactive(cb) {
// 对cb函数异常处理,并返回cb函数的执行结果
const effectFn = function () {
try {
effectStack.push(effectFn);
cb();
} finally {
effectStack.pop();
}
}
return effectFn;
}
const data = {
foo: "foo"
}
function update() {
document.querySelector(".container").innerHTML = data.foo;
}
effect(update);
</script>
现在页面正常显示:
但是如果我们想实现data.foo = xxx
页面就变化的话,是不是只要我们在set
时,执行对应函数就可以,所以接下来我们要做的就是:拦截对象属性的操作。
Proxy
主要用来拦截对象的属性操作,作为Vue3.0响应式的核心成员之一,它的实现也很简单:
Proxy (obj,handler)
- obj:想要操作的对象
- handler:
ProxyHandler
类型(get、set、deleteProperty、...)
function defineReactive(obj) {
return new Proxy(obj, {
get(target, key) {
return target[key];
},
set(target, key, value) {
const res = target[key];
if (res !== value) {
target[key] = value;
}
},
defineProperty(target, key) {
delete target[key];
}
})
}
处理data中的属性
const data = defineReactive({
foo: "foo"
})
在setter
时调用更新函数
<div class="container">
</div>
<script>
// Proxy
function defineReactive(obj) {
return new Proxy(obj, {
...,
set(target, key, value) {
const res = target[key];
if (res !== value) {
target[key] = value;
update();
}
}
})
}
</script>
测试一下代码,页面会在2秒之后,变成newFoo
。
如此,我们已经实现了我们想要的效果。
但是应用中不可能只有一个对象,也不可能一个对象只有一个key,我们需要将cb函数分别存储到对应的key中,这个过程简称依赖收集。
什么是依赖收集?
- 当你触发某个
key
的getter
时,会调用track
,track
会将存储在effectStack
里面的cb函数联合target、key
,组成类似于{target:{key:[cb]}}
数据,把它存储到targetMap
中,到时候trigger
的时候,就可以根据target、key
,找到对应的cb
函数,执行更新。
trigger和track
两句话描述这两个函数:
getter
时调用track
进行依赖收集setter
时调用trigger
通知更新
// 依赖收集
function track(target, key) {
const effect = effectStack[effectStack.length - 1]; // 获取更新函数
const depMap = targetMap.get(target);
if (!depMap) {
depMap = new Map();
targetMap.set(target, depMap);
}
const deps = depMap.get(key);
if (!deps) {
deps = new Set();
depMap.set(key, deps);
}
deps.add(effect); // 将更新函数存储到当前key对应的数组中
}
// 通知更新
function trigger(target, key) {
const depMap = targetMap.get(target);
if (depMap) {
const deps = depMap.get(key);
if (deps) {
deps.forEach(dep => {
dep();
})
}
}
}
完整代码
<div class="container">
</div>
<script>
// effect
const effectStack = [];
function effect(cb) {
const e = createEffectReactive(cb);
e();
}
function createEffectReactive(cb) {
function effect() {
try {
effectStack.push(effect);
cb();
} finally {
effectStack.pop();
}
}
return effect;
}
// Proxy
function defineReactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key);
return target[key];
},
set(target, key, value) {
const res = target[key];
if (res !== value) {
target[key] = value;
trigger(target, key);
}
},
defineProperty(target, key) {
delete target[key];
trigger(target, key);
}
})
}
const targetMap = new WeakMap();
// track(依赖收集)
function track(target, key) {
const effect = effectStack[effectStack.length - 1];
let depMap = targetMap.get(target);
if (!depMap) {
depMap = new Map();
targetMap.set(target, depMap);
}
let deps = depMap.get(key);
if (!deps) {
deps = new Set();
depMap.set(key, deps);
}
deps.add(effect);
}
// trigger(通知更新)
function trigger(target, key) {
const depMap = targetMap.get(target);
if (depMap) {
const deps = depMap.get(key);
if (deps) {
deps.forEach(dep => {
dep();
})
}
}
}
const data = defineReactive({
foo: "foo"
})
function update() {
document.querySelector(".container").innerHTML = data.foo;
}
effect(update);
setTimeout(() => {
console.log("触发");
data.foo = "newFoo";
}, 2000)
</script>
实现ref
基础类型做响应式一般用ref
,读取方式为.value
,所以它的实现方式也很简单:
function ref(value) {
return defineReactive({
value
})
}
实现computed
function computed(fn) {
const res = ref();
effect(() => res.value = fn());
return res;
}
结尾
文章有总结不到位的地方,我后期会持续修改。