从零实现一个vue3(二) 初见 reactivity

158 阅读5分钟

目标 实现一个

<body>
  <div id="app"></div>
</body>
<script>
  // 从 Vue 中结构出 reactie、effect 方法
  const { reactive, effect } = Vue

  // 声明响应式数据 obj
  const obj = reactive({
    name: '张三'
  })

  // 调用 effect 方法
  effect(() => {
    document.querySelector('#app').innerText = obj.name
  })

  // 定时修改数据,视图发生变化
  setTimeout(() => {
    obj.name = '李四'
  }, 2000)
</script>

大致过程就是

step1 使用proxy 创建实例, 完成基础reactive

创建 packages/shared/src/index.ts

/**
 * 判断是否为一个对象
 */
export const isObject = (val: unknown) =>
  val !== null && typeof val === 'object'

创建 packages/reactivity/src/reactive.ts

import { isObject } from '@vue/shared'
/**
 * 创建响应式数据
 * */ 
export function reactive(obj) {
  if (!isObject(obj)) return;
  return new Proxy(obj, {

  })
}

创建 packages/reactivity/src/index.ts

export { reactive } from './reactive';

创建 packages/vue/src/index.ts

export { reactive,  } from '@vue/reactivity';

对于 reactive 函数, 主要是通过proxy 来实现对数据的读写相应

/**
 * 创建响应式数据
 * */ 
import { isObject } from '@vue/shared'
/**
 * 创建响应式数据
 * */ 
export function reactive(obj) {
  if (!isObject(obj)) return;
  return new Proxy(obj, {
    get(target, key) {
      // 当访问proxy代理对象的属性时, 会执行get函数
      // 读
      console.log("进行读入了")
      return target[key]
    },
    set(target, key, value) {
      // 当设置proxy代理对象的属性时, 会执行set函数
      // 写
      target[key] = value;
      console.log("进行写入改变了")
      return true
    }
  })
}

然后 运行 npm run build 创建 dist文件

创建 example/reactivity/reactive-01.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <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">
      <p id="p1"></p>
      <p id="p2"></p>
    </div>
  </body>

  <script>
    const { reactive } = Vue

    const obj = reactive({
      name: '张三'
    })

    setTimeout(() => {
      obj.name = '李四'
    }, 2000)
    console.log(obj.name);
  </script>
</html>

实现最基本的 读写状态改变

step2 effect 触发副作用

创建packages/reactivity/src/effect.ts

原理就是 创建一个 副作用数组, 在 proxy读写的时候 将数组遍历执行

// 副作用函数数组
export const bucket = [] as Array<Function>;

// 只要有fn 就写入到 
export function effect<T = any>(fn: () => T) {
  // 先初始化执行一遍 
  fn();
  bucket.push(fn);
}

在 reactive函数中

import { bucket } from './effect';

/**
 * 创建响应式数据
 * */ 
