vue3源码解析——响应式分析 手写reactive(一)

1. 准备工作

现在github上下载vue3的源码 Vue3源代码地址。下载完成后简单看一眼目录结构,我们就直接进入 packages=>reactivity=>reactive.ts 文件,深入了解一下vue3中的响应式,reactive是如何实现的。

接下来跟随我的思路,手写一个简易的reactive,看看vue在实现过程中都做了哪些考虑和涉及到哪些知识点。

2.手写简易reactive

我们都知道,vue3响应式的实现是基于proxy,并且在 读取数据属性时 为其添加依赖,在设置属性值时 触发更新。基于这点知识,我们可以写出如下代码

reactive.ts 这个文件中实现reactive函数

ts 复制代码
import { track, trigger } from "./effect";

export function reactive<T extends object>(target: T): T;
export function reactive(target: object) {
    const proxy = new Proxy(target, {
        get(target, key) {
            // 依赖收集
            track(target, key)
            const res = Reflect.get(target, key)
            return res
        },
        set(target, key, val) {
            // 派发更新
            tigger(target, key, val)
            const res = Reflect.set(target, key, val)
            return res
        }
    })
    return proxy
}

可能你对ProxyReflect不太熟悉?或者不知道为什么返回值

effect.ts 这个文件中涉及到依赖收集,派发更新和监听数据变化的一系列函数

ts 复制代码
// 暂时不需要知道 track和 trigger中具体做了什么事情
export const track = (target: object, key: unknown) => {
  console.log("在收集依赖中", target, `${key}属性被读取`);
};

export const trigger = (target: object, key: unknown, val: unkown) => {
  console.log("在派发更新中", target, `${key}属性更改,触发更新`);
};

index.ts 这个文件为主入口文件,用于测试代码返回结果

ts 复制代码
import { reactive } from "./reactive";

// 定义普通对象,测试是否代理成功
const obj = {
    name: 'labmen',
    age: 18,
    grade: {
        math: 90,
        english: 15,
        get totalScore() {
            return this.math + this.english
        }
    }
}

const proxyObj = reactive(obj)

console.log(proxyObj.name) // 在收集依赖中 name 属性被读取
proxyObj.name = 'xiaoMing' // 在派发更新中 name 属性被更改 触发更新

3.边界问题处理

现在响应式最最最基础的内容就写完了,但是还存在很多问题,传入的参数类型不可能是任意的,所以要先进行类型判断。

3.1 参数是否为对象检测

也就是检测传入值是否是 对象,如果不是,那么直接返回原值就可以了。

我们新建一个文件夹utils来写一些工具函数

ts 复制代码
export function isObject(val: unkown) :val is Record<any,any> {
    return val !== null && typeof val === 'object'
}

reactive函数中就多了一步处理

ts 复制代码
import { track, trigger } from "./effect";
import { isObject } from "./utils";

export function reactive<T extends object>(target: T): T;
export function reactive(target: object) {
    // 传入类型判断是否为对象,如果不是则直接返回原值
    if(!isObject) {
        return target
    }
    const proxy = new Proxy(target, {
        get(target, key) {
            // 依赖收集
            track(target, key)
            const res = Reflect.get(target, key)
            return res
        },
        set(target, key, val) {
            // 派发更新
            tigger(target, key, val)
            const res = Reflect.set(target, key, val)
            return res
        }
    })
    return proxy
}

我们在vue3中如果一个对象 被代理了多次,每次的返回值都是全等的,这样做的好处是:

  1. 避免重复代理消耗内存
  2. 防止依赖收集混乱
  3. 不同proxy的状态可能会不一致
ts 复制代码
const proxyObj1 = reactive(obj)
const proxyObj2 = reactive(obj)
console.log(proxyObj1 === proxyObj2) // true

所以在我们写的代码中也要加上这个判断

3.2 检测对象是否被代理过,如果代理过就直接返回代理过的数据

我们可以用一个Map来维护已经代理过数据的状态,为了性能问题可以使用WeakMap

reactive函数中就又多了一步处理

ts 复制代码
import { track, trigger } from "./effect";
import { isObject } from "./utils";

const proxyMap = new WeakMap()

