Vue3响应式原理解析

时间:2023-01-19 08:53:44

前言

今年上半年开始,自己开始在新项目中使用 Vue3 进行开发,相比较于 Vue2 来说,最大的变化就是 composition Api 代替了之前的 options Api,更像是 React Hooks 函数式组件的编程方式。

Vue3相对于Vue2响应式原理也发生了变化,由原先的 Object.defineproperty 改成了使用 Proxy 替代。Proxy 相对于 Object.defineproperty 有以下几个优化点:

  • 对象新增属性不再需要手动 $set 添加响应式,Proxy 默认会监听动态添加属性和属性的删除等操作。
  • 消除无法监听数组索引,length 属性等等,不再需要在数组原型对象上重写数组的方法。
  • Object.defineproperty 是劫持所有对象属性的 get/set 方法,需要遍历递归去实现,Proxy 是代理整个对象。
  • Vue2 只能拦截对象属性的 getset 操作,而 Proxy 拥有 13 种拦截方法。
Vue3响应式原理解析

所有这些优化,都指向了同一个点:Vue3 将拥有更快的响应速度。下面,将结合代码揭秘 Vue3 实现响应式的原理。

Proxy

Proxy 能够为另一个对象创建代理,该代理可以拦截和重新定义该对象的基本操作,例如获取、设置和定义属性。

Proxy 接受两个参数:

  • 要代理的原始对象。
  • 一个对象,它定义了哪些操作将被拦截以及如何重新定义被拦截的操作。
const target = {name: "ts",age: 18
};
const handler = {};
const proxy = new Proxy(target, handler); 

我们可以在 handler 对象上定义函数做自定义代理:

const target = {name: "ts",age: "18"
};
const handler = {get(target, key, receiver) {console.log(`访问属性${key}值`)return Reflect.get(target, key, receiver)},set(target, key, value, receiver) {console.log(`设置属性${key}值`)return Reflect.set(target, key, value, receiver)},
};
const proxy = new Proxy(target, handler);
console.log(proxy.name)
proxy.name = 'jkl';
proxy.sex = 'male'; 

打印:

Vue3响应式原理解析

注意

  • set 方法,要求返回一个布尔值,而 Reflect.set 方法刚好就是一个返回一个布尔值,直接 return 就好了。
Vue3响应式原理解析
  • sex 属性是我们后面新增的,但是也能在 getset 中拦截到,说明 Proxy 是自动给新增属性添加响应式,而不需要手动 $set 添加响应式。

通过对 Proxy 用法的基本介绍,我们发现 ProxyObject.defineproperty 用法有一个相似之处,它们内部都有 getset 方法,我们可以在 getset 方法中拦截和重新定义一些逻辑处理,和 Object.defineproperty 一样,我们可以在 Proxyget 方法中进行依赖收集即 track 操作,在 set 方法中进行触发更新即 trigger 操作。

Reflect

Reflect定义

Reflect 是一个内置的对象,与 Math 类似,它提供拦截 JavaScript 操作的方法,这些方法与 Proxy handlers 提供的的方法是一一对应的,且 Reflect 不是一个函数对象,即不能进行实例化,其所有属性和方法都是静态的。

Vue3响应式原理解析

Reflect.get/set 参数说明

  • target 指的是原始数据对象。
  • key 指的是操作的属性名。
  • newVal 指的是操作接收到的最新值。
  • receiver 指向的是当前操作正确的上下文,代理对象。
Vue3响应式原理解析

receiver 作用

receiver 是为了在执行对应的拦截操作的方法时能传递正确的 this 上下文。

reactive

基于上面对 Proxy 的基本使用,我们可以试着实现 reactive,在 Vue3 中 reactive 是返回一个 Proxy 的方法,接受一个对象作为参数:

基本实现

export const reactive = (target: object) => {return new Proxy(target, {get(target, key, receiver) {return Reflect.get(target, key, receiver)},set(target, key, value, receiver) {return Reflect.set(target, key, value, receiver)}})
} 

