一文带你看懂vue3响应式系统原理

前言

vue3 beta版本已经发布快两个月了,相信你们或多或少都有去了解一些vue3的新特性,也有一部分人调侃学不动了,在我看来,技术确定是不断更迭的,新的技术出现可以提升生产力,落后的技术确定是要被淘汰的,五年前会JQ一把梭就能找到一份还行的工做,如今只会JQ应该不多公司会要了吧。恰好前两天尤大也发了一篇文章讲述了vue3的制做历程,有兴趣的同窗能够点击连接前往查看,文章是全英文的,英文不是很好的同窗能够借助翻译插件阅读。好了,废话很少说,本篇的主题是手写vue3的响应式功能。html

vue3的代码实例

在写代码前,不妨来看看如何使用vue3吧,咱们能够先去 github.com/vuejs/vue-n… clone一份代码,使用npm install && npm run dev后,会生成一个packages -> vue -> dist -> vue.global.js文件,这样咱们就可使用vue3了,在vue文件夹新建一个index.html文件。vue

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue3示例</title>
</head>
<body>
    <div id="app"></div>
    <button id="btn">按钮</button> 
    <script src="./dist/vue.global.js"></script>    
    <script> const { reactive, computed, watchEffect } = Vue; const app = document.querySelector('#app'); const btn = document.querySelector('#btn'); const year = new Date().getFullYear(); let person = reactive({ name: '烟花渲染离别', age: 23 }); let birthYear = computed(() => year - person.age); watchEffect(() => { app.innerHTML = `<div>我叫${person.name},今年${person.age}岁,出生年是${birthYear.value}</div>`; }); btn.addEventListener('click', () => { person.age += 1; }); </script>
</body>
</html>
复制代码

能够看到,咱们每次点击一次按钮,触发person.age += 1;,而后watchEffect自动执行,计算属性也相应更新,如今咱们的目标就很明确了,就是实现reactivewatchEffectcomputed方法。react

reactive方法

咱们知道vue3是基于proxy来实现响应式的,对proxy不熟悉的能够去看看阮一峰老师的es6教程:es6.ruanyifeng.com/#docs/proxy reflect 也是es6新提供的API,具体做用也能够参考阮一峰老师的es6教程:es6.ruanyifeng.com/#docs/refle… ,简单来讲他提供了一个操做对象的新API,将Object对象属于语言内部的方法放到Reflect对象上,将老Object方法报错的状况改为返回false值。 下面咱们来看看具体的代码吧,它对对象的getsetdel操做进行了代理。git

function isObject(target) {
    return typeof target === 'object' && target !== null;
}

function reactive() {
    // 判断是否对象,proxy只对对象进行代理
    if (!isObject(target)) {
        return target;
    }
    const baseHandler = {
        set(target, key, value, receiver) { // receiver:它老是指向原始的读操做所在的那个对象,通常状况下就是 Proxy 实例
            trigger(); // 触发视图更新
            return Reflect.set(target, key, value, receiver);
        },
        get(target, key, receiver) {
            return Reflect.get(target, key, value, receiver);
        },
        del(target, key) {
            return Reflect.deleteProperty(target, key);
        }
    };
    let observed = new Proxy(target, baseHandler);
    return observed;
}
复制代码

添加更新限制

上面的代码看上去好像没啥问题,可是在代理数组的时候,添加、删除数组的元素,除了能监听到数组自己要设置的元素变化,还会监听到数组长度length属性修改的变化,以下图:es6

因此咱们应该只在新增属性的时候去触发更新,咱们添加hasOwnProperty判断与老值和新值比较判断,只有修改自身对象的属性或者修改了自身属性而且值不一样的时候才去更新视图。github

set(target, key, value, receiver) {
    const oldValue = target[key];
    if (!target.hasOwnProperty(key) || oldValue !== value) { // 新增属性或者设置属性老值不等于新值
        trigger(target, key); // 触发视图更新函数
    } 
    return Reflect.set(target, key, value, receiver);
}
复制代码

深层级对象监听

上面咱们只对对象进行了一层代理,若是对象的属性对应的值仍是对象的话,它并无被代理过,此时咱们去操做该对象的时候,就不会触发set,也就不会更新视图了。以下图:npm

那么咱们应该怎么进行深层次的代理呢?设计模式

咱们观察一下person.hair.push(4)这个操做,当咱们去取person.hair的时候,会去调用personget方法,拿到属性hair的值,那么咱们就能够再它拿到值以后判断是不是对象,再去进行深层次的监听。数组

get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    return isObject(res) ? reactive(res) : res;
},
复制代码

缓存已代理对象

代理过的对象再去执行reactive方法的时候,会去从新设置代理,咱们应该避免这种状况,经过hashmap缓存代理过的对象,这样在再次代理的时候,判断对象存在hashmap中,直接返回该结果便可。缓存

  • 进行屡次代理示例
let obj = {
    name: '烟花渲染离别',
    age: 23,
    hair: [1,2,3]
}
let person = reactive(obj);
person = reactive(obj);
person = reactive(obj);
复制代码
  • 定义hashmap缓存代理对象

咱们使用WeakMap缓存代理对象,它是一个弱引用对象,不会致使内存泄露。 es6.ruanyifeng.com/#docs/set-m…

const toProxy = new WeakMap(); // 代理后的对象
const toRaw = new WeakMap(); // 代理前的对象

function reactive(target) {
    // 判断是否对象,proxy只对对象进行代理
    if (!isObject(target)) {
        return target;
    }
    let proxy = toProxy.get(target); // 当前对象在代理表中,直接返回该对象
    if (proxy) { 
        return proxy;
    }
    if (toRaw.has(target)) { // 当前对象是代理过的对象
        return target;
    }
    let observed = new Proxy(target, baseHandler);

    toProxy.set(target, observed);
    toRaw.set(observed, target);
    return observed;
}
let obj = {
    name: '烟花渲染离别',
    age: 23,
    hair: [1,2,3]
}
let person = reactive(obj);
person = reactive(obj); // 再去代理的时候返回的就是从缓存中取到的数据了
复制代码

这样reactive方法就基本已经实现完了。

收集依赖,自动更新

咱们先来瞅瞅以前是怎么渲染DOM的。

watchEffect(() => {
    app.innerHTML = `<div>我叫${person.name},今年${person.age}岁,出生年是${birthYear.value}</div>`;
});
复制代码

在初始化默认执行一次watchEffect函数后,渲染DOM数据,以后依赖的数据发生变化,会自动再次执行,也就会自动更新咱们的DOM内容了,这就是咱们常说的收集依赖,响应式更新。

那么咱们在哪里进行依赖收集,何时通知依赖更新呢?

  • 咱们在用到数据进行展现的时候,它就会触发咱们建立好的proxy对象的get方法,这个时候咱们就能够收集依赖了。
  • 在数据发生变化的时候一样会触发咱们的set方法,咱们在set中通知依赖更新。 这实际上是一种设计模式叫作发布订阅。

咱们在get中收集依赖:

get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    track(target, key); // 收集依赖,若是目标上的key变化,执行栈中的effect
    return isObject(res) ? reactive(res) : res;
}
复制代码

set中通知依赖更新:

set(target, key, value, receiver) { 
    if (target.hasOwnProperty(key)) {
        trigger(target, key); // 触发更新
    }
    return Reflect.set(target, key, value, receiver);
}
复制代码

能够看到咱们在get中执行了一个track方法进行收集依赖,在set中执行trigger触发更新,这样咱们知道了它的过程后,再来看看怎么实现watchEffect方法。

watchEffect方法

咱们传入到watchEffect方法里的函数就是咱们要收集的依赖,咱们将收集到的依赖用栈保存起来,栈是一种先进后出的数据结构,具体咱们看看下面代码实现:

let effectStack = []; // 存储依赖数据effect

function watchEffect(fn, options = {}) {
    // 建立一个响应式的影响函数,往effectsStack push一个effect函数,执行fn
    const effect = createReactiveEffect(fn, options);
    return effect;
}

function createReactiveEffect(fn) {
    const effect = function() {
        if (!effectsStack.includes(effect)) { // 判断栈中是否已经有过该effecy,防止重复添加
            try {
                effectsStack.push(effect); // 将当前的effect推入栈中
                return fn(); // 执行fn
            } finally {
                effectsStack.pop(effect); // 避免fn执行报错,在finally里执行,将当前effect出栈
            }
        }
    }
    effect(); // 默认执行一次
}
复制代码

关联effect和对应对象属性

上面咱们只是收集了fn存到effectsStack中,可是咱们还没将fn和对应的对象属性关联,下面步咱们要实现track方法,将effect和对应的属性关联。

let targetsMap = new WeakMap();

function track(target, key) { // 若是taeget中的key发生改变,执行栈中的effect方法
    const effect = effectsStack[effectsStack.length - 1];
    // 最新的effect,有才建立关联
    if (effect) {
        let depsMap = targetsMap.get(target);
        if (!depsMap) { // 第一次渲染没有,设置对应的匹配值
            targetsMap.set(target, depsMap = new Map());
        }
        let deps = depsMap.get(key);
        if (!deps) { // 第一次渲染没有,设置对应的匹配值
            depsMap.set(key, deps = new Set());
        }
        if (!deps.has(effect)) {
            deps.add(effect); // 将effect添加到当前的targetsMap对应的target的存放的depsMap里key对应的deps
        }
    }
}

