1、Vue的响应式原理

411 阅读5分钟

Vue的响应式原理

文章基于Vue3,内容源于自己的学习Vue3源码的笔记与总结。

Vue是一个MVVM框架。在Vue中,当数据发生变化的时候,会去驱动视图的更新。总结得出一个公式: UI = render(data) 。 其中 UI 就是就是我们需要的输出,data数据就是我们需要开发的数据与业务模型。render()就是Vue的作用。

1、全损版数据响应式

基于逻辑进行分析: 当数据发生变化的时候,会去驱动视图的更新。

  1. 首先需要有一个数据data
  2. 其次有一个effect去处理渲染的问题
  3. 最后当数据data发生修改的时候,去触发effect然后对函数进行渲染。
// data数据
let data = {
    text: "effect"
}
// effect函数,渲染的数据来自于data
function effect() {
    document.body.innerHTML = `<div>${data.text}</div>`
}
// 初始化执行
effect();

setTimeout(() => {
    data.text = "reactivity";
    // 当数据发生变化的时候,执行渲染
    effect();
}, 2000);

非常简陋的,需要手动去处理的数据响应式已经诞生了。
对于当前的数据响应式,虽然实现了,但是问题多多。
是否有办法可以直接在 data.text = "reactivity"时,自动就执行渲染呢?
可以使用Object.defineProperty。当属性发生变化的时候会触发set操作,set触发时进行渲染操作。
但是这是Vue2的使用方法。在Vue3中使用的是proxy。

2、使用Proxy去处理响应式问题

稍微对方法进行一下封装处理。(Proxy文档)

  1. createReactiveObject函数去创建一个响应式对象。
  2. 创建render函数,其中包含的是需要渲染的内容。
  3. effect副作用函数,接收一个渲染函数作为参数,然后执行该渲染函数。
  4. 当响应式对象修改的时候,去执行渲染。
// 响应式对象的创建
function createReactiveObject(raw) {
    // 创建一个proxy对象
    const proxy = new Proxy(raw, {
        // get数据获取的时候
        get(target, key) {
            const res = Reflect.get(target, key);
            // TODO
            return res;
        },
        //  数据设置的时候
        set(target, key, value) {
            const res = Reflect.set(target, key, value);
            // TODO
            effect(render);
            return res;
        }
    });
    // 返回一个响应式对象
    return proxy;
}

let obj = createReactiveObject({
    text: "effect2"
});
// 副作用函数,用于执行
function effect(fn) {
    fn();
}
// render 需要渲染的内容
function render() {
    document.body.innerHTML = `<div>${obj.text}</div>`
}

// 初始化调用
effect(render);
// 延时修改响应式对象的数据。
setTimeout(() => {
    obj.text = "reactivity2";
}, 2000);

从上面的内容可以看出。

  1. 通过createReactiveObject 方法创建了一个proxy对象。
  2. render方法中渲染绑定的是obj (proxy创建的代理对象)。
  3. effect(render); 初始化调用时,渲染内容为 "effect2" ;
  4. setTimeout时,去修改obj.text 的内容时,会触发proxy对象的set操作。set操作中会执行 effect 副作用函数,去处理渲染的问题。

但是对于上面的内容,再进行优化处理。 处理的内容包括:

  1. 处理effect,因为effect在处理render的时候,会需要处理一些其他的操作
  2. 移除createReactiveObject中的手动effect。 配合改写后的effect,在get的时候,对当前的effect进行track收集。在set的时候,把effect中收集的内容进行调用。

首先是对effect进行改造
如果想要在渲染的时候进行特殊的处理。那么就把fn在执行前,调用所需要处理的函数。
这里采用的是抽离 一个effect 的类去处理, 在后续的扩展中,可以在执行渲染之前去处理想要处理的内容。具体代码如下:

// 声明一个全局变量,用来保存当前的effect,并且用于在依赖收集时进行收集
let activeEffect = '';
class ReactiveEffect {
    constructor(fn) {
        this._fn = fn;
    }
    run() {
        // 在run的时候,保存当前active的副作用函数
        activeEffect = this;
        this._fn();
    }
}

function effect(fn) {
    // 创建一个_effect的实例。
    let _effect = new ReactiveEffect(fn);
    // 可以在执行run之前,执行其他渲染前的操作
    _effect.run();
}

对createReactiveObject进行改造:

// 响应式对象的创建
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;
}
const targetMap = new WeakMap();
// targetMap: WeakMap  -> 内容为 target => depsMap
// depsMap: Map  -> 内容为 key => deps
// deps: Set -> 内容为 { effect...}
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)
    }
}
// 触发依赖
function trigger(target, key) {
	// 获取依赖
    const depsMap = targetMap.get(target);
    if (!depsMap) return;
    const deps = [];
    deps.push(depsMap.get(key));
    const effects = [];
    deps.forEach((dep) => {
        effects.push(...dep)
    });
    // 遍历执行依赖
    effects?.forEach((fn) => fn.run())
}

将上述的代码进行整合。形成完整的数据响应式原理。

3、完整的响应式原理

完整的响应式原理如下所示

// 响应式对象的创建
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);
            // effect(render);
            return res;
        }
    });
    // 返回一个响应式对象
    return proxy;
}
// 当前的effect副作用函数
let activeEffect = '';
// 依赖收集的容器
const targetMap = new WeakMap();
// targetMap: WeakMap  -> 内容为 target => depsMap
// depsMap: Map  -> 内容为 key => deps
// deps: Set -> 内容为 { effect...}
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);
    }
}
// 依赖触发。触发依赖主要是跟收集时是同样的步骤的。不同的是触发依赖最后一步是 执行依赖(收集起来的依赖)
function trigger(target, key) {
// 依赖的触发与收集形式是差不多的
    const depsMap = targetMap.get(target);
    if (!depsMap) return;
    
    let deps = depsMap.get(key); 
    for (const effect of deps) { 
        effect.run(); 
    }
    //const deps = [];
    // 这里留存的有包括后续扩展内容,暂时生搬硬套即可(在后续任务调度会添加额外的内容)
    //deps.push(depsMap.get(key));
    //const effects = [];
    //deps.forEach((dep) => {
    //  effects.push(...dep)
    //});
    //effects?.forEach((fn) => fn.run())
}

let obj = createReactiveObject({
    text: "effect3"
});
// 副作用函数,用于执行
class ReactiveEffect {
    constructor(fn) {
        // 将副作用函数需要处理的对调函数进行接受
        this._fn = fn;
    }
    run() {
        // 全局变量保存当前的副作用函数,用于依赖收集和调用
        activeEffect = this;
        this._fn();
    }
}
// 副作用函数
function effect(fn) {
    let _effect = new ReactiveEffect(fn);
    _effect.run();
}
// render 需要渲染的内容
function render() {
    document.body.innerHTML = `<div>${obj.text}</div>`
}
effect(render);
setTimeout(() => {
    obj.text = "reactivity3";
}, 2000);