如果 target 对象存在深层次结构,我们就需要递归实现:

递归完整实现

const isObject = target => target !== null && typeof target == 'object'
export const reactive = (target: object) => {return new Proxy(target, {get(target, key, receiver) {console.log(`访问属性${key}值`)const result = Reflect.get(target, key, receiver)// 判断result是否是引用类型,是需要递归处理if (isObject(result)) {return reactive(result)}return result},set(target, key, value, receiver) {console.log(`设置属性${key}值`)return Reflect.set(target, key, value, receiver)}})
} 

测试

<!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>
</head>

<body><script type="module"> import { reactive } from './reactive.js'const proxy = reactive({name: 'ts', age: 18})console.log(proxy.name);console.log(proxy.age);proxy.name = 'jkl';proxy.age = 20; </script>
</body>

</html> 
Vue3响应式原理解析

收集依赖和触发更新

收集依赖和触发更新是 Vue3 响应式最核心的部分。这里涉及到三个核心概念:effecttracktrigger依赖收集依赖触发更新

Vue3响应式原理解析

访问代理对象 target 属性,会触发 get 方法,在这里会进行依赖收集即执行 track 方法。收集的依赖存储在 deps 里。修改 target 对象属性时,触发 set 方法,在这里会进行触发更新的操作即依次执行 deps 里面的依赖。

Vue3响应式原理解析

存储容器说明:

  • 选择 weakMap 类型作为容器是因为 weakMap 对键的引用是弱类型,当外部没有对键引用时,weakMap会自动删除,保证对象能被垃圾回收。
  • Map 类型对键的引用是强引用,即便外部没有对该对象保持引用,但至少还存在 Map 本身对该对象的引用关系,因此会导致该对象不能及时的被垃圾回收。
  • 对应的响应式数据对象作为 targetMap 的键,存储和当前响应式数据对象相关的依赖关系 depsMap,即 depsMap 存储的就是和当前响应式对象的每一个 key 对应的具体依赖。
  • deps 作为 depsMap 每个 key 对应的依赖集合,因为每个响应式数据可能在多个副作用函数中被使用,并且 Set 类型用于自动去重的能力。

effect

effect 依赖里面放着数据更新的逻辑,通常我们放在一个函数里面。

// activeEffect 表示当前正在走的 effect
let activeEffect = null;
export const effect = (fn:Function) => {activeEffect = fnfn()activeEffect = null
} 

这里使用一个全局变量 activeEffect 来收集当前正在走的副作用函数,并且初始化的时候调用一下。

let age = 18;
let result;
const effect = () => result = age * 2
age = 20;
effect();
console.log(result) // 40 

为了让大家理解 effect,上面这段代码是一个比较形象的例子:age 是一个变量,effect 是副作用函数,当 age 发生了变化 age = 20,这时候我们调用 effect(),更新了 result 值。在这里我们是手动写的调用 effect(),在真实响应式流程中,我们如何进行依赖收集以及自动触发更新 effect 呢?

track

track 函数用来进行依赖收集,即把依赖于变量的 effect 函数收集起来,放在 deps 里面,deps 是一个 Set 数据结构。

const targetMap = new WeakMap()
export const track = (target, key) => {// 没有activeEffect就不进行追踪if (!activeEffect) return// 获取target的依赖图let depsMap = targetMap.get(target)// 没有就新建if (!depsMap) {depsMap = new Map()targetMap.set(target, depsMap)}// 获取key所对应依赖的集合let deps = depsMap.get(key)// 没有就新建if (!deps) {deps = new Set()depsMap.set(key, deps)}// 判断activeEffect是否存在,不存在才添加,防止重复添加if (!deps.has(activeEffect)) {deps.add(activeEffect)}
} 

在介绍 Proxy 的时候,我们提到“我们会在 Proxy 的 get 方法中进行依赖收集即 track 操作”,现在我们可以把 track 添加到 get 方法中了:

const isObject = target => target !== null && typeof target == 'object'
export const reactive = (target: object) => {return new Proxy(target, {get(target, key, receiver) {console.log(`访问属性${key}值`)const result = Reflect.get(target, key, receiver)// 收集依赖track(target,key)// 判断result是否是引用类型,是需要递归处理if (isObject(result)) {return reactive(result)}return result},set(target, key, value, receiver) {console.log(`设置属性${key}值`)return Reflect.set(target, key, value, receiver)}})
} 

trigger

实现 trigger

const targetMap = new WeakMap()
export const trigger = (target, key) => {// 获取target的依赖图const depsMap = targetMap.get(target)// 没有说明没有被追踪,就returnif (!depsMap) return// 获取key所对应依赖的集合const deps = depsMap.get(key)// 遍历依赖的集合,依次执行副作用函数if (deps) {deps.forEach(effect => effect())}
} 

在介绍 Proxy 的时候,我们提到“在 set 方法中进行触发更新即 trigger 操作”,现在我们可以把 trigger 添加到 set 方法中了:

const isObject = target => target !== null && typeof target == 'object'
export const reactive = (target: object) => {return new Proxy(target, {get(target, key, receiver) {console.log('访问属性"+key+"值')const result = Reflect.get(target, key, receiver)// 收集依赖track(target,key)// 判断result是否是引用类型,是需要递归处理if (isObject(result)) {return reactive(result)}return result},set(target, key, value, receiver) {console.log(`设置属性${key}值`)// 触发更新trigger(target, key)return Reflect.set(target, key, value, receiver)}})
} 

完整代码

core.js

// activeEffect 表示当前正在走的 effect
let activeEffect = null
export const effect = fn => {activeEffect = fnfn()activeEffect = null
}

const targetMap = new WeakMap()
export const track = (target, key) => {// 没有activeEffect就不进行追踪if (!activeEffect) return// 获取target的依赖图let depsMap = targetMap.get(target)// 没有就新建if (!depsMap) {depsMap = new Map()targetMap.set(target, depsMap)}// 获取key所对应依赖的集合let deps = depsMap.get(key)// 没有就新建if (!deps) {deps = new Set()depsMap.set(key, deps)}// 判断activeEffect是否存在,不存在才添加,防止重复添加if (!deps.has(activeEffect)) {deps.add(activeEffect)}
}

export const trigger = (target, key) => {// 获取target的依赖图const depsMap = targetMap.get(target)// 没有说明没有被追踪,就returnif (!depsMap) return// 获取key所对应依赖的集合const deps = depsMap.get(key)console.log(deps, 'deps=====')// 遍历依赖的集合,依次执行副作用函数if (deps) {deps.forEach(effect => effect())}
} 

reactive.js

import { track, trigger } from './core.js'
const isObject = target => target !== null && typeof target == 'object'
export const reactive = target => {return new Proxy(target, {get(target, key, receiver) {console.log(`访问属性${key}值`)const result = Reflect.get(target, key, receiver)// 收集依赖track(target, key)// 判断result是否是引用类型,是需要递归处理if (isObject(result)) {return reactive(result)}return result},set(target, key, value, receiver) {console.log(`设置属性${key}值`)// 触发更新trigger(target, key)return Reflect.set(target, key, value, receiver)}})
} 

index.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>
</head>
<body><div class="box"></div><button>修改</button><script type="module"> import { reactive } from './reactive.js'import { effect } from './core.js'const user = reactive({name: 'ts'})effect(() => { document.querySelector('.box').innerText = `${user.name}` })document.querySelector('button').onclick = function () {user.name = 'jkl'} </script>
</body>
</html> 

效果

Vue3响应式原理解析

总结

Vue2 和 Vue3 实现响应式的思路或者核心都是相同的,即数据劫持/对象代理(自定义get / set)、依赖收集、触发更新。Vue3 使用 Proxy 实现响应式是对 Object.defineproperty 实现方案存在缺陷的一种优化。

最后

为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。
Vue3响应式原理解析
Vue3响应式原理解析
Vue3响应式原理解析
有需要的小伙伴,可以点击下方卡片领取,无偿分享