Vue响应式的完善与完整过程的分析
当前为Vue响应式的详细过程分析,并通过实现 cleanup与分支切换的方式去理解在响应式过程中发生的每一个步骤。到当前篇章为止,可熟练的掌握响应式的详细过程。后续内容为Vue api的实现
1、文件处理(可略过)
将第一篇文章中的两个方法单独放在两个不同的js文件中进行存放。
// !# reactive.js
import { track, trigger } from "./effect.js";
// 响应式对象的创建
export function createReactiveObject(raw) {
// 创建一个proxy对象
const proxy = new Proxy(raw, {
// get数据获取的时候
get(target, key) {
const res = Reflect.get(target, key);
// TODO 依赖收集
track(target, key);
return res;
},
// 数据设置的时候
set(target, key, value) {
const res = Reflect.set(target, key, value);
// TODO 依赖触发
trigger(target, key);
return res;
}
});
// 返回一个响应式对象
return proxy;
}
// !# effect.js
let activeEffect = '';
//
const targetMap = new WeakMap();
// 副作用函数,用于执行
class ReactiveEffect {
constructor(fn) {
this._fn = fn;
}
run() {
activeEffect = this;
this._fn();
}
}
// targetMap: WeakMap -> 内容为 target => depsMap
// depsMap: Map -> 内容为 key => deps
// deps: Set -> 内容为 { effect...}
export function track(target, key) {
if (!activeEffect) return;
// 从依赖收集Map中通过 target 提取 depsMap: Map - (key => deps)
let depsMap = targetMap.get(target);
if (!depsMap) {
// 如果depsMap不存在,表示首次收集,new Map 存入
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 从 depsMap中通过key获取 deps: Set - { effect... }
let deps = depsMap.get(key);
if (!deps) {
// 如果 deps不存在,则给 depsMap添加一个new Set
deps = new Set();
depsMap.set(key, deps);
}
if (!deps.has(activeEffect)) {
// 如果deps中不存在activeEffect, 则对依赖进行收集
deps.add(activeEffect)
}
}
export function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
let deps = depsMap.get(key);
deps?.forEach(effect => effect.run());
}
export function effect(fn) {
let _effect = new ReactiveEffect(fn);
_effect.run();
}
2、正文开始篇章
基于处理之后的函数,编写一个案例。 在案例中,通过手动的方式去修改 satus 和 text 的数据。
// !# index.html
<!DOCTYPE html>
<html lang="en"
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button onclick="onChangeStatus()">status</button>
<button onclick="onChangeText()">text</button>
<div id="app"></div>
<script type="module">
import { effect } from "./src/effect.js";
import { createReactiveObject } from "./src/reactive.js";
const app = document.querySelector("#app");
let obj = createReactiveObject({
text: "effect3",
status: true,
});
// render 需要渲染的内容
function render() {
app.innerHTML = `<div>${ obj.status ? obj.text : 'hello'}</div>`
}
effect(render);
window.onChangeStatus = function () {
obj.status = false
}
window.onChangeText = function () {
obj.text = "Vue"
}
</script>
</body>
</html>
首先分析上面代码的完整运行过程。
1、effect(render)调用时,把 render当做参数去创建一个ReactiveEffect实例 __effect。
2、调用创建的完实例之后调用 _effect.run( )。
3、_effect.run( )执行的时候把 当前实例赋值给全局变量 activeEffect。
4、然后调用 this.__fn( )。this.__fn就是effect中的render方法。
5、render执行时,会读取 obj.status。 读取obj.status时会触发createReactiveObject 中代理的get操作。
6、由于 obj.status 的值是 true。所以get 会执行两次。先是处理 status,然后处理 text。触发track 的操作。
触发track操作时:
1、track( target, key ) 就是先使用全局targetMap 依赖收集的容器,targetMap以代理对象为key,以Map为value。这里的Map就是后续的depsMap。
2、depsMap是一个Map类型, depsMap以 代理对象的key值作为key。 以Set为value。Set就是deps。
3、deps就是一个Set类型,其中保存的就是与代理对象obj的key值所对应的ReactiveEffect。
通过修改 obj的值时,会触发trigger的操作。 trigger会通过 targetMap ——> depsMap ——> deps的方式。去获取到deps,然后再遍历deps( deps由一个个 ReactiveEffect 组成),然后执行每一个 ReactiveEffect 的 run 方法。 run方法执行的时候,就会执行 render,然后render 的执行又会触发 track 的依赖进行重新收集。最后完成render 的执行。
3、track 与 trigger全流程
将trigger整合成流程图如下: 因为完整的trigger包含有 track 的流程:
track流程: run() ——> __fn执行 ——> render() ——> 读取代理对象 ——> 依赖收集进targetMap中 ——> render执行完毕(依赖收集完成后)
trigger流程: 修改代理对象内容 ——> 触发set ——> 从targetMap中获取依赖 ——> 遍历deps执行run ——> 触发track流程
targetMap的结构:
depsMap的结构:
deps是Set类型, 其数据结构就是 [ ReactiveEffect, ReactiveEffect, ... ]。
4、cleanup与分支切换
在targetMap中有两个依赖,status 和 text。
无论修改status 还是修改text。都会触发render进行渲染。
当status为 fasle的时候,无论text修改成什么内容。渲染的结果只会是hello。
但是由于targetMap中有 text的依赖。
所以当text修改的时候,还是会 触发render 的执行。有什么办法可以避免这种影响呢?
function render() {
app.innerHTML = `<div>${obj.status ? obj.text : 'hello'}</div>`
}
在上面代码中,
当 status 为true时,会执行 text 的读取。所以track 会执行两次。
当 status 为false时,由于 text 并不会读取。所以track 只会执行一次。
假设,默认情况下 status 为true, 进行了两次的tack。再targetMap中,存在 status 的依赖以及text 的依赖。
然后在 主动设置 status 为 false 的时候,具体流程如下代码;
// 1、set
set(target, key, value) {
const res = Reflect.set(target, key, value);
// TODO 依赖触发
trigger(target, key);
return res;
}
// 2、trigger 中的依赖触发
function trigger(target, key) {
....
...
deps?.forEach(effect => effect.run());
}
// 3、run的执行会触发 ReactiveEffect 中的run
run() {
activeEffect = this;
this._fn(); // fn 执行
}
// 4、 fn的执行会触发 render的执行
app.innerHTML = `<div>${obj.status ? obj.text : 'hello'}</div>`
// 5、 render的执行会重新触发track。 但是由于status 为false。 所以 text 不会被读取,所以text不会track
get(target, key) {
const res = Reflect.get(target, key);
// TODO 依赖收集
track(target, key);
return res;
},
也就是说 当 status 被手动更改为 fasle 的时候,会触发当前依赖的更新。
如果在run的时候( 上面3 ),把所有的依赖进行清空。
但是 4 和 5 仍然会继续进行。然后因为 三元表达式的问题。并不会触发 text 的读取。所以只会进行 status 的依赖收集。
最后由于 text 中的deps 不存在。所以在修改text 的时候 deps?.forEach(effect => effect.run()) 不会执行渲染。
从而完美的解决问题所在。
那么如何拿到deps,然后清空他呢?
分析上面完整的流程, 其中与deps 有关的,就是在红色框内圈出的内容。
track是把deps收集起来,trigger的时候获取 deps。
如果在track的时候,把 deps 指向一个全局存储的空间,那么在 run 的时候就可以使用起来。
全局中有activeEffect 变量。
所以稍微对 ReactiveEffect 和 track 进行改造 。
// track 方法
export function track(target, key) {
......
......
if (!deps.has(activeEffect)) {
deps.add(activeEffect);
// 给全局 activeEffect 添加一个deps进行存储
activeEffect.deps.push(deps);
}
}
class ReactiveEffect {
constructor(fn) {
this._fn = fn;
this.deps = []; // 添加一个存储deps 的数组,因为 ReactiveEffect与activeEffect相同
}
run() {
cleanupEffect(this);
activeEffect = this;
this._fn();
}
}
// 清空deps的方法
function cleanupEffect(effect) {
const { deps } = effect;
if (deps.length) {
deps.forEach(item => item.delete(effect))
deps.length = 0
}
}
由于trigger 中对deps 进行遍历 run 的时候,会去执行cleanupEffect 对deps进行delete,然后 fn执行时,又会对依赖进行add。
循环内进行数据删除后又添加,会导致无限循环。
想要避免无限递归的出现,那么在循环前,拷贝一份数据遍历,这样就可以避免对同一份数据进行修改。
重新修改trigger 方法。
export function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
let deps = depsMap.get(key);
triggerEffect(new Set(deps));
}
export function triggerEffect(deps) {
deps?.forEach(effect => effect.run());
}
这样分支切换功能就完成实现了。
总结
在上面的文章中,解释了完整的响应式执行流程。并通过实现cleanup去解读 track与trigger的过程,在其中添加分支切换的功能,掌握响应式流程已经问题不大。
全部代码如下:
// !#effect.js
let activeEffect = '';
//
const targetMap = new WeakMap();
// 副作用函数,用于执行
class ReactiveEffect {
constructor(fn) {
this._fn = fn;
this.deps = [];
}
run() {
console.log("ReactiveEffect run ~");
cleanupEffect(this);
activeEffect = this;
this._fn();
}
}
function cleanupEffect(effect) {
const { deps } = effect;
if (deps.length) {
console.log("cleanupEffect清空依赖");
deps.forEach(item => item.delete(effect))
deps.length = 0
}
}
// targetMap: WeakMap -> 内容为 target => depsMap
// depsMap: Map -> 内容为 key => deps
// deps: Set -> 内容为 { effect...}
export function track(target, key) {
if (!activeEffect) return;
console.log("track~");
// 从依赖收集Map中通过 target 提取 depsMap: Map - (key => deps)
let depsMap = targetMap.get(target);
if (!depsMap) {
// 如果depsMap不存在,表示首次收集,new Map 存入
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 从 depsMap中通过key获取 deps: Set - { effect... }
let deps = depsMap.get(key);
if (!deps) {
// 如果 deps不存在,则给 depsMap添加一个new Set
deps = new Set();
depsMap.set(key, deps);
}
if (!deps.has(activeEffect)) {
// 如果deps中不存在activeEffect, 则对依赖进行收集
deps.add(activeEffect);
activeEffect.deps.push(deps);
}
}
export function trigger(target, key) {
console.log("trigger~");
const depsMap = targetMap.get(target);
if (!depsMap) return;
let deps = depsMap.get(key);
triggerEffect(new Set(deps));
}
export function triggerEffect(deps) {
console.log("triggerEffect~");
deps?.forEach(effect => effect.run());
}
export function effect(fn) {
let _effect = new ReactiveEffect(fn);
_effect.run();
}
// !#reactive.js
import { track, trigger } from "./effect.js";
// 响应式对象的创建
export function createReactiveObject(raw) {
// 创建一个proxy对象
const proxy = new Proxy(raw, {
// get数据获取的时候
get(target, key) {
console.log("get的触发");
const res = Reflect.get(target, key);
// TODO
track(target, key);
return res;
},
// 数据设置的时候
set(target, key, value) {
const res = Reflect.set(target, key, value);
console.log("set的触发");
// TODO
trigger(target, key);
// effect(render);
return res;
}
});
// 返回一个响应式对象
return proxy;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button onclick="onChangeStatus()">status</button>
<button onclick="onChangeText()">text</button>
<button onclick="onChangeName()">name</button>
<div id="app"></div>
<script type="module">
import { effect } from "./src/effect.js";
import { createReactiveObject } from "./src/reactive.js";
const app = document.querySelector("#app");
let obj = createReactiveObject({
text: "effect3",
status: true,
});
// render 需要渲染的内容
function render() {
app.innerHTML = `<div>${obj.status ? obj.text : "hello"}</div>`;
renderName();
}
effect(render);
window.onChangeStatus = function () {
obj.status = false
}
window.onChangeText = function () {
obj.text = "1"
}
</script>
</body>
</html>