Vue 3 源码解析(4)reactive 的实现(下)

950 阅读8分钟

Vue3 源码系列文章会持续更新,全部文章请查阅我的掘金专栏

本系列的文章 demo 存放于我的 Github 仓库,推荐大家下载和调试,进而加深理解。

一. 只读接口

Vue 提供了 readonly 接口,来防止指定对象的属性被修改:

  const { readonly, effect } = Vue;
  
  const div = document.querySelector('div');
  const readonlyData = readonly({  // 调用 readonly 接口
    msg: 'old msg',
  });

  effect(() => {
    div.innerText = readonlyData.msg;
  });

  // 不生效,因为 readonlyData 是只读对象,不允许属性被修改
  readonlyData.msg = 'latest msg';  

点击查看 codepen 线上示例

readonly 的实现同样借助了 Proxy 的拦截器 —— 被 get 拦截时正常返回数据,被 set 拦截时绕过改动行为。

改动参考如下:

/** reactive.js **/

import { readonlyHandlers } from './baseHandlers.js'
export const readonlyMap = new WeakMap();

// 新增 readonly 接口
export function readonly(target) {
    const existingProxy = readonlyMap.get(target);
    if (existingProxy) {
        return existingProxy
    }

    const proxy = new Proxy(
        target,
        // 和 reactive 的区别是使用了另一个 handler
        readonlyHandlers  
    );

    readonlyMap.set(target, proxy);
    return proxy
}


/** baseHandlers.js **/

// 新增
const readonlyGet = createGetter();

// 新增
export const readonlyHandlers = {
    get: readonlyGet,
    set(target, key) {
        // 直接返回 true,不做修改操作
        return true
    },
    deleteProperty(target, key) {
        // 直接返回 true,不做删除操作
        return true
    }
}

但目前 createGetter 方法所生成的 get 拦截器是针对常规响应式对象的。对于只读属性的对象而言,它不需要执行依赖追踪操作。我们可以为 createGetter 方法新增 isReadonly 参数,来对只读的逻辑进行处理:

function createGetter(isReadonly = false) {  // 新增 isReadonly 参数
    return function get(target, key, receiver) {
        // 新增
        const targetFromMap = (isReadonly ? readonlyMap : proxyMap).get(target);
        // 将 proxyMap.get(target) 改为 targetFromMap
        if (key === ReactiveFlags.RAW && targetFromMap) { 
            return target;
        }

        const targetIsArray = isArray(target);

        // 新增 !isReadOnly 判断条件,只读对象执行数组方法可放行(毕竟会绕过追踪)
        if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
            return Reflect.get(arrayInstrumentations, key, receiver)
        }

        const res = Reflect.get(target, key, receiver);

        if (isSymbol(key) && builtInSymbols.has(key)) {
            return res;
        }

        // 新增 !isReadOnly 判断条件,只读对象无须追踪
        if (!isReadonly) {
            track(target, key);
        }

        if (isObject(res)) {
            // 新增 isReadOnly 判断。确保子孙属性都能被处理到
            return isReadonly ? readonly(res) : reactive(res);
        }

        return res;
    }
}

如上方代码所示,get 拦截器并非只是单纯地判断是否只读,然后返回 Reflect.get 的结果,而是会顺着原有响应式的逻辑走到最后,方便处理对象的深层属性。

为方便理解,这段代码可以简化为:

function createGetter(isReadonly = false) { 
    const res = Reflect.get(target, key, receiver);

    /** 绕过其它响应式的逻辑... **/
    /** 因为只读属性的访问不需要做任何依赖收集处理 **/ 

    if (isObject(res)) {
        // 确保深层属性都能被处理
        return isReadonly ? readonly(res) : reactive(res);
    }

    return res;
}

二. 浅响应和浅只读接口

2.1 浅响应

有时候我们只希望对被代理对象的最外层属性做响应式的处理,对其嵌套对象的属性则不做处理(从而提升性能)。这种能力简称为浅响应,在 Vue 中对应的接口是 shallowReactive

  const { shallowReactive, effect } = Vue;
  
  const div = document.querySelector('div');
  const shallowReactiveData = shallowReactive({
    a: '[old a]',
    b: {
      msg: '[old b]'
    }
  });

  effect(() => {
    div.innerText = shallowReactiveData.a + ';' + shallowReactiveData.b.msg;
  });

  setTimeout(() => {
    shallowReactiveData.a = '[latest a]';
    shallowReactiveData.b.msg = '[latest b]';  // 不会生效
  }, 2000); 

