Vue3的响应数据简易实现-Composition API(reactive, ref, toRef...)

480 阅读2分钟

前言

  • vue3的reactivity源码地址
  • reactivity 响应式系统
  • 实现的composition API有:
  • reactive, shallowReactive, shallowReadonly, readonly
  • ref, shallowRef
  • toRef, toRefs
  • effect:
  • reactivity内部方法, 不暴露在外面
  • 当数据变化时去执行effect函数

shared

// 是不是个对象
export const isObject = (value) => typeof value == 'object' && value !== null
// 合并对象
export const extend = Object.assign
// 是不是数组
export const isArray = Array.isArray
// 是不是函数
export const isFunction = (value) => typeof value == 'function'
// 是不是数字
export const isNumber = (value) => typeof value == 'number'
// 是不是字符
export const isString = (value) => typeof value === 'string'
// 是不是正整数
export const isIntegerKey = (key) => parseInt(key) + '' === key


// 是不是自己的属性
let hasOwnpRroperty = Object.prototype.hasOwnProperty
export const hasOwn = (target, key) => hasOwnpRroperty.call(target, key)

// 是不是同一个值
export const hasChanged = (oldValue,value) => oldValue !== value

reactive

  • reactive: 对数据进行Proxy代理(重点)
  • shallowReactive: 对数据进行浅代理(就是只关注第一层)
  • shallowReadonly: 对数据进行浅代理, 并且只能读取, 无法修改(不收集track)
  • readonly: 只能读取, 无法修改(不收集track)
  • 大致流程(以下面例子为准):
  • 执行effect函数
  • state.arr.length state.son.name state.arr[3]会走proxy的get
  • 通过track收集当前的effect函数
  • 当数据发生变化时会走proxy的set
  • 通过trigger会去再次执行当前的effect函数
  • 只有在effect传递的函数中有的变量 才会去收集effect函数(相当于vue2中的watcher)

示例

<div id="app"></div>
<script src="../node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
<script>
  let { effect, reactive } = VueReactivity
  let state = reactive({
    name: 'tom',
    age: 38,
    son: {name: 'Bob', age: 18},
    arr: [1,2,3,4,5]
  })

  effect(() => {
      // app.innerHTML = state.name + state.name
      app.innerHTML = `${state.arr.length}-${state.son.name}-${state.arr[3]}`
  })

  setTimeout(() => {
    // state.arr.push(100)
    state.son.name = 'Pretty'
    state.arr.length = 1
  }, 2000)

</script>
+---------------------+    +----------------------+
|                     |    |                      |
|       5-Bob-4       +--->|  1-Pretty-undefined  +
|                     |    |                      |
+---------------------+    +----------------------+

reactive.ts

import { isObject } from "@vue/shared/src"
import { mutableHandlers, shallowReactiveHandlers, readonlyHandlers, shallowReadonlyHandlers } from './baseHandlers'

/**
 * @description 拦截数据
 */
export function reactive(target) {
  return createReactiveObject(target, false, mutableHandlers)
}

/**
 * @description 拦截第一层数据(浅响应)
 */
export function shallowReactive(target) {
  return createReactiveObject(target, false, shallowReactiveHandlers)
}

/**
 * @description 只读数据
 */
export function readonly(target) {
  return createReactiveObject(target, true, readonlyHandlers)
}

/**
 * @description 浅的只读数据
 */
export function shallowReadonly(target){
  return createReactiveObject(target, true, shallowReadonlyHandlers)
}

// 创建WeakMap, 响应还是只读
// 方便查找, 存储的key必须是对象
// 会自动垃圾回收, 不会造成内存泄漏
const reactiveMap = new WeakMap()
const readonlyMap = new WeakMap()

/**
 * @description 创建数据代理
 * @param target        要拦截的目标(数组或者对象)
 * @param isReadonly    是不是只读
 * @param baseHandlers  Proxy第二个参数对象
 */
export function createReactiveObject(target, isReadonly, baseHandlers) {
  if (!isObject) { return target }

  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  const existProxy = proxyMap.get(target)
  if (existProxy) { return existProxy }

  const proxy = new Proxy(target, baseHandlers)
  proxyMap.set(target, proxy)

  return proxy
}

operators.ts

/**
 * @description 收集effect时 枚举
 */
export const enum TrackOpTypes {
  GET
}

/**
 * @description 发布effect时 枚举
 */
export const enum TriggerOrTypes {
  ADD,
  SET
}

baseHandlers.ts

