vue3源码-reactivity部分

本文主要记录我在学习vue3的reactivity部分的源码的学习路径。通过删减源码复杂的分支判断,一步步构建一个简单的vue应用,从而学习reactivity实现的逻辑。

通过图形理解大致的实现思路

  1. 首先reactive需要通过Proxy提供代理对象访问属性的能力。
  2. 然后proxy拦截访问属性和改变属性,在访问属性时追踪执行函数与被代理对象的依赖关系,在修改属性时触发执行函数。

第二部分的实现很复杂,一上来先看源码对我的心智负担还是太重了,通过生动的图形能帮助我理解vue实现追踪被执行对象和执行函数大致的思路。

www.bilibili.com/video/BV1SZ...

  • vue3使用WeakMap先建立被代理对象与具体依赖关系的映射关系,key是被代理对象的引用,value是用于存储每个属性依赖关系的Map
  • depsMap负责记录被代理对象中每个属性和对应的执行函数的映射关系,key是具体的属性,valuedep
  • dep是所有相关的执行函数的Set

到此就可以带着初步的思路来构建一个简单的vue应用了。

构建vue应用

我应该怎么使用这个简易的vue?

首先先从具体使用开始,沿着reactiveeffect这两个暴露出来的方法逐步深入。

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <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
  // reactive应该传入一个对象
  const obj = reactive({
    name: '张三'
  })

  // 可以调用多个 effect 方法,它们会随着name属性的改变自动触发
  effect(() => {
    document.querySelector('#p1').innerText = obj.name
  })
  effect(() => {
    document.querySelector('#p2').innerText = obj.name
  })

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

</html>

对于reactive方法,我希望直接传入一个对象。 对于effect方法,我希望它能随着依赖的属性的改变而自动触发,而且可以调用多次不会互相扰乱。

reactive方法的实现

ts 复制代码
import { mutableHandlers } from './basehandlers'

/* 
  创建一个weakMap用于存储原对象和生成的proxy对象的映射
  key: object
  value: proxy 
*/
export const proxyMap = new WeakMap<object, any>()

/* 创建reactive方法,返回创建响应性对象的调用值,将复杂实现隐藏在内部函数 */
export function reactive(target: object) {
  return createReactiveObject(target, mutableHandlers, proxyMap)
}

/* 创建对象的proxy代理实例,最终会返回已经存在的proxy或新建的proxy */
function createReactiveObject(
  target: object,
  basehandlers: object,
  proxyMap: WeakMap<object, any>
) {
  // 在proxyMap中根据传入的对象寻找对应的proxy实例
  const existProxy = proxyMap.get(target)
  // 如果找到直接返回实例
  if (existProxy) {
    return existProxy
  }
  // 否则创建一个新的proxy
  const proxy = new Proxy(target, basehandlers)

  // 将对象和proxy的映射关系存放到WeakMap中
  proxyMap.set(target, proxy)
  return proxy
}

Proxy

reactive方法在vue2与vue3中依靠不同的API实现。vue2中是利用Object.defineProperty()通过给对象已知的属性逐一添加属性描述符从而拦截set和get操作实现监听对象的变化,而vue3中利用Proxy实现了监听的改进,二者的具体差异可以参考下表。

vue2 vue3
核心API Object.defineProperty() Proxy
新增属性 实现响应性是通过具体的对象的具体的属性来设置属性描述符添加setter和getter,如果新增属性是没有办法直接获取到这个属性的 通过Proxy生成一个实例代理对象,handler中的操作是添加到被代理对象的所有属性上的,因此新增属性不会影响响应性
修改对象属性 直接在原对象上进行修改 需要在代理对象上进行修改

Proxy相比 Object.defineProperty()最核心的改进就是可以直接代理整个对象,这样就在新增属性的时候就不需要额外的操作了。除此之外Proxy也可以直接监听数组的变化,而Object.defineProperty()需要对数组进行额外的处理。

mutableHandlers方法负责gettersetter的逻辑

ts 复制代码
import { track, trigger } from './effect'
// 生成setter
const get = createGetter()
function createGetter() {
  return function get(target: object, key: string | symbol, receiver: object) {
    // 利用Reflect得到返回值,receiver的指定保证在读取属性时指针始终在proxy对象内
    const result = Reflect.get(target, key, receiver)
    // 当被读取时收集该属性和effect的依赖关系,以便setter中触发effect效果
    track(target, key)
    return result
  }
}

// 生成setter
const set = createSetter()
function createSetter() {
  return function get(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ) {
    // 利用Reflect设置新值
    const result = Reflect.set(target, key, value, receiver)
    // 触发依赖
    trigger(target, key)
    return result
  }
}

export const mutableHandlers: ProxyHandler<object> = {
  set,
  get
}

作为Proxy的第二个参数传入的mutableHandlers可以在setget拿到target:传入的对象,key:当前操作的属性,valueset操作时传入的新值和receiver:当前的代理对象。

Reflect

Reflect是JavaScript中的一个内置对象,它提供了一组静态方法,用于操作对象

通过Reflect提供的get和set方法而不是直接操作被代理对象,是因为通过传入receiver参数可以改变this的指向为代理对象,保证对属性的所有操作都走代理对象的操作逻辑。