点击查看 codepen 线上示例

shallowReactive 的实现原理较简单 —— 在 get 拦截器中直接返回结果即可,不对嵌套属性进行遍历。
具体实现如下:

/** reactive.js **/

import { shallowReactiveHandlers } from './baseHandlers.js'
export const shallowReactiveMap = new WeakMap();

export function shallowReactive(target) {
    const existingProxy = shallowReactiveMap.get(target);
    if (existingProxy) {
        return existingProxy
    }

    const proxy = new Proxy(
        target,
        shallowReactiveHandlers  // 浅响应专属 handler
    );

    shallowReactiveMap.set(target, proxy);
    return proxy
}


/** baseHandlers.js **/

export const shallowReactiveHandlers = Object.assign(
    {},
    mutableHandlers,
    {
        get: createGetter(false, true),
        set: createSetter()
    }
)

function createGetter(isReadonly = false, shallow = false) {  // 新增 shallow 参数
    return function get(target, key, receiver) {
        const targetFromMap = (
            isReadonly ? readonlyMap :
                (shallow ? shallowReactiveMap : proxyMap)  // 新增判断
        ).get(target);
        if (key === ReactiveFlags.RAW && targetFromMap) {
            return target;
        }

        // ...

        if (shallow) {  // 新增,若为浅响应,直接返回属性值
            return res
        }

        if (isObject(res)) {  // 浅响应不会走到这里来,即嵌套属性不会被处理为响应式
            return isReadonly ? readonly(res) : reactive(res);
        }

        return res;
    }
}

2.2 浅只读

如果你希望让一个对象的最外层属性不被修改,但其嵌套属性依旧保持可读写的能力,可以使用 Vue 的 shallowReadonly 浅只读接口:

  const { shallowReadonly } = Vue;
  
  const div = document.querySelector('div');
  const shallowReadonlyData = shallowReadonly({
    a: '[old a]',
    b: {
      msg: '[old b]'
    }
  });
  
  shallowReadonlyData.a = '[latest a]';  // 不会生效
  shallowReadonlyData.b.msg = '[latest b]';  // 生效

  div.innerText = shallowReadonlyData.a + ';' + shallowReadonlyData.b.msg;

点击查看 codepen 线上示例

浅只读的实现较为简单,它结合了 readonly 和 shallowReactive 的逻辑:

/** reactive.js **/

import { shallowReadonlyHandlers } from './baseHandlers.js'
export const shallowReadonlyMap = new WeakMap();

export function shallowReadonly(target) {
    const existingProxy = shallowReadonlyMap.get(target);
    if (existingProxy) {
        return existingProxy
    }

    const proxy = new Proxy(
        target,
        shallowReadonlyHandlers  // 浅只读专属 handler
    );

    shallowReadonlyMap.set(target, proxy);
    return proxy
}


/** baseHandlers.js **/

export const shallowReadonlyHandlers = Object.assign(
    {},
    readonlyHandlers,  // 复用 readonly 的 handler
    {
        get: createGetter(true, true)  // 传入 isReadonly shallow 参数
    }
)

这里复用了 readonly 的 handler,这意味着被代理对象在 set 和 deleteProperty 阶段不会做任何操作。同时重写了 get 拦截器,传入的 shallow 参数可以确保其只对最外层的属性做处理。

💡 shallowReadonly 接口代理后的对象,仅是把最外层的属性被设置为只读而已,其不具备收集依赖/触发副作用函数的能力。

三. markRaw

如果有的对象你希望永远不要被 Vue 响应式接口的 Proxy 所代理,可以使用 markRaw 方法来对该对象进行标记:

  const { markRaw, reactive, effect } = Vue;
  
  const div = document.querySelector('div');
  const obj = markRaw({
    msg: 'old msg'
  });
  const obj2 = reactive(obj);
  
  effect(() => {
    div.innerText = obj2.msg
  })

  obj2.msg = 'latest msg';  // 不会触发副作用函数执行

点击查看 codepen 线上示例

