持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情
前言
学习vue3有一段时间了,也用vue3写了几个项目,今天抽空整理一下关于vue3的响应式原理知识,写文章的初衷是,加深自己的记忆、理解,方便以后复习、查阅。
关于Proxy和Reflect
Proxy是es6为操作对象提供的API,可以理解为“代理”,它给目标对象架设一层“拦截”,可以设置外界对目标对象访问的过滤和改写。
Reflect也是es6为操作对象提供的API,它和Object相似,只不过更加的优雅而已;比较重要的一点是,Reflect对象的方法和Proxy对象的方法一一对应,只要是Proxy对象上的方法,在Reflect上都能找到对应的方法,所以Reflect简直就是Proxy的最佳搭档。
具体的介绍,可以看“阮一峰”大佬的《ECMAScript 6 入门》,顺便也可以了解下weakMap、Map、Set这几种es6新增的数据类型,在vue3响应式实现中,都有用到。
vue3为什么要用Proxy
Object.defineProperty的局限性
- 只对初始化对象的属性进行拦截,新增的属性不会触发get和set。
- 无法监听数组基于下标的修改(这里其实有误区)
Object.defineProperty其实也是可以监听数组下标的变化的,本质上数组也是键值对集合,只不过key是数字, 那么Object.defineProperty自然是可以监听到的。
但是为啥vue2要改写数组的七个原生方法push、pop、shift、unshift、splice、sort、reverse,而不是用Object.defineProperty来实现数组全方位监听呢?
原因是出于性能方面的取舍,数组的操作一般用上面那7种方法就够了,如果要用Object.defineProperty全方位监控的话,每次改变都要重新将整个数组的所有key通过递归加上setter和getter,而且数组的key和value变动都是很频繁的,当数据量大的时候,难免会带来性能开销问题。 - 因为
Object.defineProperty只能对初始化对象的属性进行拦截,所以vue2的响应式数据需要写到data对象里面,在组件初始化的时候为整个data对象的属性加上getter和setter,而且如果对象嵌套多层的话,需要去递归遍历给每个属性加上getter和setter,在data对象比较庞大的时候,会影响到组件的初始化速度。
Proxy的优势
- 可以拦截对象新增的属性。
- 只要是对象都能被代理。
- 实现惰性监听
vue3通过提供reactive、computed、effect方法,用户可以自己选择是否需要使用响应式数据,组件实例化的时候是不需要去遍历对象的,而且对于多次嵌套的对象,只会在访问的时候为去设置Proxy代理,这样做就可以加快组件初始化时间,减少依赖项的保存,降低运行内存。
手写vue3 响应式
- 在Proxy的Handler中拦截对象的各种取值、赋值操作,依托track和trigger两个函数进行依赖收集和派发更新。
- track用来在读取时收集依赖
- trigger用来在更新时触发依赖
- 创建一个effect函数
- 立即执行effect,然后将当前渲染的effect赋值给activeEffect
// 定义reactive方法
function reactive(target) {
const handler = {
// target:目标对象
// key: 要访问的属性名
// receiver: proxy实例本身(严格地说,是操作行为所针对的对象)
get(target, key, receiver) {
track(receover,key) // 访问时收集依赖
return Reflect.get(target, key, receiver)
},
// value:属性值
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
trigger(receiver, key) // 值变动时自动派发更新
}
}
return new Proxy(target, handler)
}
// 实现effect函数,接收一个回调函数,并赋值给activeEffect,并立即执行
let activeEffect = null // 用来存放当前执行的副作用函数
function effect(cb) {
activeEffect = cb
activeEffect() // 立即执行,访问响应式对象,触发响应式数据getter
activeEffect = null // 执行完之后,需要清空,因为如果有多层嵌套的对象,这是一个递归的过程
}
// 实现track方法
const targetMap = new WeakMap() // 用来存放收集的所有Reactive Object对象集合
function track(target, key) {
if(!activeEffect) return
let depsMap = targetMap.get(target)
// 如果当前依赖里面没有,那么创建一个
if (!depsMap) {
// depsMap = new Map() 用来存放响应式对象中的属性
targetMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(key)
// 如果当前对象里面没有这个属性,那么创建一个
if (!dep) {
// dep = new Set() 用来存放收集的副作用函数,Set可以自动去重
depsMap.set(key, dep = new Set())
}
dep.add(activeEffect) // 把此时的activeEffect添加进去
}
// 实现 trigger函数,派发更新(执行收集到的依赖)
function trigger(target, key) {
let depsMap = targetMap.get(target)
// 如果有依赖,那么取出其中的副作用函数,依次执行
if (depsMap) {
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
}