Reactive
一般而言,数值、布尔值、字符串、数组变量的响应式使用ref
,而对象的响应式使用reactive
,可以深度检测内部对象的变更
实现分析
通过reactive
双向绑定的vue文件样例,我们分析一下需要实现的功能特性
<template>
<input v-model:value="form.username" type="text" name="username" />
<input v-model:value="form.password" type="password" name="password" />
</template>
<script setup>
import { reactive, watchEffect } from 'vue'
const form = reactive({
username: '',
password: ''
})
watchEffect(() => {
console.log(form.username)
console.log(form.password)
})
</script>
从上述样例可以看出,reactive
作为函数,参数为对象进行调用,返回一个变量,该变量仍然具有对象的特征,且需要具有响应式的特征,那么在取值和赋值的时候就都需要进行监听介入,我们无法使用上一个篇章Ref
一样使用class
,因为对象里面的键值对是不可知,我们使用Ref
需要监听每个key
也是不可取的。
ECMAScript 6
提供了一个新的特性 Proxy ,Proxy
代理可以劫持对象,对目标对象的访问都需要经过一层“拦截”,我们可以使用代理来实现响应式的功能。
那么我们可以总结需要实现的reactive
特性如下:
reactive
函数包裹的参数为对象- 使用
Proxy
代理劫持对象,分别在get
和set
的时候进行拦截
vue2和vue3对比
vue2
使用Object.defineProperty
进行劫持,vue3
使用Proxy
,两者有性能上和作用范围上的差异,在后续章节会进行对比
Proxy代理
reactive
的核心是创建Proxy
,关键实现应该在于set
和get
的拦截
1. 创建Proxy
// 创建reactive对象,返回Proxy
export const reactive = (target) => {
return createReactiveObject(target)
}
// 创建Proxy
export const createReactiveObject = (target) => {
return new Proxy(target, mutableHandlers)
}
mutableHandlers
为get
和set
的实现,在后续步骤实现
2. 缓存目标对象
为了避免目标对象重复套用reactive
,进行不必要的计算,使用 WeakMap 缓存该对象得到的 Proxy
代理,在下一次调用reactive
函数时可以直接返回。
const targetMap = new WeakMap()
export const reactive = (target) => {
return createReactiveObject(target)
}
export const createReactiveObject = (target) => {
const existPrpxy = targetMap.get(target)
// 判断是否已经缓存
if (existPrpxy) {
return existPrpxy
}
const proxy = new Proxy(target, mutableHandlers)
// 缓存对象的代理
targetMap.set(target, proxy)
return proxy
}
3. 劫持对象
与Ref
相似,我们主要在获取数据时收集依赖,在改变数据时触发依赖,那么baseHandlers
主要涉及get
和set
两种操作
export const mutableHandlers = {
get,
set
}
4. 拦截get
Proxy
的get
的初始获取是以下方式:
export const get = (target, key, receiver) {
return Reflect.get(target, key, receiver)
}
我们需要在获取之前进行依赖收集
export const get = (target, key, receiver) {
// 依赖收集
track(target, key, 'get')
return Reflect.get(target, key, receiver)
}
其中,track
函数就是进行依赖收集,在后面章节我们会具体实现
5. 拦截set
Proxy
的get
的初始赋值是以下方式:
export const set = (target, key, value, receiver) {
return Reflect.set(target, key, value, receiver)
}
我们需要在赋值后进行依赖触发(通知订阅者)
export const set = (target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
// 触发依赖
trigger(target, key, 'set')
return result
}
其中,trigger
函数就是进行依赖触发,在后面章节我们会具体实现
依赖收集和触发
依赖收集和触发就是实现track
和trigger
函数,其具体实现需要与ref
有差异性,因为reactive
的的参数是对象,是一个有键值对的数据结构,我们获取一个对象里的一个值是通过key
去获取的,其他的key
可能不需要进行监听以免浪费不必要的内存空间和计算能力,所以我们需要对key
进行依赖收集
1. 依赖收集
首先,对target
进行缓存,以避免多次对该目标对象取值时重复计算
const targetMap = new WeakMap()
export const track = (target, key, type) => {
// 判断是否需要收集
if (!isTracking()) {
return;
}
// 获取target对应的依赖关系Map
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
}
TIP
使用targetMap
这个WeakMap
将target
缓存起来,其中depsMap
就是target
相对应的依赖Map关系
接着,我们来实现key
对应的依赖关系
const targetMap = new WeakMap()
// 创建依赖集合(订阅者集合)
export const createDep = (effects?: any) => {
return new Set(effects);
}
export const track = (target, key, type) => {
// 判断是否需要收集
if (!isTracking()) {
return;
}
// 获取target对应的依赖关系Map
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 获取key对应的依赖关系Set
let dep = depsMap.get(key)
if (!dep) {
dep = createDep()
depsMap.set(key, dep);
}
}
我们可以看到dep
变量就是key
对应的依赖集合(订阅者集合),createDep
是创建Set
类型依赖集合的函数
最后就是正式将Effect
添加进该key
的依赖集合中,我们使用在依赖添加中的trackEffect
函数实现
const targetMap = new WeakMap()
// 创建依赖集合(订阅者集合)
export const createDep = (effects?: any) => {
return new Set(effects);
}
export const track = (target, key, type) => {
// 判断是否需要收集
if (!isTracking()) {
return;
}
// 获取target对应的依赖关系Map
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 获取key对应的依赖关系Set
let dep = depsMap.get(key)
if (!dep) {
dep = createDep()
depsMap.set(key, dep);
}
trackEffects(dep);
}
2. 依赖触发
依赖触发跟依赖收集的步骤相似,使用的是依赖通知中的triggerEffect
函数实现,下面是实现的代码:
// 触发reactive依赖
export const trigger = (target, key, type) => {
const depsMap = targetMap.get(target)
if (!depsMap) {
return;
}
const depSet = depsMap.get(key);
if (!depSet) {
return;
}
const deps = [...depSet];
triggerEffects(createDep(deps));
}
进阶
我们的代码都是基于reative
特性而写,但其实readonly
和shallowReadonly
都跟reative
原理相近,我们来优化一下我们的代码风格,使其在后续篇章能够被复用
以下是代码的优化点:
1. createReactiveObject优化
export function createReactiveObject (target, proxyMap, baseHandlers) {
const existPrpxy = proxyMap.get(target)
if (existPrpxy) {
return existPrpxy;
}
const proxy = new Proxy(target, baseHandlers)
proxyMap.set(target, proxy)
return proxy
}
createReactiveObject
参数添加proxyMap
和baseHandlers
,分别代表target
的代理缓存和代理的劫持操作,这样后期支持readonly
和shallowReadonly
仅需要改变对应的传参即可,那么相应reactive
调用的时候需要更改为:
export function reactive (target) {
return createReactiveObject(target, targetMap, mutableHandlers);
}
2. get逻辑抽取
我们在mutableHandlers
直接使用get
和set
两个拦截操作,get
可以被readonly
和shallowReadonly
复用,所以让我们来改造一下
export const createGetter = (readonly = false, shallow = false) => {
return (target, key, receiver) => {
track(target, key, 'get')
return Reflect.get(target, key, receiver);
}
}
// reavtive
const get = createGetter()
export const mutableHandlers = {
get,
set
}
上面的代码将get
相同逻辑抽取到createGetter
中,通过传参的方式来区分不同的拦截类型
3. get拦截优化
在获取reactive
返回的proxy
时,如果要获取其原对象,和判断是否为reactive
数据,我们需要对某些特定的key
进行单独的处理返回,这里我们选用_v_raw_
和_v_isReactive
两个key
来进行单独处理
// 枚举
export enum ReactiveFlags {
RAW = '_v_raw_',
IS_REACTIVE = '_v_isReactive'
}
export const createGetter = (readonly = false, shallow = false) => {
return (target, key, receiver) => {
// 获取原对象
if (key === ReactiveFlags.RAW && targetMap.get(target) === receiver) {
return target
}
// 获取是否为reactive对象
if (key === ReactiveFlags.IS_REACTIVE) {
return true
}
track(target, key, 'get')
return Reflect.get(target, key, receiver);
}
}
同时,reactive
是深层次的,所以如果其里面的值是对象的话,也需要进行reactive
处理:
// 枚举
export enum ReactiveFlags {
RAW = '_v_raw_',
IS_REACTIVE = '_v_isReactive'
}
export const createGetter = (readonly = false, shallow = false) => {
return (target, key, receiver) => {
// 获取原对象
if (key === ReactiveFlags.RAW && targetMap.get(target) === receiver) {
return target
}
// 获取是否为reactive对象
if (key === ReactiveFlags.IS_REACTIVE) {
return true
}
const result = Reflect.get(target, key, receiver);
track(target, key, 'get')
if (isObject(result)) {
return reactive(result)
}
return result
}
}
单元测试
describe("reactive", () => {
it('base function', () => {
const form = reactive({
username: 'hello',
password: 'world'
})
form.username = `${form.username} ${form.password}`
expect(form.username).toBe('hello world')
}),
it('reactibility', () => {
const form = reactive({
count: 1,
})
let calls = 0;
let sum = 0
effect(() => {
sum += form.count
calls++;
})
expect(calls).toBe(1);
form.count += 1;
expect(calls).toBe(2);
expect(form.count).toBe(2);
expect(sum).toBe(3)
}),
it('deep reactibility', () => {
const form = reactive({
inner: {
count: 1
}
})
let sum = 0
let calls = 0
effect(() => {
sum += form.inner.count ;
calls++
})
expect(form.inner.count).toBe(1)
form.inner.count += 1;
expect(form.inner.count).toBe(2)
expect(calls).toBe(2)
expect(sum).toBe(3)
})
})