vue3源码系列(一)——reactive、effect篇

212 阅读6分钟

这是本专栏的第一篇文章,我们就从reactive这个API来打开vue3源码大门吧。众所周知,vue2定义响应式变量的方式一般是在data函数中定义。而在vue3中定义响应式变量一般是通过reactive或者ref(后面会讲到)来定义的。而reactive本质就是基于es6的新增APIProxy来实现数据响应式的。

一、Proxy

在介绍reactive之前我们先介绍一下Proxy这个API。Proxy可以代理一个对象,当我们改变访问代理对象时会触发get方法,改变代理对象的值的时候,会触发set方法。具体请看我的另一篇文章:[es6新增API Proxy]

二、reactive

import { mutableHandlers } from "./baseHandler";
export function reactive(target) {
    if(!isObject(target)) return; // reactive的参数必须要是一个对象
    // mutableHandlers: 进行一系列捕捉器操作
    const proxy = new Proxy(target, mutableHandlers); // 代理传入的对象
    return proxy;
}


// baseHandler.ts文件
export const mutableHandlers = {
    get(target, key, receiver) {
        let res = Reflect.get(target, key, receiver); // 重点:这里用Reflect是为了让取值时内部指向代理对象proxy,如果不用就指向源对象target

        if(isObject(res)) {
            return reactive(res); // 深度代理实现, 性能好, 只有取值触发get方法,且值是对象类型才递归,初始化不会递归
        }
        return res
    },
    set(target, key, value, receiver) {

        let oldValue = target[key];
        let result = Reflect.set(target, key, value, receiver); 
        return result;
    }
}

以上是简单的reactive创建一个代理对象,但该对象是如何成为响应式的(数据更新触发视图更新)呢?请看如下代码:

import { track, trigger } from './effect';
export const mutableHandlers = {
    get(target, key, receiver) {
    
       + track(target, 'get', key); // 做依赖收集的方法:当访问响应式对象时,会触发get方法,进行依赖收集

        let res = Reflect.get(target, key, receiver); // 重点:这里用Reflect是为了让取值时内部指向代理对象proxy,如果不用就指向源对象target

        if(isObject(res)) {
                return reactive(res); // 深度代理实现, 性能好, 只有取值触发get方法,且值是对象类型才递归,初始化不会递归
        }
        return res
    },
    set(target, key, value, receiver) {


        let result = Reflect.set(target, key, value, receiver); 
            // 给响应式对象某个属性赋值,会触发更新,即运行之前track时收集的方法
        + trigger(target, 'set', key, value, oldValue); 
        return result;
    }
}

effect

以上新增了两个方法:track, trigger,一个是进行依赖收集,一个是触发更新。说到这两个方法就不得不说Vue的另一个API:effect了。vue2源码是基于三种watcher来实现响应式的,而vue3是基于effect来实现的(接下来要讲解的computed,watch,ref等都会依赖此模块)。在vue3中template模板其实就是一个个嵌套的effect函数。具体如下:

export  let activeEffect = undefined; // 表示当前正在运行的effect,依赖收集时收集的就是这个

export function effect(fn, options:any = {}) {
    // 这里fn可以根据状态变化,重新执行, effect可以嵌套使用

    const _effect = new ReactiveEffect(fn, options.scheduler); // 创建响应式effect

    _effect.run();

    const runner = _effect.run.bind(_effect); // 绑定this执行
    runner.effect = _effect; // 将effe绑定到runner函数上

    return runner;
}


export class ReactiveEffect{
    public parent = null; // 用来记录正在执行的effect的父级effect(effect可this.fn()以嵌套使用)
    public active = true; // 表示这个effect默认为激活状态,ts这么写表示this.active = true
    public deps = []; // 用于effect收集属性
    constructor(public fn, public scheduler) { // 用户传递的参数会挂载到this上this.fn = fn
        this.fn = fn;
        this.scheduler = scheduler;
    }
    run() { // 执行effect
        if(!this.active) return this.fn(); // 这里表示如果是非激活的情况,只需要执行函数,不需要进行依赖收集(收集当前属性和effect的映射关系)

        // 这里做依赖收集(核心就是将当前的effect和稍后渲染的属性关联在一起)
        try{
                this.parent = activeEffect

                activeEffect = this; // 重点: 把当前正在执行的effect赋值给全局变量activeEffect


                return this.fn(); // 当稍后调用取值操作的时候,就可以获取到这个全局的activeEffect了

        }finally{
                activeEffect = this.parent; // effect执行完就为undefined
                this.parent = null;
        }
    }
}