function trigger(target, key, type) {
    // 触发更新,找到依赖effect
    let depsMap = targetsMap.get(target);
    if (depsMap) {
        let deps = depsMap.get(key);
        if (deps) {
            deps.forEach(effect => {
                effect();
            });
        }
    }
}
复制代码

targetsMap的数据结构较为复杂,它是一个WeakMap对象,targetsMapkey就是咱们target对象,在targetsMap中该target对应的值是一个Map对象,该Map对象的keytarget对象的属性,Map对象对应的key的值是一个Set数据结构,存放了当前该target.key对应的effect依赖。看下面的代码可能会比较清晰点:

let person = reactive({
    name: '烟花渲染离别',
});
targetsMap = {
    person: {
        'name': [effect]
    }
}
// {
// target: {
// key: [dep1, dep2]
// }
// }
复制代码

执行流程

  • 收集流程:执行watchEffect方法,将fn也就是effectpush到effectStack栈中,执行fn,若是fn中有用到reactive代理过的对象,此时会触发该代理对象的get方法,而咱们在get方法中使用了track方法收集依赖,track方法首先从effectStack中取出最后一个effect,也就是咱们刚刚push到栈中的effect,而后判断它是否存在,若是存在的话,咱们从targetMap取出对应的targetdepsMap,若是depsMap不存在,咱们手动将当前的target做为keydepsMap = new Map()做为值设置到targetMap中,而后咱们再从depsMap中取出当前代理对象key对应的依赖deps,若是不存在则存放一个新Set进去,而后将对应的effect添加到该deps中。
  • 更新流程:修改代理后的对象,触发set方法,执行trigger方法,经过传入的targettargetsMap中找到depsMap,经过keydepsMap中找到对应的deps,循环执行里面保存的effect

computed方法

computed以前咱们也来回顾下它的用法:

let person = reactive({
    name: '烟花渲染离别',
    age: 23
});
let birthYear = computed(() => 2020 - person.age);
person.age += 1;
复制代码

能够看到computed接受一个函数,而后返回一个通过处理后的值,在依赖的数据发生了修改后,computed也会从新计算一次。

实际computed它也是一个watchEffect函数,不过它比较特殊,这里在调用watchEffect时候传入了两个参数,一个是computedfn,还有一个就是咱们要给watchEffect的参数{ lazy: true, computed: true },咱们以前写watchEffect的时候并无对这些参数进行处理,因此如今咱们还得进行处理。

function computed(fn) {
    let computedValue;
    const computedEffect = watchEffect(fn, { 
        lazy: true, 
        computed: true
    });
    return {
        effect: computedEffect,
        get value() {
            computedValue = computedEffect();
            trackChildRun(computedEffect);
            return computedValue;
        }
    }
}
function trackChildRun(childEffect) {
    if (!effectsStack.length) return;
    const effect = effectsStack[effectsStack.length - 1];
    for (let i = 0; i < childEffect.deps.length; i++) {
        const dep = childEffect.deps[i];

        if (!dep.has(effect)) {
            dep.add(effect);
            effect.deps.push(dep);
        }
    }
}
复制代码

修改watchEffect方法,接收一个opstion参数而且添加lazy属性判断,当lazytrue时不当即执行传入的函数,由于computed方法是不会当即执行的。

function watchEffect(fn, options = {}) {
    // 建立一个响应式的影响函数,往effectsStack push一个effect函数,执行fn
    const effect = createReactiveEffect(fn, options);
    // start: 添加的代码
    if (!options.lazy) {
        effect()
    }
    // end: 添加的代码
    return effect;
}
复制代码

修改createReactiveEffect方法,添加options参数,而且给当前的effect添加deps用于收集被计算的属性的依赖,在本文的实例中就是age属性的依赖集合,保存computedlazy属性。

function createReactiveEffect(fn, options) {
    const effect = function() {
        // 判断栈中是否已经有过该effect,避免递归循环重复添加,好比在监听函数中修改依赖数据
        if (!effectsStack.includes(effect)) { 
            try {
                effectsStack.push(effect); // 将当前的effect推入栈中
                return fn(); // 执行fn
            } finally {
                effectsStack.pop(effect); // 避免fn执行报错,在finally里执行,将当前effect出栈
            }
        }
    }
    // start: 添加的代码
    effect.deps = [];
    effect.computed = options.computed;
    effect.lazy = options.lazy;
    // end: 添加的代码
    return effect;
}
复制代码

