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操作时触发对应的依赖关系,从而调用所有相关的执行函数。

相关推荐
4triumph8 分钟前
Vue.js教程笔记
前端·vue.js
程序员大金25 分钟前
基于SSM+Vue+MySQL的酒店管理系统
前端·vue.js·后端·mysql·spring·tomcat·mybatis
清灵xmf28 分钟前
提前解锁 Vue 3.5 的新特性
前端·javascript·vue.js·vue3.5
程序员大金35 分钟前
基于SpringBoot的旅游管理系统
java·vue.js·spring boot·后端·mysql·spring·旅游
二川bro1 小时前
Vue 修饰符 | 指令 区别
前端·vue.js
程序员大金2 小时前
基于SpringBoot+Vue+MySQL的养老院管理系统
java·vue.js·spring boot·vscode·后端·mysql·vim
尸僵打怪兽2 小时前
后台数据管理系统 - 项目架构设计-Vue3+axios+Element-plus(0920)
前端·javascript·vue.js·elementui·axios·博客·后台管理系统
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS网上购物商城(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
陈小白_weilin3 小时前
VUE-CLI配置全局SCSS变量
vue.js·scss
For. tomorrow3 小时前
Vue3中el-table组件实现分页,多选以及回显
前端·vue.js·elementui