export function reactive<T extends object>(target: T): T;
export function reactive(target: object) {
    // 传入类型判断是否为对象,如果不是则直接返回原值
    if(!isObject) {
        return target
    }
    // 如果同一个对象已经被代理过了  就不需要再重复代理了
    if(proxyMap.get(target)) {
        retrun proxyMap.get(target)
    }
    const proxy = new Proxy(target, {
        get(target, key) {
            // 依赖收集
            track(target, key)
            const res = Reflect.get(target, key)
            return res
        },
        set(target, key, val) {
            // 派发更新
            tigger(target, key, val)
            const res = Reflect.set(target, key, val)
            return res
        }
    })
    // 收集代理过的对象
    proxyMap.set(target, proxy)
    return proxy
}

3.3 判断传入的是否是代理过的proxy对象

如果传入的是代理过的proxy对象,那么就不需要再嵌套代理了,直接返回这个对象就可以。我们可以在get读取属性时,为代理过的对象加一个标识__v_isReactive 如果有这个属性,那么表明现在传入的就是响应式对象了。我们用一个enum来维护这个值,并且加上判断

reactive函数中就又又多了一步处理

ts 复制代码
import { track, trigger } from "./effect";
import { isObject } from "./utils";

export const enum ReactiveFlags {
    IS_REACTIVE = "__v_isReactive",
}

const proxyMap = new WeakMap()

export function reactive<T extends object>(target: T): T;
export function reactive(target: object) {
    // 传入类型判断是否为对象,如果不是则直接返回原值
    if(!isObject) {
        return target
    }
    
    // 如果同一个对象已经被代理过了  就不需要再重复代理了
    if(proxyMap.get(target)) {
        retrun proxyMap.get(target)
    }

    // 判断是不是 响应式对象 如果是直接返回
    if (target[ReactiveFlags.IS_REACTIVE]) {
        return target;
    }
    
    const proxy = new Proxy(target, {
        get(target, key) {
            // 依赖收集
            track(target, key)
            const res = Reflect.get(target, key)
            return res
        },
        set(target, key, val) {
            // 派发更新
            tigger(target, key, val)
            const res = Reflect.set(target, key, val)
            return res
        }
    })
    // 收集代理过的对象
    proxyMap.set(target, proxy)
    return proxy
}

打印测试

ts 复制代码
const proxyObj1 = reactive(obj)
const proxyObj2 = reactive(proxyObj1)
console.log(proxyObj1 === proxyObj2) // true

3.4 递归代理对象

我们能看到 原始对象中又有一个对象属性 grade 那么这个对象里的属性 能不能进行依赖收集呢,我们来尝试一下

ts 复制代码
console.log(proxyObj.grade.math) // 在收集依赖中 grade 属性被读取

我们发现,只有第一层的对象属性可以进行依赖收集,所以我们需要再修改一下reactive函数

ts 复制代码
import { track, trigger } from "./effect";
import { isObject } from "./utils";

export const enum ReactiveFlags {
    IS_REACTIVE = "__v_isReactive",
}

const proxyMap = new WeakMap()

export function reactive<T extends object>(target: T): T;
export function reactive(target: object) {
    // 传入类型判断是否为对象,如果不是则直接返回原值
    if(!isObject) {
        return target
    }
    
    // 如果同一个对象已经被代理过了  就不需要再重复代理了
    if(proxyMap.get(target)) {
        retrun proxyMap.get(target)
    }

    // 判断是不是 响应式对象 如果是直接返回
    if (target[ReactiveFlags.IS_REACTIVE]) {
        return target;
    }
    
    const proxy = new Proxy(target, {
        get(target, key) {
            // 依赖收集
            track(target, key)
            const res = Reflect.get(target, key)
            // 如果读取到的属性是对象 那么就进行递归代理
            if(isObject(res)) {
                return reactive(res)
            }
            return res
        },
        set(target, key, val) {
            // 派发更新
            tigger(target, key, val)
            const res = Reflect.set(target, key, val)
            return res
        }
    })
    // 收集代理过的对象
    proxyMap.set(target, proxy)
    return proxy
}

3.5 原始对象中getter中用this读取本身值无法被依赖收集问题

我们发现,grade 中有一项 totalSorce 他的成绩是 mathenglish 的加和,按理说当我们读取totalSorce属性时,由于也用到了mathenglish 这两个属性也应该被依赖收集。但是结果却并没有获取到。

其实proxy 的get和set中都有一个receiver参数,这个参数就相当于是代理后的对象,可以理解为当前对象的this,我们只需要把这个参数传递下去就可以了

ts 复制代码
import { track, trigger } from "./effect";
import { isObject } from "./utils";