构建被代理对象和执行函数的依赖关系

effect函数

ts 复制代码
export function effect<T = any>(fn: () => T) {
  // 生成 ReactiveEffect实例,并立即执行一次执行函数
  const _effect = new ReactiveEffect(fn)
  _effect.run()
}

let activeEffect: ReactiveEffect | undefined

export class ReactiveEffect<T = any> {
  constructor(public fn: () => T) {}
  run() {
    // 为activeEffect赋值,然后触发执行函数
    activeEffect = this
    return this.fn()
  }
}

effect函数的作用是基于传入的执行函数生成一个ReactiveEffect实例,这样做的目的是将函数逻辑和执行操作分开,同时实例对象也便于数据存储。

targetMap的构建

ts 复制代码
// targetMap 收集 响应性对象 和 某一个属性 和 属性对应的执行函数的Map 的WeakMap
/* 
  key: 响应性对象
  value: Map
    key: 指定的某一个属性
    value: 当指定属性改变时需要执行的函数Set
*/
const targetMap = new WeakMap<object, KeyToDepMap>()

targetMap负责建立被代理对象与具体依赖关系的映射关系,key是被代理对象的引用,value是用于存储每个属性依赖关系的Map。使用WeakMap的理由主要有两点:

  1. key可以是对象。
  2. key是弱引用,如果key对象没有其他的强引用时会被垃圾回收。

收集依赖和触发依赖

ts 复制代码
// 根据 effects 生成 dep 实例
export const createDep = (effects?: ReactiveEffect[]): Dep => {
  // 使用Set存储所有Effect效果,解决多个Effect同时存在的问题
  const dep = new Set(effects) as Dep
  return dep
}
/* 
  用于收集依赖的方法 
*/
export function track(target: object, key: unknown) {
  // 1
  let depsMap = targetMap.get(target)
  // 2
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  // 3
  let dep = depsMap.get(key)
  // 4
  if (!dep) {
    dep = createDep()
    depsMap.set(key, dep)
  }
  // 5
  trackEffects(dep)
}

export function trackEffects(dep: Dep) {
  dep.add(activeEffect!)
}

/* 
  用于触发依赖的方法 
*/
export function trigger(target: object, key: unknown) {
  // 1
  const depsMap = targetMap.get(target)
  // 2
  if (!depsMap) return

  // 3
  const dep = depsMap.get(key)

  // 4
  if (!dep) return
  // 5
  triggerEffects(dep)
}

// 通过将dep转化成数组依次触发run方法
export function triggerEffects(dep: Dep) {
  // 通过 dep 获取执行函数组成的数组
  const effects = isArray(dep) ? dep : [...dep]
  // 一次触发
  for (const effect of effects) {
    triggerEffect(effect)
  }
}

export function triggerEffect(effect: ReactiveEffect) {
  effect.run()
}

收集依赖的主要步骤:

  1. 根据传入的对象查找targetMap中对应的depsMap
  2. 判断depsMap是否存在,不存在则新建一个Map,并存入targetMap中。
  3. depsMap对应的 Set 可能存在多个effect,需要根据key获取指定的effect
  4. 判断dep是否存在,如果不存在则新建一个dep,同时存入depsMap中。
  5. dep中添加effect

触发依赖的主要步骤:

  1. 根据传入的对象查找targetMap中对应的depsMap
  2. 如果不存在直接返回(表示set操作的是非响应式对象,不起作用)。
  3. 继续向下查找depsMap中对应keydep
  4. 如果dep不存在,表示没有执行函数,直接返回。
  5. dep存在,依次调用存储在Set中的各个执行函数。

到这里,这个简易vue库就实现了。整体上主要是两个模块,一个是基于Proxy的API实现监听reactive方法传入的对象,另一个就是在拦截get操作时构建从对象到依赖映射再到执行函数的完整依赖关系,拦截set操作时触发对应的依赖关系,从而调用所有相关的执行函数。

相关推荐
卤蛋fg631 分钟前
vxe-table 列拖拽排序与行拖拽排序:让表格布局任意排序
vue.js
粉末的沉淀1 小时前
vue:Vite项目中高效管理纯色SVG图标的方案
前端·javascript·vue.js
卤蛋fg61 小时前
vxe-table 列宽与行高拖拽调整:让表格布局极其灵活,拖拽功能非常强大
vue.js
向日的葵0062 小时前
Vue 路由传参的三种方式(三)
vue.js·路由
如果超人不会飞2 小时前
TinyVue Checkbox复选框组件使用指南
前端·vue.js
如果超人不会飞2 小时前
TinyVue Radio单选框组件使用指南
vue.js
鲁班小子2 小时前
Vite resolve.dedupe 使用教程
vue.js·vite
如果超人不会飞2 小时前
TinyVue Input输入框组件使用指南
vue.js
如果超人不会飞2 小时前
TinyVue Pager分页组件使用指南
前端·vue.js
大刚测试开发实战3 小时前
TestHub重磅更新!AI用例生成增加流式输出、Markdown文档上传、模型配置检测、AI评审开关控制...
vue.js·后端·github