markRaw 会给对象加上一个不可枚举的私有属性 __v_skip,在调用响应式接口时先检查对象是否存在该属性,若有则返回原始对象。

具体实现如下:

/** reactive.js **/

export function reactive(target) {
    const existingProxy = proxyMap.get(target);
    if (existingProxy) {
        return existingProxy
    }

    // 新增判断,存在 markRaw 标记属性则直接返回原始内容
    if(target[ReactiveFlags.SKIP]) {
        return target
    }

    const proxy = new Proxy(
        target,
        mutableHandlers
    );

    proxyMap.set(target, proxy);
    return proxy
}

export function markRaw(value) {
    def(value, ReactiveFlags.SKIP, true)
    return value
}



/** shared.js **/
/** def 方法的实现 **/

export const def = (obj, key, value) => {
    Object.defineProperty(obj, key, {
        configurable: true,
        enumerable: false,
        value
    })
}

我们还需给 readonlyshallowReactive 和 shallowReadonly 都加上检查 markRaw 标记属性的逻辑,不过鉴于这些方法和 reactive 方法的结构非常相近,可以对其做进一步的封装:

function createReactiveObject(target, handler, map) {
    const existingProxy = map.get(target);
    if (existingProxy) {
        return existingProxy
    }

    // 被 markRaw 标记了的对象、非对象类型或不可扩展数组是不能被代理的,直接返回原始内容
    if (target[ReactiveFlags.SKIP] || !isObject(target) || !Object.isExtensible(target)) {
        return target
    }

    const proxy = new Proxy(
        target,
        handler
    );

    map.set(target, proxy);
    return proxy
}

export function reactive(target) {
    return createReactiveObject(target, mutableHandlers, proxyMap)
}

export function readonly(target) {
    return createReactiveObject(target, readonlyHandlers, readonlyMap)
}

export function shallowReactive(target) {
    return createReactiveObject(target, shallowReactiveHandlers, shallowReactiveMap)
}

export function shallowReadonly(target) {
    return createReactiveObject(target, shallowReadonlyHandlers, shallowReadonlyMap)
}

留意在 createReactiveObject 封装方法里,我们不仅对让 markRaw 标记了的对象跳过了后续的 Proxy 代理,顺便让非 Object 类型、不可扩展数组也都跳过了代理(因为它们都不是响应式接口的受理类型)。

四. 工具方法补充

Vue 提供了一些工具方法,用于识别指定对象是否被某个接口代理过。
它们的原理和 toRaw 接口的逻辑类似 —— 查询对象是否存在特定属性,查询过程会被 get 拦截器拦截,并返回规则匹配的结果。

具体实现如下:

/** reactive.js **/

export const ReactiveFlags = {
    RAW: '__v_raw',
    SKIP: '__v_skip',
    IS_REACTIVE: '__v_isReactive', // 新增
    IS_READONLY: '__v_isReadonly',  // 新增
    IS_SHALLOW: '__v_isShallow',  // 新增
};

// 查询是否是由 shallowReactive 或 shallowReadonly 创建的代理对象
export function isShallow(value) {
    return !!(value && value[ReactiveFlags.IS_SHALLOW]);
}

// 查询是否是由 readonly 创建的只读代理
export function isReadonly(value) {  
    return !!(value && value[ReactiveFlags.IS_READONLY]);
}

// 查询是否是由 reactive 创建的响应式代理
export function isReactive(value) {  
    if (isReadonly(value)) {  // 处理 readonly(reactive(target)) 场景
        return isReactive((value)[ReactiveFlags.RAW])
    }
    return !!(value && value[ReactiveFlags.IS_REACTIVE]);
}

// 查询是否由 reactive 或 readonly 创建的代理对象
export function isProxy(value) {
    return isReactive(value) || isReadonly(value)
}


/** baseHandlers.js **/

function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {

        const targetFromMap = (isReadonly
            ? shallow
                ? shallowReadonlyMap
                : readonlyMap
            : shallow
                ? shallowReactiveMap
                : proxyMap
        ).get(target);

        if (key === ReactiveFlags.IS_REACTIVE) {  // 新增规则
            return !isReadonly
        } else if (key === ReactiveFlags.IS_READONLY) {  // 新增规则
            return isReadonly
        } else if (key === ReactiveFlags.IS_SHALLOW) {  // 新增规则
            return shallow
        } else if (key === ReactiveFlags.RAW && targetFromMap) {
            return target;
        }

        // ...
    }
}