export function reactive(obj) {
  ...
  return new Proxy(obj, {
   ...
   set(target, key, value) {
      // 当设置proxy代理对象的属性时, 会执行set函数
      // 写
      target[key] = value;
      bucket.forEach((fn) => fn())
      return true
    }
  }
}

在 packages/vue/src/index.ts

export { reactive,  effect } from '@vue/reactivity';

然后 运行 npm run build 创建 dist文件

创建 example/reactivity/reactive.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <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">
      <p id="p1"></p>
      <p id="p2"></p>
    </div>
  </body>

  <script>
    const { reactive, effect } = Vue

    const obj = reactive({
      name: '张三'
    })

    // 调用 effect 方法
    effect(() => {
      document.querySelector('#p1').innerText = obj.name
    })
    effect(() => {
      document.querySelector('#p2').innerText = obj.name
    })

    setTimeout(() => {
      obj.name = '李四'
    }, 2000)
  </script>
</html>

可以看到 可以 通过 effect 去触发 相应修改

step3 实现依赖收集

对effect 和 reactive 进行优化

前面 执行副作用的时候 我们没有区分 对 proxy 不同属性 执行不同的副作用函数, 现在换成 WeekMap类型 对 属性进行收集

effect 由上面数组 bucket类型 改为 实例ReactiveEffect Map类型

/**
 * 单例的,当前的 effect
 */
export let activeEffect: ReactiveEffect | undefined

/**
 * effect 函数
 * @param fn 执行方法
 * @returns 以 ReactiveEffect 实例为 this 的执行函数
 */
/**
 * effect 函数
 * @param fn 执行方法
 * @returns 以 ReactiveEffect 实例为 this 的执行函数
 */
export function effect<T = any>(fn: () => T) {
  // 生成 ReactiveEffect 实例
  const _effect = new ReactiveEffect(fn)
  // 执行 run 函数
  _effect.run()
}

/**
 * 响应性触发依赖时的执行类
 */
/**
 * 响应性触发依赖时的执行类
 */
export class ReactiveEffect<T = any> {
  constructor(public fn: () => T) {}

  run() {
    // 为 activeEffect 赋值
    activeEffect = this

    // 执行 fn 函数
    return this.fn()
  }
}

在 reactive中 触发 读写 使用 track 和 trigger

import { track, trigger } from './effect';

/**
 * 创建响应式数据
 * */ 
export function reactive(obj) {
  if (!isObject(obj)) return;
  return new Proxy(obj, {
    get(target, key) {
      // 当访问proxy代理对象的属性时, 会执行get函数
      // 读
      // 收集依赖
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      // 当设置proxy代理对象的属性时, 会执行set函数
      // 写
      target[key] = value;
      // 触发依赖
      trigger(target, key, value)
      return true
    }
  })
}

在 packages/reactivity/src/effect.ts 中

track

export type Dep = Set<ReactiveEffect>

/**
 * 依据 effects 生成 dep 实例
 */
export const createDep = (effects?: ReactiveEffect[]): Dep => {
  const dep = new Set<ReactiveEffect>(effects) as Dep
  return dep
}

type KeyToDepMap = Map<any, Dep>
/**
 * 单例的,当前的 effect
 */
export let activeEffect: ReactiveEffect | undefined

/**
 * 收集所有依赖的 WeakMap 实例:
 * 1. `key`:响应性对象
 * 2. `value`:`Map` 对象
 * 		1. `key`:响应性对象的指定属性
 * 		2. `value`:指定对象的指定属性的 执行函数
 */
const targetMap = new WeakMap<any, KeyToDepMap>()
/**
 * 用于收集依赖的方法
 * @param target WeakMap 的 key
 * @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
 */
export function track(target: object, key: unknown) {
  console.log('track: 收集依赖')
  // 如果当前不存在执行函数,则直接 return
  if (!activeEffect) return
  // 尝试从 targetMap 中,根据 target 获取 map
  let depsMap = targetMap.get(target)
  // 如果获取到的 map 不存在,则生成新的 map 对象,并把该对象赋值给对应的 value
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 获取指定 key 的 dep
  let dep = depsMap.get(key)
  //为指定 map,指定key 设置回调函数
  if (!dep) {
    depsMap.set(key, (dep = createDep()))
  }

  trackEffects(dep)

}

/**
 * 利用 dep 依次跟踪指定 key 的所有 effect
 * @param dep
 */
export function trackEffects(dep: Dep) {
  dep.add(activeEffect!)
}


trigger

/**
 * 触发依赖的方法
 * @param target WeakMap 的 key
 * @param key 代理对象的 key,当依赖被触发时,需要根据该 key 获取
 * @param newValue 指定 key 的最新值
 * @param oldValue 指定 key 的旧值
 */
export function trigger(target: object, key?: unknown, newValue?: unknown) {
  console.log('trigger: 触发依赖')
  // 依据 target 获取存储的 map 实例
  const depsMap = targetMap.get(target)
  // 如果 map 不存在,则直接 return
  if (!depsMap) {
    return
  }
  let dep: Dep | undefined = depsMap.get(key)
  // dep 不存在则直接 return
  if (!dep) {
    return
  }
  // 触发 dep
  triggerEffects(dep)
}

/**
 * 依次触发 dep 中保存的依赖
 */
export function triggerEffects(dep: Dep) {
  // 把 dep 构建为一个数组
  const effects = Array.isArray(dep) ? dep : [...dep]
  // 依次触发
  for (const effect of effects) {
    triggerEffect(effect)
  }
}

/**
 * 触发指定的依赖
 */
export function triggerEffect(effect: ReactiveEffect) {
  effect.run()
}

step4 对 reactive Proxy 实例 也形成 Map相关

原因 想对两个对象 都实现 proxy响应式

  const obj = reactive({
      name: '张三'
    })
   
  const obj2 = reactive({
    name: '王四'
  })

改造reactive, 执行reactive即为 生成Map实例

import { mutableHandlers } from './baseHandlers';
import { isObject } from '@vue/shared'

export const enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive'
}

/**
 * 响应性 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)
}

/**
* 创建响应性对象
* @param target 被代理对象
* @param baseHandlers handler
*/
function createReactiveObject(
  target: object,
  baseHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<object, any>
) {
  // 如果该实例已经被代理,则直接读取即可
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
      return existingProxy
  }
  if (!isObject(target)) return
  // 未被代理则生成 proxy 实例
  const proxy = new Proxy(target, baseHandlers)

  // 缓存代理对象
  proxyMap.set(target, proxy)
  return proxy
}

新增packages/reactivity/src/baseHandlers.ts

import { track, trigger } from './effect';
/**
 * getter 回调方法
 */
const get = createGetter()

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

/**
 * setter 回调方法
 */
const set = createSetter()

/**
 * 创建 setter 回调方法
 */
function createSetter() {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ) {
    // 利用 Reflect.set 设置新值
    const result = Reflect.set(target, key, value, receiver)
    // 触发依赖
    trigger(target, key, value)
    return result
  }
}

/**
 * 响应式的handler 
 * */ 
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set
}

以上就实现了 一个 基本的Proxy

参考文章

vue3 源码学习,实现一个 mini-vue(二):初见 reactivity 模块

【杰哥课堂】Vue3.2源码设计与实现-响应式原理

Vue3 设计与实现