Vue3响应式原理基本概念及手动实现

Posted by Mars . Modified at

Vue3的响应式原理

一、 Vue3响应式基本实现原理

假设reacObj是Vue中的一个响应式对象,它具有属性a和b;

Vue3使用reactive(obj)函数,创建一个响应式的对象,返回一个obj的代理对象Proxy。

reactive内部使用track、effect和trigger三个关键方法来描述响应式过程:

  • track(obj, property):对obj中的property进行响应式追踪;
  • effect: 依赖某一个响应式property的变量的计算方法函数;
  • trigger(obj, property):对依赖obj.property的所有变量计算方法进行重新调用。

基本流程是:

reacitive函数使用Proxy的捕获器,对响应式对象property的get和set操作进行捕获。

  • 当get的时候,对属性进行track操作;
  • 当set的时候,对属性进行trigger操作。

解释:

当get操作被捕获,说明有变量计算需要访问这个响应式属性,也就是依赖这个属性,所以要对这次get操作的effect进行追踪,添加到dep中以便后续进行响应式更新。

当set操作被捕获,说明有变量被更改,那么需要找到这个变量的dep依赖集,并运行内部的全部方法,来更新依赖它的全部变量。

1.1 对于响应式对象内部属性pbj.prop

对于属性a,如果我们用一个集合Set记录了依赖a的所有变量和它们的计算方法函数(这些计算方法函数叫做effect),在更新a的时候,将这些方法一一重新调用一遍,就可以实现a更改之后依赖它的变量的实时更新,也就是a属性成为了响应式的。

Vue中,为每个响应式对象内部属性所建立的Set对象称为一个dep(PS:dependency,依赖集),只要在修改任何响应式变量后,对应的依赖集dep内方法全部运行一次,就能实现其他变量的更新。

使用Set对象的理由是:它可以保证依赖集内部没有重复。

1.2 对于响应式对象本身obj

一个响应式对象本身可能具有多个属性,需要为它们每个单独建立一个dep依赖集,然后用另一个映射Map将对象属性和dep一一对应。

当track另一个对象属性property2的时候,就把这个property2也添加到映射Map里,然后把所有依赖property2的变量计算方法添加到新的对应dep中,并在Map中与property2对应起来。

这样,这个Map就记录了这个对象obj的全部属性的依赖信息,并可以根据属性更新其对应的那一部分依赖变量。这个Map变量名为depsMap。

1.3 对于整个Vue应用实例中所有响应式对象

一个Vue应用实例,可能声明有多个响应式对象,每一个响应式对象及其内部属性都应该被记录并追踪(track)。

整个应用实例使用一个WeakMap,记录应用实例中的每个响应式对象。这个WeakMap为targetMap。

响应式依赖图

1.4 effect(fn)的实现

  • 假设一个fn在执行的过程中,引用了某一响应式对象obj的属性prop;
  • 这个fn就是一个依赖(effect),需要在obj.prop被重新赋值的时候,再次调用来保证更新;
  • 因为需要在获取响应式对象的属性过程中,收集这个依赖fn,因此必须获取到当前执行函数fn的引用;
  • 解决方法是:把函数作为effect()的参数包装起来执行,然后在包装器里面获取到函数的引用,进行依赖收集。

1.5 track(obj, property)的实现

track(obj, property)运行后:

  1. 从targetMap中查找obj,如果没有就创建一个新的Map并赋值给obj对应的值,并作为depsMap返回。如果已经存在,则找到对应的depsMap;
  2. 在找到的depsMap中,查找property。如果没有则创建一个新的Set,赋值给property对应的值作为dep,并返回这个dep。如果已经存在,则返回找到的Dep;
  3. 在dep中加入全部effect。

1.6 trigger(obj, property)的实现

trigger(obj, property)运行后:

  1. 先从targetMap中查找obj,然后找到对应的depsMap;
  2. 从depsMap中查找property,找出对应的dep;
  3. 对dep中每一个effect进行执行。

弱映射WeakMap:只能储存对象,且当对象在其他地方没有引用的时候,WeakSet内的对象会被垃圾回收机制识别并回收。

1.7 reactive()函数:将对象变成响应式

Vue使用reactive(obj)函数,将一个对象变成响应式对象。(设置捕获器,返回Proxy对象即可)

官方教程代码如下:(示例,并非源码)

reactive实现

二、为什么要使用Proxy,相比Object.defineProperty有什么好处?

  • Object.defineProperty只能劫持对象中已经存在的属性,动态添加的新属性无法劫持(Proxy可以);
  • Object.defineProperty不能监听数组变化(Proxy可以);
  • Proxy存在兼容性问题,且无法完全polyfill.

三、手动实现Vue响应式代码

// Mars 2021.08
class Dep {
    constructor() {
        this.dep = new Set();
    }

    trigger() {
        for (let e of this.dep) {
            e();
        }
    }

    add(e) {
        this.dep.add(e);
    }
}

let activeEffect = null;
const objsMap = new WeakMap();

function effect(fn) {
    activeEffect = fn;
    fn();
    activeEffect = null;
}

function track(obj, prop) {
    if (activeEffect !== null) {
        if (!objsMap.has(obj)) objsMap.set(obj, new Map());
        let depsMap = objsMap.get(obj);
        if (!depsMap.has(prop)) depsMap.set(prop, new Dep());
        let dep = depsMap.get(prop);
        dep.add(activeEffect);
    }
}

function trigger(obj, prop) {
    if (objsMap.has(obj)) {
        if (objsMap.get(obj).has(prop)) {
            objsMap.get(obj).get(prop).trigger();
        }
    }
}

function reactive(obj) {
    const handler = {
        get(obj, prop) {
            track(obj, prop);
            return Reflect.get(obj, prop);
        },
        set(obj, prop, value) {
            let oldValue = Reflect.get(obj, prop);
            if (oldValue !== value) {
                Reflect.set(obj, prop, value);
                trigger(obj, prop);
            }
            return true;
        }
    };
    return new Proxy(obj, handler);
}


// test
let cal = reactive({
    a: 12,
    b: 13
});

effect(() => {
    console.log(cal.a + cal.b);
});
Keywords: Vue
previousPost nextPost
已经有 1000000 个小伙伴看完了这篇推文。