如上代码,在effect函数接受一个函数,并且通过ReactiveEffect类创建_effect实例。ReactiveEffect类的原型上定义了一个run方法,在该方法中会将正在执行的effect赋值给全局变量activeEffect,并且运行effect中传入的fn方法,如下测试代码:

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

      <div id="app"></div>

      <script src="./reactivity.global.js"></script>

      <script>

        const { reactive, effect } = VueReactivity

        const personInfo = reactive({
          name: '张三',
          age: 18,
        })

        effect(() => {
          document.getElementById('app').innerHTML = `名字:${personInfo.name}  年龄:${personInfo.age}`
        })

        setTimeout(() => {
          personInfo.name = '李四'
        }, 2000)

      </script>
    </body>
</html>

如上代码,当运行effect函数时,会通过ReactiveEffect创建一个_effect实例,并运行_effect.run方法,在该方法中会将正在执行的effect赋值给全局变量activeEffect,并且运行effect中传入的fn方法,fn方法中访问了响应式对象personInfo的name和age属性,这就会触发get方法从而运行track方法进行依赖收集,要收集的effect就是这时的全局变量activeEffect。两秒后,运行personInfo.name = '李四',会触发set方法,从而运行trigger方法进行视图更新。接下来我们再来讲一下track、trigger这两个方法,看看他是怎么进行依赖收集和视图更新的。

// 重点: 这是收集需要双向收集,不仅仅是属性收集effect, effect也要收集属性, 这样可以清理,
// 当effect删除了之后,对应的属性收集了这个属性也要删除
const targetMap = new WeakMap()
// 依赖收集方法
export function track(target, type, key) {
    if(!activeEffect) return; // 正在执行的effect必须不为空才收集依赖
    let depsMap = targetMap.get(target);
    if(!depsMap) {
            targetMap.set(target, (depsMap = new Map()));
    } 
    let dep = depsMap.get(key);
    if(!dep) {
            depsMap.set(key, (dep = new Set()));
    }
    trackEffects(dep)
    // debugger
}

export function trackEffects(dep) {
    if(activeEffect) {
        let shouldTrack = !dep.has(activeEffect);
        if(shouldTrack) {
                dep.add(activeEffect); // 收集对应的依赖
        }
    }
	
}

// 设置完值,触发更新

export function trigger(target, type, key, value, oldValue) {
    const depsMap = targetMap.get(target);
    if(!depsMap) return;

    let effects = depsMap.get(key); 
    if(effects) {

        triggerEffects(effects)
    }
}

export function triggerEffects(effects) {
    effects = new Set(effects); // 让循环的effects和上一个effects使用不同的引用,防止循环引用造成死循环
    effects.forEach(effect => {

        // 注意: 这里要避免run触发trigger,要不会造成死循环
        if(effect !== activeEffect) {
                if(effect.scheduler) {
                        effect.scheduler(); // 如果用户传入了调度函数,则用用户的
                }else {
                        effect.run();  // 否则默认刷新视图
                }
        }
    })
}

vue3在进行依赖收集时,会全局构建依赖图。由于reactive必须接受一个对象,所以其先new WeakMap创建一个以传入的对象作为key的WekMap对象,之后再用new map创建一个以传入对象的属性作为key的对象,其值为Set集合,该集合就是存储activeEffect的。trigger就是根据对象及其对应属性找到对应集合进行执行更新。如下是具体全局收集依赖格式:

QQ图片20221013131712.png

总结

vue3响应式数据运行原理步骤为:

  1. 我们先创建了一个响应式对象 new Proxy
  2. effect 默认数据变化要能更新,我们得先将正在执行的effect作为全局变量,渲染(取值),我们在get方法中进行依赖收集
  3. weakMap(对象: map(属性: set(effect)))
  4. 稍后用户发生数据变化,会通过对象属性来查找对应的effect集合,找到effect全部执行