export const enum ReactiveFlags {
    IS_REACTIVE = "__v_isReactive",
}

const proxyMap = new WeakMap()

export function reactive<T extends object>(target: T): T;
export function reactive(target: object) {
    // 传入类型判断是否为对象,如果不是则直接返回原值
    if(!isObject) {
        return target
    }
    
    // 如果同一个对象已经被代理过了  就不需要再重复代理了
    if(proxyMap.get(target)) {
        retrun proxyMap.get(target)
    }

    // 判断是不是 响应式对象 如果是直接返回
    if (target[ReactiveFlags.IS_REACTIVE]) {
        return target;
    }
    
    const proxy = new Proxy(target, {
        get(target, key, receiver) {
            // 依赖收集
            track(target, key)
            const res = Reflect.get(target, key, receiver)
            // 如果读取到的属性是对象 那么就进行递归代理
            if(isObject(res)) {
                return reactive(res)
            }
            return res
        },
        set(target, key, val, receiver) {
            // 派发更新
            tigger(target, key, val)
            const res = Reflect.set(target, key, val, receiver)
            return res
        }
    })
    // 收集代理过的对象
    proxyMap.set(target, proxy)
    return proxy
}

此时再打印 totalSorce 的值我们会发现,依赖收集成功了!

3.6 对于其他读取属性值动作的监听

如果我们想判断一个属性是不是在响应式对象里,那是不是也是读取了响应式对象的属性呢?但是仅仅靠我们上面的代码 还是不足以劫持这种 in 操作

ts 复制代码
const proxyObj1 = reactive(obj)
console.log('name' in proxyObj1) // 没有触发依赖收集

不过在proxy中 提供了多种数据劫持的方法,比如has就可以获取到我们上面的操作。

那么只需要在处理函数中加一个has就可以了,但是如果每增加一个处理函数,就要在reactive函数中加一段代码,整个文件会显得十分冗杂,我们把这些函数的定义抽离出来,新建一个文件 baseHandlers.ts

ts 复制代码
import { reactive } from "./reactive";
import { ReactiveFlags } from "./reactive";
import { isObject } from "./utils";
import { track, trigger } from "./effect";

function get(target: object, key: string | symbol, receiver: object): any {
  // 用于判断是不是代理后的对象 打标识
  if (key === ReactiveFlags.IS_REACTIVE) {
    return true;
  }
  track(target, key);
  const res = Reflect.get(target, key, receiver);
  // 如果读取的值是对象,在递归代理
  if (isObject(res)) {
    return reactive(res);
  }
  return res;
}
function set(
  target: object,
  key: string | symbol,
  value: unknown,
  receiver: object
): boolean {
  trigger(target, key);
  return Reflect.set(target, key, value, receiver);
}
function has(target: object, key: string | symbol): boolean {
  track(target, key);
  return Reflect.has(target, key);
}

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

再修改一下reactive.ts

ts 复制代码
import { isObject } from "./utils";
import { mutableHandlers } from "./baseHandlers";

export const enum ReactiveFlags {
    IS_REACTIVE = "__v_isReactive",
}

const proxyMap = new WeakMap()

export function reactive<T extends object>(target: T): T;
export function reactive(target: object) {
    // 传入类型判断是否为对象,如果不是则直接返回原值
    if(!isObject) {
        return target
    }
    
    // 如果同一个对象已经被代理过了  就不需要再重复代理了
    if(proxyMap.get(target)) {
        retrun proxyMap.get(target)
    }

    // 判断是不是 响应式对象 如果是直接返回
    if (target[ReactiveFlags.IS_REACTIVE]) {
        return target;
    }
    
    // vue3的核心 通过 proxy 代理对象
    const proxy = new Proxy(target, mutableHandlers);
    
    // 收集代理过的对象
    proxyMap.set(target, proxy)
    return proxy
}

现在我们也可以收集 in 操作符的依赖了

ts 复制代码
const proxyObj = reactive(obj);
console.log("age" in proxyObj);
// 在收集依赖中 age属性被读取
// true

3.7 完善和区分其他数据劫持的方法

对于对象的操作,还有新增属性,删除属性以及遍历属性等。对于这些繁多的操作,我们需要在依赖收集和派发更新中 具体到是哪个操作触发了 我们这两个函数 新增一个operations.ts

