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提供了一个新的特性 Proxyopen in new window ,Proxy代理可以劫持对象,对目标对象的访问都需要经过一层“拦截”,我们可以使用代理来实现响应式的功能。

那么我们可以总结需要实现的reactive特性如下:

  • reactive函数包裹的参数为对象
  • 使用Proxy代理劫持对象,分别在getset的时候进行拦截
vue2和vue3对比

vue2使用Object.defineProperty进行劫持,vue3使用Proxy,两者有性能上和作用范围上的差异,在后续章节会进行对比

Proxy代理

reactive的核心是创建Proxy,关键实现应该在于setget的拦截

1. 创建Proxy

// 创建reactive对象,返回Proxy
export const reactive = (target) => {
  return createReactiveObject(target)
}

// 创建Proxy
export const createReactiveObject = (target) => {
  return new Proxy(target, mutableHandlers)
}

mutableHandlersgetset的实现,在后续步骤实现

2. 缓存目标对象

为了避免目标对象重复套用reactive,进行不必要的计算,使用 WeakMapopen in new window 缓存该对象得到的 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主要涉及getset两种操作

export const mutableHandlers = {
  get,
  set
}

4. 拦截get

Proxyget的初始获取是以下方式:

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

Proxyget的初始赋值是以下方式:

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函数就是进行依赖触发,在后面章节我们会具体实现

依赖收集和触发

依赖收集和触发就是实现tracktrigger函数,其具体实现需要与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这个WeakMaptarget缓存起来,其中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特性而写,但其实readonlyshallowReadonly都跟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参数添加proxyMapbaseHandlers,分别代表target的代理缓存和代理的劫持操作,这样后期支持readonlyshallowReadonly仅需要改变对应的传参即可,那么相应reactive调用的时候需要更改为:

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

2. get逻辑抽取

我们在mutableHandlers直接使用getset两个拦截操作,get可以被readonlyshallowReadonly复用,所以让我们来改造一下

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)
  })
})