vue3响应式系统流程分析与实现

2020年6月8日20:28:45 发表评论 134 views

vue3的代码实例

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

<!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>
复制代码
vue3响应式系统流程分析与实现

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

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操作进行了代理。

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属性修改的变化,如下图:

vue3响应式系统流程分析与实现

所以我们应该只在新增属性的时候去触发更新,我们添加hasOwnProperty判断与老值和新值比较判断,只有修改自身对象的属性或者修改了自身属性并且值不同的时候才去更新视图。

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,也就不会更新视图了。如下图:

vue3响应式系统流程分析与实现

那么我们应该怎么进行深层次的代理呢?

我们观察一下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;
},
复制代码
vue3响应式系统流程分析与实现

缓存已代理对象

代理过的对象再去执行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
vue3响应式系统流程分析与实现

作者:烟花_渲染离别
链接:https://juejin.im/post/5edb93caf265da771526eeda
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: