vue3源码解析(一)reactive的响应性

58 阅读6分钟

大家都知道,vue2的响应性存在以下限制:

  1. 当为 对象 新增一个没有在 data 中声明的属性时,新增的属性 不是响应性 的
  2. 当为 数组 通过下标的形式新增一个元素时,新增的元素 不是响应性 的 那Vue3做了些什么,不再有以上限制了呢?我们一起来看看吧!

今天先探索reactive的响应性。

一、构建reactive函数,获取proxy实例

整个 reactive 函数,本质上是返回了一个 proxy 实例,那么我们先去实现这个 reactive 函数,得到 proxy 实例。

创建packages/reactivity/src/reactive.ts 模块:

import { mutableHandlers } from "./baseHandlers";
/**
 * 响应性 Map缓存对象
 * key target
 * val proxy
 */
export const reactiveMap = new WeakMap<object, any>()

/**
 * 为复杂数据类型,创建响应性对象
 * @param target 被代理对象
 * @returns 代理对象
 */
export function reactive(target: object) {
    return createReactiveObject(target, mutableHandlers, reactiveMap)
}

function createReactiveObject(
    target: object, 
    baseHandlers: ProxyHandler<any>, 
    proxyMap: WeakMap<object, any>) {
        // 如果该实例已经被代理,则直接返回
        const existingProxy = proxyMap.get(target)
        if(existingProxy) {
            return existingProxy
        }
        // 未被代理则生成proxy实例
        const proxy = new Proxy(target, baseHandlers)
        // 缓存代理对象
        proxyMap.set(target, proxy)
        return proxy
    }

此时,在vue/examples/reactivity/reactive.html中建一个demo:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="../../dist/vue.js"></script>
</head>
<body>
    <div id="app"></div>
</body>
<script>
    const { reactive }  = Vue
    const obj = reactive({
        name: 'zhangsan'
    })
    console.log(obj)
</script>
</html>

可看到打印结果:

image.png

已经可以正确返回一个proxy

二、handler部分的实现

创建packages/reactivity/src/mutableHandlers.ts 模块:

里边的get方法和set方法就是整个handler的核心部分

/**
 * getter回调方法
 */
const get = createGetter()
/**
 * 创建getter回调方法
 */
function createGetter() {
    return function get(target: object, key: string | symbol, receiver: object ) {
        // 利用 Reflect 得到返回值
        const res = Reflect.get(target, key, receiver)
        // 收集依赖
        track(target, key)
        return res 
    }
}

const set = createSetter()

function createSetter() {
    return function set(target: object, key: string | symbol, value: any, receiver: object) {
        // 利用 Reflect 设置新值
        const res = Reflect.set(target, key, value, receiver)
        // 触发依赖
        trigger(target, key, value)
        return res
    }
}

export const mutableHandlers: ProxyHandler<object> = {
    get,
    set
}


以上我们知道了get方法中收集依赖,set方法触发依赖。

三、触发effect

1、创建packages/reactivity/src/effect.ts

在创建好了reactive实例之后,接下来我们需要触发effect.

demo中:

effect(() => {
        document.querySelector('#app').innerHTML = obj.name
    })

effect的实现,是生成一个ReactiveEffect,在ReactiveEffect里面,执行run函数,在run函数中,首先标记当前触发的activeEffect,然后执行fn,至此完成了回调函数的触发。

export function track(target: object, key: unknown){}

export function trigger(target: object, key: unknown, newValue: unknown){}

export let activeEffect: ReactiveEffect | undefined

export class ReactiveEffect<T = any> {
    constructor(public fn: () => T) {}
    run() {
        activeEffect = this
        return this.fn()
    }

}
export function effect<T = any>(fn: () => T) {
    const _effect = new ReactiveEffect(fn)
    _effect.run()
}

在demo中引入effect函数,可以在浏览器看到name已经渲染到div中了

2、实现track

我们知道,getter 时要收集当前的 fn 函数,以便在setter 的时候可以执行对应的 fn 函数 但是对于收集而言,如果仅仅是把 fn 存起来还是不够的,我们还需要知道,当前的这个 fn 是哪个响应式数据对象哪个属性对应的,只有这样,我们才可以在 该属性 触发 setter 的时候,准确的执行响应性。

所以我们的实现思路是这样的:创建一个weakMap: WeakMap:

  • key:响应性对象
  • value: Map对象: -key: 响应性对象的指定属性 -value: 指定对象的指定属性的执行函数
type KeyToDepMap = Map<any, ReactiveEffect>

const targetMap = new WeakMap<any, KeyToDepMap>()