track方法将收集到的属性依赖集合添加到effectdeps

function track(target, key) { // 若是taeget中的key发生改变,执行栈中的effect方法
    const effect = effectsStack[effectsStack.length - 1];
    // 最新的effect,有才建立关联
    if (effect) {
        let depsMap = targetsMap.get(target);
        if (!depsMap) { // 第一次渲染没有,设置对应的匹配值
            targetsMap.set(target, depsMap = new Map());
        }
        let deps = depsMap.get(key);
        if (!deps) { // 第一次渲染没有,设置对应的匹配值
            depsMap.set(key, deps = new Set());
        }
        if (!deps.has(effect)) {
            deps.add(effect);
            // start: 添加的代码
            effect.deps.push(deps); // 将属性的依赖集合挂载到effect
            // end: 添加的代码
        }
    }
}
复制代码

trigger方法经过以前保存在effectcomputed属性区分是computed函数仍是普通的函数,而后分别保存起来,而后先执行普通的effect函数,在执行computed函数。

function trigger(target, key, type) {
    // 触发更新,找到依赖effect
    let depsMap = targetsMap.get(target);

    if (depsMap) {
        let effects = new Set();
        let computedRunners = new Set();
        let deps = depsMap.get(key);

        if (deps) {
            deps.forEach(effect => {
                if (effect.computed) {
                    computedRunners.add(effect);
                } else {
                    effects.add(effect);
                }
            });
        }

        if ((type === 'ADD' || type === 'DELETE') && Array.isArray(target)) {
            const iterationKey = 'length';
            const deps = depsMap.get(iterationKey);
            if (deps) {
                deps.forEach(effect => {
                    effects.add(effect);
                });
            }
        }

        computedRunners.forEach(computed => computed());
        effects.forEach(effect => effect());
    }
}
复制代码

总结computed执行流程

咱们来根据下面的代码来分析执行流程。

const value = reactive({ count: 0 });
const cValue = computed(() => value.count + 1);
let dummy;

watchEffect(() => { 
    dummy = cValue.value;
    console.log(dummy)
});

value.count = 1;
复制代码

第一步:先将count对象转换成响应式的对象。

第二步:执行computed方法,computed内部会执行watchEffect,而且传入lazycomputed属性,因为传入了lazytrue,因此并不会当即执行生成的effect,为了区分,下面统称这个effect为计算effect,将传入的fn称为计算fn,也就是不会往栈中添加数据,此时cValue保存的是一个包含计算effectget方法的对象。

第三步:执行watchEffect方法:这也是最关键的一步,执行watchEffect方法,因为没有带lazy属性,因此此时会马上执行effect方法,往effectsStack中添加当前的effect,而后执行fn

第四步:执行fn,执行fn中会去获取cValue的值,此时触发了computedget方法,而后执行第二步保存的计算effect

第五步:执行计算effect,将计算effect添加到effectsStack中(此时的effectsStack[普通effect, 计算effect]),而后执行计算fn

第五步:执行计算fn,计算fn依赖了响应式对象value,此时读取valuecount属性,触发value对象的get方法,get方法中执行track方法收集依赖。

第六步:执行track方法,拿到栈中最后一个元素也就是计算effect,初始化targetsMapdepsMap,而后将计算effect保存到count对应的deps中,同时也将deps保存到计算effectdeps中,下一步要用,这样就造成了一个双向收集的关系,计算effect保存了count的全部依赖,count也存了计算effect的依赖,track方法执行完执行下一步,返回获取到的value.count的值,存到computedValue中,而后咱们继续往下执行。

第六步:执行trackChildRun,计算fn执行完则将计算effect从栈中推出,此时effectsStack的栈顶为普通effect,首先咱们在trackChildRun中拿到栈尾元素也就是剩下的普通effect,而后循环传入的计算effectdeps数据,咱们在上一步执行track的时候,在计算effectdeps中保存了count属性对应的依赖集合,此时的deps中只有一个元素[计算effect],如今将普通effect也添加到dep中,因此此时depsMap{ count: [计算effect, 普通effect] }

第七步:执行value.count = 1,触发set方法,执行trigger方法,获取到count对应的deps也就是[计算effect, 普通effect],循环deps分别存储普通的effect和计算effect,而后前后执行计算effect和普通effect

致谢

感谢小伙伴们看到了这里,以为本文写的不错的点个赞再走呗。(^o^)/~

相关文章
相关标签/搜索