2、Vue响应式的完善与完整过程的分析

326 阅读1分钟

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流程

trigger流程.jpg

targetMap的结构:

targetMap.png

depsMap的结构:

depsMap.jpg

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,然后清空他呢?

1681215046096.jpg

分析上面完整的流程, 其中与deps 有关的,就是在红色框内圈出的内容。
track是把deps收集起来,trigger的时候获取 deps。
如果在track的时候,把 deps 指向一个全局存储的空间,那么在 run 的时候就可以使用起来。
全局中有activeEffect 变量。
所以稍微对 ReactiveEffecttrack 进行改造 。

//  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>