关于javascript:从零实现Vue3的响应式库1

32次阅读

共计 5677 个字符,预计需要花费 15 分钟才能阅读完成。

Vue3 和 Vue2 的响应式有很大的不同,因为 Vue3 应用 Proxy 代替了 defineProperty,使得 Vue3 比 Vue2 在响应式数据处理方面有着更好的性能,更简洁高效的解决形式,还实现了诸多在 Vue2 上无奈实现的性能。此外 Vue3 的响应式库 reactivity 是一个独自的包,它能够不依赖 Vue 运行,意味着咱们能够将它运行在其余框架里。事实上,Vue3 的响应式库的实现形式以及市面上其余的大多数响应式库(如 observer-util,meteor 等)的实现形式都是相似的,Vue 也是参考这些库实现的,所以咱们还是很有必要去钻研一下的,毕竟咱也不能掉队了 ????,那么各位小伙伴们上面就跟我一起来看下这个 @vue/reactivity 到底是怎么实现的。

本文章的源码曾经发在了我的 git 上,能够返回查看:reactivity

浏览本文章之前你要先理解以下知识点

  • WeakMap
  • Set
  • Reflect
  • Proxy

下面这些有不理解的同学能够间接点链接查看具体的文档,文章外面就不再解释了。

咱们首先看一个应用 reactivity 的例子

// 创立一个响应式对象
const state = reactive({count: 1})

// 执行 effect
effect(() => {console.log(state.count)
})

state.count = 2 // count 扭转时执行了 effect 内的函数,控制台输入 2 

这个例子通过 reactive 创立了一个响应式对象 state,而后调用 effect 执行函数,这个函数外部拜访了 state 的属性,随后咱们更改这个 state 的属性,这时,effect 内的函数会再次执行。

这样一个响应式数据的通常实现的形式是这样的

  1. 定义一个数据为响应式(通常通过 defineProperty 或者 Proxy 拦挡 get、set 等操作)
  2. 定义一个副作用函数(effect),这个副作用函数外部拜访到响应式数据时会触发 1 中的 getter,进而能够在这里将 effect 收集起来
  3. 批改响应式数据时,就会触发 1 中的 setter,进而执行 2 中收集到的 effect 函数

对于 effect:effect 在 Vue 里通常叫做副作用函数,因为这种函数内通常执行组件渲染,计算属性等其余工作。在其余库外面可能叫观察者函数(observe)或其余,集体能了解到是什么意思就好,因为本篇文章是剖析 Vue3 的,所以对立叫副作用函数(effect)

依据以上的思路,咱们就能够开始入手实现了

reactive

首先咱们须要有一个 reactive 函数来将咱们的数据变为响应式。

// reactive.ts
import {baseHandlers} from './handlers'
import {isObject} from './utils'

type Target = object

const proxyMap = new WeakMap()

export function reactive<T extends object>(target: T): T {return createReactiveObject(target)
}

function createReactiveObject(target: Target) {
  // 只对对象增加 reactive
  if (!isObject(target)) {return target}
  // 不能反复定义响应式数据
  if (proxyMap.has(target)) {return proxyMap.get(target)
  }
  // 通过 Proxy 拦挡对数据的操作
  const proxy = new Proxy(target, baseHandlers)
  // 数据增加进 ProxyMap 中
  proxyMap.set(target, proxy)
  return proxy
}

这里次要对数据做了简略的判断,要害是在 const proxy = new Proxy(target, baseHandlers) 中,通过 Proxy 对数据进行解决,这里的 baseHandlers 就是对数据的 get,set 等拦挡操作,上面来实现下baseHandlers

get 收集依赖

首先实现下拦挡 get 操作,使得拜访数据的某一个 key 时,能够收集到拜访这个 key 的函数(effect),并把这个函数储存起来。

// handlers.ts
import {track} from './effect'
import {reactive, Target} from './reactive'
import {isObject} from './utils'

export const baseHandlers: ProxyHandler<object> = {get(target: Target, key: string | symbol, receiver: object) {
    // 收集 effect 函数
    track(target, key)
    // 获取返回值
    const res = Reflect.get(target, key, receiver)
    // 如果是对象,要再次执行 reactive 并返回
    if (isObject(res)) {return reactive(res)
    }
    return res
  }
}

这里咱们拦挡到 get 操作后,通过 track 收集依赖,track 函数做的事件就是把以后的 effect 函数收集起来,执行完 track 后,再获取到 target 的 key 的值并返回,留神这里是判断了下 res 是否是对象,如果是对象的话要返回 reactive(res),是因为思考到可能有多个嵌套对象的状况,而 Proxy 只能批改到到以后对象,并不能批改到子对象,所以在这里要解决下,上面咱们须要再实现track 函数

// effect.ts

// 存储依赖
type Deps = Set<ReactiveEffect>
// 通过 key 去获取依赖,key => Deps
type DepsMap = Map<any, Deps>
// 通过 target 去获取 DepsMap,target => DepsMap
const targetMap = new WeakMap<any, DepsMap>()
// 以后正在执行的 effect
let activeEffect: ReactiveEffect | undefined

