Vue3响应式原理

52 阅读3分钟

Vue3的reactivity响应式如何实现的

问题

当我们在vue-sfc单文件组件setup中用reactive定义一个变量,变量的属性值改变之后,会触发视图更新,这到底是怎么实现的呢?

<template>
    <sapn>username: {{user.name}} </span>
</template>
<script setup>
let user = reactive({name:'zhangsan',age:22})
</script>

分析reactive函数

通过reactive的用法我们容易发现,reactive函数接受了一个对象作为入参,并且返回了一个对象,这个对象到底是什么呢?其实就是proxy。简单写一下reactive如下所示:

function reactive(target){
    // 如果是对象就proxy代理一下
    if(typeof target === 'object'){
        // MutableHandler是处理函数,有set,get的对象
        return new Proxy(target, MutableHandler)
    }
    // 如果是普通纸就直接返回,不做响应式
    return target
}

代理proxy定义完成后,那么现在来到了MutableHandler,它其实就是拦截get和set。

const MutableHandler{
    get(target,prop, receiver){
        // 依赖收集,收集了target[key]相关的effect
        // 到底收集了什么依赖呢?
        track(target, TrackOpTypes.GET, key);
        return Reflect.get(target,prop, receiver)
    },
    set(target,prop,value,receiver){
        trigger(target, TrackOpType.SET, key)
        return Reflect.set(target,prop,value,receiver)
    }
}

看到这里,估计会有些迷茫,取值调用get时到底收集的是什么依赖呢? 这里就需要提到effect副作用的概念

引申 effect

effect的含义是副作用,reactive返回的proxy取值一般都是在effect中使用的。effect接收一个函数作为参数。 简单的来说,effect中传入的函数在运行时如果用到了user的属性值,那么user的get就被调用,并收集这个effect。

// 此时的user是一个proxy
let user = reactive({name:"zhangsan", age:22})
// 取值函数
const fn = () => {
    console.log(user.name)
}
// effect执行时,user.name就收集了这个effect
effect(fn)

user.name == 'lisi';
// user.name变了,就会触发effect重新执行.

effect源码中比较复杂,简单来说effect传入的fn函数立刻执行时,当前这个effect对象就被保存为activeEffect,即正在执行中的effect。那么fn在执行时,console.log(user.name)就触发了get,此时收集的就是这个activeEffect。如果重新设置user.name ='lisi'后,就触发的set会将所有收集到的依赖(effect), 全部执行一遍。即effect又要执行一次。

PS: 如果effect嵌套即effect中套一个effect执行,就需要一个effectStack的栈来保存,执行时加入栈,执行完pop一下拿到上一个effect继续执行。

依赖收集的数据结构 WeakMap + Map + Set

targetsMap = new WeakMap()        // WeakMap 是 target 的集合
depsMap = targetsMap.get(target); // Map     是 一个target中所有属性prop的集合
dep = depsMap.get(prop);          // Set     是一个属性收集到的所有依赖的集合

//套用对象表示如下
{
    target1: {
        prop1: [effect1, effect2, ....],
        prop2: [.......]
    },
    target2: {
        prop1: [effect1, effect2, .....]
        prop2: [.......]
    },
    ......
}

关联

值得一提的是,reactive在单文件组件中都是运行在setup中的。setup只在组件初始化时执行一次,组件会保存setup返回的reactive代理值。组件在初始化时会创建一个effec用来更新组件的视图, 这个effect会保存在组件实例instance.update方法上。下次代理值触发set时,组件就会重新执行instance.update。

结论

reactive实际上做了一层包装返回了proxy实例,拦截了get,set方法。在取对象的某个属性值时收集当前的effect, 在设置对象的某个值值时将收集的effect全部执行。