import { extend, hasChanged, hasOwn, isArray, isIntegerKey, isObject } from "@vue/shared/src"
import { track, trigger } from "./effect"
import { TrackOpTypes, TriggerOrTypes } from "./operators"
import { reactive, readonly } from "./reactive"

/**
 * @description 创建get
 * @param isReadonly  是不是只读
 * @param shallow     是不是浅拦截
 */
function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    // Reflect.get 具备返回值(target[key])
    // 目标值类型不是Object 则抛出一个TypeError
    const res = Reflect.get(target, key, receiver)

    if (!isReadonly) {
      // 收集effect
      track(target, TrackOpTypes.GET, key)
      
    }

    if (shallow) { return res }

    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

/**
 * @description 创建set 只针对不是只读的数据
 * @param shallow 是不是浅拦截
 */
function createSetter(shallow = false) {
  return function set(target, key, value, receiver) {
    const oldValue = target[key]
    
    let hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key)
    
    // Reflect.set 具备返回值(boolean)
    const result = Reflect.set(target, key, value, receiver)

    if (!hadKey) {
      // 新增
      trigger(target, TriggerOrTypes.ADD, key, value)
    } else if(hasChanged(oldValue,value)) {
      // 修改
      trigger(target, TriggerOrTypes.SET, key, value, oldValue)
    }

    return result
  }
}

const get = createGetter()
const shallowGet = createGetter(false, true)
const readonlyGet = createGetter(true)
const showllowReadonlyGet = createGetter(true, true)

const set = createSetter()
const shallowSet = createSetter(true)

export const mutableHandlers = {
  get,
  set
}

export const shallowReactiveHandlers = {
  get: shallowGet,
  set: shallowSet
}

// 只读 set时发出警告
let readonlyObj = {
  set: (target, key) => {
      console.warn(`set on key ${key} falied, 当前是只读属性`)
  }
}

export const readonlyHandlers = extend({
  get: readonlyGet,
}, readonlyObj)

export const shallowReadonlyHandlers = extend({
  get: showllowReadonlyGet,
}, readonlyObj)

effect.ts

当effect传入的函数放入全部的数组 如 app.innerHTML = state.arr 当前targetMap.get(state.arr)会收集全部的索引 length join toString 调用push, pop, splice等方法时 会触发数组的长度 如 state.arr.push(100) 源码做了限制, 当调用方法时 限制让length收集, 这里只写核心流程

import { isArray, isIntegerKey } from "@vue/shared/src"
import { TriggerOrTypes } from "./operators"

/**
 * @description uid           effect唯一标识
 * @description activeEffect  指向当前的effect
 * @description effectStack   存储effect的栈, 结束一个去除一个
 */
let uid = 0
let activeEffect;
const effectStack = []

function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
    // 判断是为了防止 如: fn里 -> state.age++ 不停的更新 造成死循环 
    if (!effectStack.includes(effect)) {
      try {
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
      }

    }
  }

  effect.id = uid++
  effect._isEffect = true   // 标识是响应式的effect
  effect.raw = fn           // 传入的原始函数
  effect.options = options

  return effect
}

/**
 * @description   数据在effect传的函数中调用 当数据发生变化trigger会再次发布
 * @param fn      传的函数
 * @param options 选项 如是不是懒的(computed 默认不执行)
 */
export function effect(fn, options: any = {}) {
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }

  return effect
}


/**
 * @description effect存储的变量 track收集的effect
 * @description targetMap 结构
 * {name: 'xxx', age: 000} => {
 *    name => [effect effect],
 *    age  => [effect effect]
 * },
 * [1,2,3] => {
 *    1 => [effect, efffect]
 * }
 */
const targetMap = new WeakMap()

/**
 * @description 让某个对象中的属性 收集当前对应的effect函数
 * @description 只有在执行effect时, 并且在effect里的变量才会去收集
 */
export function track(target, type, key) {
  // activeEffect 指向当前执行的effect
  if (activeEffect === undefined) { return }
  
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map))
  }
  
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set))
  }
  
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
  }

}


/**
 * @description 寻找属性对应的effect 让其执行(这里只有数组和对象)
 * @param target    目标
 * @param type      类型 新增 或者 修改
 * @param key       key
 * @param newValue  新值
 * @param oldValue  老值
 */