五. 代理集合类型

集合类型包括了 SetMapWeakSet 和 WeakMap,它们和普通的对象类型很不一样 —— 除了获取集合元素的数量会直接访问其 size 属性,其它访问/修改集合数据的形式都是通过调用原生的接口方法来实现。

为了区别于处理常规对象的 baseHandler.js,Vue 专门封装了一个 collectionHandlers.js 来处理集合类型的响应式逻辑。

我们先看 reactive.js 模块的改动:

/** reactive.js - 第一部分 **/

import {
    mutableHandlers, readonlyHandlers,
    shallowReactiveHandlers, shallowReadonlyHandlers
} from './baseHandlers.js'

// 新增
const TargetType = {
    COMMON: 1,
    COLLECTION: 2
}

// 新增
function targetTypeMap(rawType) {
    switch (rawType) {
        case 'Object':
        case 'Array':
            return TargetType.COMMON
        case 'Map':
        case 'Set':
        case 'WeakMap':
        case 'WeakSet':
            return TargetType.COLLECTION
    }
}

// 新增 collectionHandlers 参数
function createReactiveObject(target, handlers, collectionHandlers, map) {  
    // ...

    // 新增。toRawType 可获取对象类型名称,实现见右方
    const targetType = targetTypeMap(toRawType(target));

    const proxy = new Proxy(
        target,
        // 集合类型使用专属的 handlers
        targetType === TargetType.COLLECTION ? collectionHandlers : handlers 
    );

    map.set(target, proxy);
    return proxy
}
/** reactive.js - 第二部分 **/

const toRawType = (value) => {
    // 从类似 "[object RawType]" 的字符串中抽取出 "RawType"
    return toTypeString(value).slice(8, -1)
}

export function reactive(target) {
    //  新增 mutableCollectionHandlers 参数 
    return createReactiveObject(target, mutableHandlers, 
        mutableCollectionHandlers, proxyMap)
}

export function readonly(target) {
    //  新增 readonlyCollectionHandlers 参数 
    return createReactiveObject(target, readonlyHandlers, 
        readonlyCollectionHandlers, readonlyMap)
}

export function shallowReactive(target) {
    //  新增 shallowCollectionHandlers 参数 
    return createReactiveObject(target, shallowReactiveHandlers, 
        shallowCollectionHandlers, shallowReactiveMap)
}

export function shallowReadonly(target) {
    //  新增 shallowReadonlyCollectionHandlers 参数 
    return createReactiveObject(target, shallowReadonlyHandlers, 
        shallowReadonlyCollectionHandlers, shallowReadonlyMap)
}

我们还需要实现 collectionHandlers.js 模块中的 mutableHandlersreadonlyHandlersshallowReactiveHandlers 和 shallowReadonlyHandlers 接口。

鉴于集合类型都是靠调用原生方法(或者查询 size 属性)来访问和修改数据的,我们只需要在这四个 handler 中配置 get 拦截器即可。

collectionHandlers.js 模块的实现参考:

/** collectionHandlers.js **/

function createInstrumentationGetter(isReadonly, shallow) {
    return (target, key, receiver) => {
        // TODO...
    }
}

export const mutableCollectionHandlers = {  // 响应式 handler
    get: createInstrumentationGetter(false, false)
}

export const shallowCollectionHandlers = {  // 浅响应 handler
    get: createInstrumentationGetter(false, true)
}

export const readonlyCollectionHandlers = {  // 只读 handler
    get: createInstrumentationGetter(true, false)
}

export const shallowReadonlyCollectionHandlers = {  // 浅只读 handler
    get: createInstrumentationGetter(true, true)
}

createInstrumentationGetter 方法会在集合类型访问 size 属性,或调用 gethasforEach 、枚举(keysvaluesentries)方法时,对依赖进行收集(执行 track);
在调用 addsetdeleteclear 方法时,触发相应的副作用函数(执行 trigger)。

鉴于 createInstrumentationGetter 的实现很接近于我们之前对数组栈方法(例如 push)的处理,本文不再赘述。

collectionHandlers.js 模块完整的代码可以点击这里获取。