ts 复制代码
export const enum TrackOpTypes {
  GET = "get",
  HAS = "has",
  ITERATE = "iterate",
}

export const enum TriggerOpTypes {
  SET = "set",
  ADD = "add",
  DELETE = "delete",
}

同时需要对trigger的条件做一些判断,比如是SET值还是ADD值?如果修改的值和原来的值相等我们就不需要触发更新了?

utils.ts中新增 hasChanged函数用来判断新旧值是否相同

ts 复制代码
export const hasChanged = (oldVal: any, val: any) => {
  // 不用 === 要考虑到 NaN === NaN 和 +0 === -0 的问题
  return !Object.is(oldVal, val);
};

baseHandlers.ts

ts 复制代码
import { reactive } from "./reactive";
import { ReactiveFlags } from "./reactive";
import { isObject, hasChanged } from "./utils";
import { track, trigger } from "./effect";
import { TrackOpTypes, TriggerOpTypes } from "./operations";

// 迭代的时候获取不到具体的key值,我们自定义一个symbol来标识迭代器
const ITERATE_KEY = Symbol("iterate");
function get(target: object, key: string | symbol, receiver: object): any {
  // 用于判断是不是代理后的对象 打标识
  if (key === ReactiveFlags.IS_REACTIVE) {
    return true;
  }
  track(target, TrackOpTypes.GET, key);
  const res = Reflect.get(target, key, receiver);
  // 如果读取的值是对象,在递归代理
  if (isObject(res)) {
    return reactive(res);
  }
  return res;
}

// 要判断是否有这个属性,区别修改还是新增,并且要判断前后更改的值是否相同
function set(
  target: Record<string | symbol, any>,
  key: string | symbol,
  value: unknown,
  receiver: object
): boolean {
  // 判断是否有这个属性
  const hadKey = target.hasOwnProperty(key);
  const oldVal = target[key];
  // 如果没有这个属性,说明是新增操作
  if (!hadKey) {
    trigger(target, TriggerOpTypes.ADD, key);
    // 如果有这个属性,并且确实改变了前后的值 那么派发更新
  } else if (hasChanged(oldVal, value)) {
    trigger(target, TriggerOpTypes.SET, key);
  }
  return Reflect.set(target, key, value, receiver);
}
function has(target: object, key: string | symbol): boolean {
  track(target, TrackOpTypes.HAS, key);
  return Reflect.has(target, key);
}
function ownKeys(target: object): (string | symbol)[] {
  track(target, TrackOpTypes.ITERATE, ITERATE_KEY);
  return Reflect.ownKeys(target);
}
// 删除的时候要判断本身对象中有没有这个属性,有才触发更新
function deleteProperty(target: object, key: string | symbol) {
  // 判断对象是否有这个属性
  const hadKey = target.hasOwnProperty(key);
  // 并且判断删除是否成功
  const res = Reflect.deleteProperty(target, key);
  if (hadKey && res) {
    trigger(target, TriggerOpTypes.DELETE, key);
  }
  return res;
}
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  has,
  ownKeys,
  deleteProperty,
};

让我们来测试一下,大功告成

小结

以上关于对象相关的操作的依赖收集,已经定义好了。接下来是对于数组的处理 和 依赖收集以及派发更新具体做了什么事情

相关推荐
优秀稳妥的JiaJi23 分钟前
用 Mousetrap 优雅地管理快捷键:从代码到爱情故事
前端·vue.js·前端框架
刺客_Andy33 分钟前
React Vue 项开发中组件封装原则及注意事项
前端·vue.js·react.js
邵柏涛40 分钟前
最新Vue3 Vite安装Tailwind CSS 4.0 教程
前端·vue.js
chengxue_12342 分钟前
在项目中写一个购物车功能
前端·vue.js·html
fridayCodeFly2 小时前
:class=“{ ‘addCheckstyle‘: hasError }“这是什么意思
前端·javascript·vue.js
有诺千金3 小时前
使用AI一步一步实现若依(21)
vue.js
苏琢玉4 小时前
一个PHPer的偷懒哲学:如何用两套模板跳过重复造轮子
vue.js·php
PLJJ6854 小时前
Vue.js 应用的入口文件main.js
前端·javascript·vue.js
fridayCodeFly4 小时前
vue里localStorage可以直接用吗
前端·javascript·vue.js
念九_ysl5 小时前
Vue 3 打包优化实战指南:从构建到部署的全链路性能提升
前端·javascript·vue.js