export function trigger(target, type, key?, newValue?, oldValue?) {

  const depsMap = targetMap.get(target)
  if (!depsMap) return
  
  // 要发布的effect去重
  // 将所有的要执行的effect 全部存到一个新的集合中 最终一起执行
  const effects = new Set()
  const add = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => effects.add(effect))
    }
  }

  // 数组 修改长度 traget.length = newValue
  // 如 [1,2,3,4,5] => [1,2,3,4,5].length = 1
  if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key > newValue) {
        add(dep)
      }
    })
  } else {
    // 可能是对象
    // 这里肯定是修改 不能是新增
    // 如果是新增 depsMap.get(key) -> undefined
    if (key !== undefined) {
      add(depsMap.get(key))
    }

    // 如果修改数组中的某个索引
    // 如 arr=[1,2,3] -> arr[100]=1
    switch (type) {
      case TriggerOrTypes.ADD:
        if (isArray(target) && isIntegerKey(key)) {
          add(depsMap.get('length'))
        }
        break;
    }
  }

  // 发布
  effects.forEach((effect: any) => effect())

}

ref 和 toRef

ref

  • ref大部分情况下 只针对单一变量let name = ref('tom') 其实内部用的是Object.defineProperty(这里用的是class类属性访问器get和set)
  • 如果let state = ref({})是对象, 将会去用reactive({})API
  • shallowRef 只做第一层的Object.defineProperty, 不会reactive({})

示例

<script src="../node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
<div id="app"></div>
<script>
  const { ref, shallowRef, effect } = VueReactivity
  let name = ref('Tom')
  // let state = ref({a: 'a1', b: 'b1'})

  effect(() => {
      app.innerHTML = name.value
      // app.innerHTML = state.value.a
  })

  setTimeout(() => {
      name.value = 'Bob'
      // state.value.a = 'a2'
  }, 1000)
</script>
+---------------------+    +----------------------+
|                     |    |                      |
|         Tom         +--->|          Bob         +
|                     |    |                      |
+---------------------+    +----------------------+

toRef

  • 调用proxy的变量, 做了一层代理
  • toRefs只是循环调用toRef, 做了一层代理

示例

<script src="../node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
<div id="app"></div>
<script>
  const { effect, reactive, toRef, toRefs } = VueReactivity
  let proxy = reactive({name:'Tom', age: 100})
  // let r1 = toRef(proxy, 'name')
  // let r2 = toRef(proxy, 'age')
  const { name, age } = toRefs(proxy)
  effect(()=>{
      app.innerHTML = name.value + '-' + age.value
  })
  setTimeout(() => {
      proxy.name = 'Bob'
  }, 2000)
</script>
+---------------------+    +----------------------+
|                     |    |                      |
|       Tom-100       +--->|        Bob-100       +
|                     |    |                      |
+---------------------+    +----------------------+

ref.ts

import { hasChanged, isArray, isObject } from "@vue/shared/src"
import { track, trigger } from "./effect"
import { TrackOpTypes, TriggerOrTypes } from "./operators"
import { reactive } from "./reactive"

/**
 * @description ref shallowRef
 * @description reactive内部采用proxy ref中内部使用的是defineProperty
 */
export function ref(value) {
  return createRef(value)
}

export function shallowRef(value) {
  return createRef(value, true)
}

// 传进来的值 如果是对象 用reactive代理
const convert = (val) => isObject(val) ? reactive(val) : val

class RefImpl {
  public _value;
  public __v_isRef = true // 表示是一个ref属性
  constructor(public rawValue, public shallow) {
    this._value = shallow ? rawValue : convert(rawValue)
  }

  get value() {
    // 收集effect
    track(this, TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newValue) {
    if (hasChanged(newValue, this.rawValue)) {
      this.rawValue = newValue
      this._value = this.shallow ? newValue : convert(newValue)
      // 发布 effect
      trigger(this, TriggerOrTypes.SET, 'value', newValue)
    }
  }

}

function createRef(rawValue, shallow = false) {
  return new RefImpl(rawValue, shallow)
}

/**
 * @description toRef toRefs
 * @description 将一个对象转换成ref类型 就是做了一层代理
 */
class ObjectRefImpl {
  public __v_isRef = true
  constructor(public target, public key) {}

  // 如果原对象是响应式的就会track依赖收集
  get value(){
    return this.target[this.key]
  }

  // 如果原来对象是响应式的 就会trigger触发更新
  set value(newValue){
    this.target[this.key] = newValue
  }
}

export function toRef(target, key) {
  return new ObjectRefImpl(target, key)
}

export function toRefs(object) {
  const ret = isArray(object) ? new Array(object.length) : {}
  for (const key in object) {
    ret[key] = toRef(object, key)
  }

  return ret
}