export function track(target: object, key: unknown){
    // 如果当前不存在执行函数,则直接返回
    if(!activeEffect) return
    // 尝试从targetMap中,根据target获取map
    let depsMap = targetMap.get(target)
    // 如果获取到的 map 不存在,则生成新的 map 对象,并把该对象赋值给对应的value
    if(!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
    }
    // 为指定map 指定key设置回调函数
    depsMap.set(key, activeEffect)
    console.log(targetMap)
}

打印targetMap,已经得到相应的weakMap:

image.png

3、实现trigger

以上可知,所有的依赖关系都保存到了targetMap,所以说依赖触发也就是从targetMap中读取对应的effect,然后执行对应的函数就可以了。

/**
 * 触发依赖的方法
 * @param target  WeakMap 的 key
 * @param key 代理对象的key,当依赖被触发时, 需要根据该key获取
 */
export function trigger(target: object, key: unknown){
    // 依据 target 获取存储的map实例
    const depsMap = targetMap.get(target)
    if(!depsMap) return
    // 依据 key, 从depsMap中取出value,该value是一个ReactiveEffect类型的数据
    const effect = depsMap.get(key) as ReactiveEffect
    if(!effect) {
        return
    }
    effect.fn()
}

四、构建Dep模块,处理一对多的依赖关系

创建以下测试实例,就会发现,只有第二个div的值是改变的。

<body>
    <div id="app1"></div>
    <div id="app2"></div>
</body>
<script>
    const { reactive, effect }  = Vue
    const obj = reactive({
        name: 'zhangsan'
    })
    effect(() => {
        document.querySelector('#app1').innerHTML = obj.name
    })
    effect(() => {
        document.querySelector('#app2').innerHTML = obj.name
    })
    console.log(obj)
    setTimeout(() => {
        obj.name = 'lisi'
    }, 2000);
</script>

原因是在做依赖收集时,指定object的每个key只对应了一个value值,也就是说只能完成一个key对应一个有效effect函数的对应关系,所以对于上边的demo,一个name属性对应了两个effect,目前的代码是不支持的。想要一个key对应多个effect也很简单,只需要让原先Map里的一个ReactiveEffect变成数组即可。

创建packages/reactivity/src/dep.ts

用set来存储多个effect

import { ReactiveEffect } from "./effect";

export type Dep = Set<ReactiveEffect>

export const createDep = (effects?: ReactiveEffect[]): Dep => {
    const dep = new Set<ReactiveEffect>(effects) as Dep
    return dep
}

接下来修改track函数中的代码,将之前代码depsMap.set(key, activeEffect)处进行修改:

    const dep = depsMap.get(key)
    if(!dep) {
        depsMap.set(key, (dep = createDep()))
    }
    trackEffects(dep)
export function trackEffects(dep: Dep) {
    dep.add(activeEffect!)
}

trigger同样也需要进行修改,将原来的const effect = depsMap.get(key) as ReactiveEffect处进行修改:

// 依据 key, 从depsMap中取出value,该value是一个ReactiveEffect类型的数据
    const dep: Dep | undefined = depsMap.get(key)
    if(!dep) {
        return
    }
    triggerEffects(dep)
/**
 * 
 * @param 依次触发dep中保存的依赖
 */
export function triggerEffects(dep: Dep) {
    const effects = Array.isArray(dep) ? dep : Array.from(dep)
    // 依次触发依赖
    for(const effect of effects) {
            triggerEffect(effect)
        }
}

export function triggerEffect(effect: ReactiveEffect) {
    effect.run()
}

至此就完成了多个effect的依赖收集和多个effect的依赖触发,本质上就是把Map里的value值由原来的ReactiveEffect变成了对应的Set数组。

五、reactive函数的局限性

1、reactive只能对复杂数据类型进行使用

由以上分析可知,reactive内部使用的是proxy,而proxy只能接收复杂类型,不能接收简单类型,所以简单数据类型的响应性不能通过reactive函数来实现

2、reactive的响应性数据,不可以进行解构

当使用解构赋值时,创建的是一个普通对象,而不是通过Proxy包装的对象,因此失去了响应性。

六、总结

通过以上的学习,我们在构建了reactive响应性函数,我们知道了它是通过是通过 proxy 的 setter 和 getter,来实现的数据监听,需要配合 effect 函数进行使用,基于 WeakMap 完成的依赖收集和处理,可以存在一对多的依赖关系。同时也了解了reactive函数的不足。

因为reactive的不足,所以vue3又为我们提供了ref函数构建响应性,那么ref 函数的内容是如何进行实现的呢?ref 可以构建简单数据类型的响应性吗?为什么 ref 类型的数据,必须要通过 .value 访问值呢?让我们下一篇文章见吧。

本文表述若有不准确的地方,欢迎大佬指正,互相学习,共同成长~