前言
今年上半年开始,自己开始在新项目中使用 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 只能拦截对象属性的
get
和set
操作,而Proxy
拥有13
种拦截方法。
所有这些优化,都指向了同一个点: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';
打印:
注意:
-
set
方法,要求返回一个布尔值,而Reflect.set
方法刚好就是一个返回一个布尔值,直接 return 就好了。
-
sex
属性是我们后面新增的,但是也能在get
和set
中拦截到,说明Proxy
是自动给新增属性添加响应式,而不需要手动$set
添加响应式。
通过对 Proxy
用法的基本介绍,我们发现 Proxy
和 Object.defineproperty
用法有一个相似之处,它们内部都有 get
和 set
方法,我们可以在 get
和 set
方法中拦截和重新定义一些逻辑处理,和 Object.defineproperty
一样,我们可以在 Proxy
的 get
方法中进行依赖收集即 track
操作,在 set
方法中进行触发更新即 trigger
操作。
Reflect
Reflect定义
Reflect
是一个内置的对象,与 Math
类似,它提供拦截 JavaScript
操作的方法,这些方法与 Proxy handlers
提供的的方法是一一对应的,且 Reflect
不是一个函数对象,即不能进行实例化,其所有属性和方法都是静态的。
Reflect.get/set 参数说明
-
target
指的是原始数据对象。 -
key
指的是操作的属性名。 -
newVal
指的是操作接收到的最新值。 -
receiver
指向的是当前操作正确的上下文,代理对象。
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 响应式最核心的部分。这里涉及到三个核心概念:effect
、track
、trigger
即依赖
、收集依赖
、触发更新
。
访问代理对象 target
属性,会触发 get
方法,在这里会进行依赖收集即执行 track
方法。收集的依赖存储在 deps
里。修改 target
对象属性时,触发 set
方法,在这里会进行触发更新的操作即依次执行 deps
里面的依赖。
存储容器说明:
- 选择
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>
效果
总结
Vue2 和 Vue3 实现响应式的思路或者核心都是相同的,即数据劫持/对象代理(自定义get / set)、依赖收集、触发更新。Vue3 使用 Proxy
实现响应式是对 Object.defineproperty
实现方案存在缺陷的一种优化。
最后
为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。
有需要的小伙伴,可以点击下方卡片领取,无偿分享