// 收集依赖
export function track(target: object, key: unknown) {if (!activeEffect) {return}
  // 获取到这个 target 对应的 depsMap
  let depsMap = targetMap.get(target)
  // depsMap 不存在时新建一个
  if (!depsMap) {targetMap.set(target, (depsMap = new Map()))
  }
  // 有了 depsMap 后,再依据 key 去获取这个 key 所对应的 deps
  let deps = depsMap.get(key)
  // 也是不存在时就新建一个
  if (!deps) {depsMap.set(key, (deps = new Set()))
  }
  // 将 activeEffect 增加进 deps
  if (!deps.has(activeEffect)) {deps.add(activeEffect)
  }
}

留神有两个 map 和一个 set,targetMap => depsMap => deps,这样就能够使咱们通过 target 和 key 精确地获取到这个 key 所对应的 deps(effect),把以后正在执行的 effect(activeEffect)存起来,这样在批改 target[key] 的时候,就又能够通过 target 和 key 拿到之前收集到的所有的依赖,并执行它们,这里有个问题就是这个 activeEffect 它是从哪里来的,get 是怎么晓得以后正在执行的 effect 的?这个问题能够先放一放,咱们前面再将,上面咱们先实现这个 set。

实现 set

// handlers.ts

export const baseHandlers: ProxyHandler<object> = {get() {//...},
  set(target: Target, key: string | symbol, value: any, receiver: object) {
    // 设置 value
    const result = Reflect.set(target, key, value, receiver)
    // 告诉更新
    trigger(target, key, value)
    return result
  }
}

咱们在方才的 baseHandlers 上面再加一个 set,这个 set 外面次要就是赋值而后告诉更新,告诉更新通过 trigger 进行,咱们须要拿到在 get 中收集到的依赖,并执行,上面来实现下 trigger 函数

// effect.ts

// 告诉更新
export function trigger(target: object, key: any, newValue?: any) {
  // 获取该对象的 depsMap
  const depsMap = targetMap.get(target)
  // 获取不到时阐明没有触发过 getter
  if (!depsMap) {return}
  // 而后依据 key 获取 deps,也就是之前存的 effect 函数
  const effects = depsMap.get(key)
  // 执行所有的 effect 函数
  if (effects) {effects.forEach((effect) => {effect()
    })
  }
}

这个 trigger 就是获取到之前收集的 effect 而后执行。

其实除了 get 和 set,还有个罕用的操作,就是删除属性,当初咱们还不能拦挡到删除操作,上面咱们来实现下

实现 deleteProperty

export const baseHandlers: ProxyHandler<object> = {get() {//...},
  set() {//...},
  deleteProperty(target: Target, key: string | symbol) {
    // 判断要删除的 key 是否存在
    const hadKey = hasOwn(target, key)
    // 执行删除操作
    const result = Reflect.deleteProperty(target, key)
    // 只在存在 key 并且删除胜利时再告诉更新
    if (hadKey && result) {trigger(target, key, undefined)
    }
    return result
  }
}

咱们在方才的 baseHandlers 外面再加一个 deleteProperty,它能够拦挡到对数据的删除操作,在这里咱们须要先判断下删除的 key 是否存在,因为可能用户会删除一个并不存在 key,而后执行删除,咱们只在存在 key 并且删除胜利时再告诉更新,因为如果 key 不存在时,这个删除是无意义的,也就不须要更新,再有就是如果删除操作失败的话,也不须要更新,最初间接触发trigger 就能够了,留神这里的第三个参数即 value 是undefined

当初咱们曾经实现了 getsetdeleteProperty 这三种操作的拦挡,还记不记得在 track 函数中的 activeEffect,那里留了个问题,就是这个activeEffect 是怎么来的?,在最开始的例子外面,咱们要通过 effect 执行函数,这个 activeEffect 就是在这里设置的,上面咱们来实现下这个 effect 函数。

// effect.ts

type ReactiveEffect<T = any> = () => T
// 存储 effect 的调用栈
const effectStack: ReactiveEffect[] = []

export function effect<T = any>(fn: () => T): ReactiveEffect<T> {
  // 创立一个 effect 函数
  const effect = createReactiveEffect(fn)
  return effect
}

function createReactiveEffect<T = any>(fn: () => T): ReactiveEffect<T> {const effect = function reactiveEffect() {
    // 以后 effectStack 调用栈不存在这个 effect 时再执行,防止死循环
    if (!effectStack.includes(effect)) {
      try {
        // 把以后的 effectStack 增加进 effectStack
        effectStack.push(effect)
        // 设置以后的 effect,这样 Proxy 中的 getter 就能够拜访到了
        activeEffect = effect
        // 执行函数
        return fn()} finally {
        // 执行完后就将以后这个 effect 出栈
        effectStack.pop()
        // 把 activeEffect 复原
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect<T>
  return effect
}

这里次要是通过 createReactiveEffect 创立一个 effect 函数,fn 就是调用 effect 时传入的函数,在执行这个 fn 之前,先通过 effectStack.push(effect) 把这个 effect 推入 effectStack 栈中,因为 effect 可能存在嵌套调用的状况,保留下来就能够获取到一个残缺的 effect 调用栈,就能够通过下面的 effectStack.includes(effect) 判断是否存在循环调用的状况了,而后再 activeEffect = effect 设置 activeEffect,设置完之后再执行 fn,因为这个 activeEffect 是全局惟一的,所以咱们执行 fn 的时候,如果外部拜访了响应式数据,就能够在 getter 里拿到这个 activeEffect,进而收集它。

当初基本上是实现了,当初通过咱们写的这个 reactivity 库就能够实现例子中的成果了,然而还有一些边界状况须要思考,下篇文章就增加一些常见的边界状况解决。

正文完
 0