前言
- More efficient ref implementation (~260% faster read / ~50% faster write)
- ~40% faster dependency tracking
- ~17% less memory usage
这是一位社区大佬@basvanmeurs,在Vue3.2中,对响应式做出的优化
ref
API 的读效率提升260%
,写效率提升约为50%
。- 依赖收集的效率提升
40%
, - 内存占用减少
17%
。
看到这种程度的提升,我只能说:我斑愿称你为最强!
这位大佬是怎么做到的呢?我们来从Vue3.0开始说起。
一、Proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
1、 用法
new Proxy(target, handler)
const origin = {}
const proxy = new Proxy(origin, {
get(target, key, reciver) {
console.log('飒!')
return target[key]
},
set(target, key, value) {
target[key] = value
return true
}
})
proxy.a = 'aaaa'
proxy.a // print 飒!
不了解具体用法的同学可以去MDN上看看讲解,或者阮一峰的ES6也介绍的很清晰。传送门~
2、Proxy并不快
Vue3.0使用了proxy
可能会让人有错觉,proxy
的速度比原本的defineProperty
要快,其实并不是。这篇文章写了一些例子,比较详细的对比了proxy
和defineProperty
的执行速度。
该例子是统计了,每秒内通过=
、defineProperty
、proxy
这三种进行赋值的次数。
在图中可以比较清晰的看到,相较于普通的赋值和defineProperty
,proxy
根本都不是一个数量级。
既然不快,那为什么还要用proxy?
因为proxy
解决了以下的问题:
- 在vue2.x中,受限于
Object.defineProperty
,收集依赖只能通过getter
,触发依赖只能通过setter
。新增属性,删除属性都无法触发响应式。 - 同样的原因,我们无法让
Map、Set
这类数据类型转变为响应式,Proxy
可以。 - Vue2.x中,为了性能的考量,
数组通过劫持原生方法的方式实现的响应式
,但是通过Proxy
我们不在去考虑数组的空位导致的empty问题。
3、Proxy并不是深层代理
对于深层的对象,proxy只会代理第一层,并不会递归的将对象的每一层都代理到。
const origin = {
data: {
a: 1,
b: 2
},
msg: 'message'
}
const handler = {
get(target, key, reciver) {
console.log('代理')
return Reflect.get(target, key, reciver)
}
}
const proxy = new Proxy(origin, handler)
proxy.msg // print 代理
const data = proxy.data // print 代理
data.a // 没有任何console.log
从这个例子可以看到,对于'data'
和'msg'
这两个属性,proxy
都会进行代理,但是当单独去访问data.a
的时候,代理就消失了
原因其实也很简单,new Proxy
返回的是一个代理对象,但是proxy.data
返回的是origin
对象中data
属性的值,这个值并不再是proxy
代理对象了。
Vue中reactive
的实现,其实是递归的将对象全部都代理了一遍
二、Vue3.0的响应式
实际上Vue3.0中的响应式和原本2.x的思路是一样的,Vue3.0中用到了monorepo
,将响应式进行结偶,作为单独的reactivity
模块。
随之而来的发生改变的是一些API,简化后大致为以下四个重要的角色:
- effect
- effectFunction (简称fn)
- Dep
- ReactiveData
我们来一个一个聊
1、ReactiveData
这个东西其实就是响应式数据,在Vue2.x中使用Object.defineProperty
做的,Vue3.0中使用Proxy
做的
2、Dep
当触发了Reactive
的get
的时候,ReactiveData
就会去收集依赖,以便在下次数据发生变化的时候触发依赖中收集的函数。
首先第一个问题:依赖所收集的函数是什么呢?其实就是effect,在3.0中这类函数被称之为副影响函数(满满的react风,真是应了那句 — wherever React go, others follow)
另外,与2.x的区别在于,之前的Dep都通过闭包的方式,保存在了getter中;在3.0中,Vue通过一个Map将所有的Dep统一进行了管理,如图
3、effect
上面也提到了effect,其实就是副影响函数,我们先看看他是如何使用的,再说它的作用
import { reactive, effect } from '@vue/reactivity'
const person = reactive({
name: 'Itachi',
age: 26
})
const fn = () => console.log(person.age)
effect(fn)
// print 26
person.age = 27
// print 27
看了这个例子,我觉得大家多少也理解这个effect到底是干啥的了,其实很简单,说通俗点可以说是 '搭桥' 的,让reactiveData
和使用了reactiveData的函数
,互相之间建立起联系,这就是effect
所做的事情。
要完成 '搭桥' 这件事,除了effect之外,我们还需要
- effectStack:栈用来保存当前正在执行的
effect
,因为effect
之间经常会存在层叠的调用。 - activeEffect:当前正在执行的栈顶的那个
effect
,好让ReactiveData
更加精准的收集到这个依赖。
那显而易见,effectiFunction
,其实就是例子中的fn这个函数
4、流程
具体流程如下,大家细品~。
5、 源码
// 原始数据和proxy数据的映射
const proxyMap = new WeakMap()
// 依赖统一管理 { target => key => dep }
const targetMap = new WeakMap()
// effect执行栈
const effectStack = []
// 栈顶
const activeEffect = null
function reactive(target) {
// 如果已经reactive过,直接拿缓存
if (proxyMap.has(target)) return proxyMap.get(target)
// 简单类型无法proxy,直接返回值即可
if (typeof target !== 'object') return target
const proxy = new Proxy(target, {
get,
set
})
proxy
return proxy
}
function get(target, key, reciver) {
const res = Reflect.get(target, key, reciver)
// 收集
track(target, key)
if (isObject(res)) {
// 递归proxy
res = reactive(res)
}
return res
}
function set(target, key, value, reciver) {
const oldValue = traget[key]
const res = Reflect.set(target, key, value, reciver)
// 触发
trigger(target, key, value, oldValue)
return res
}
// 收集
function track(target, key) {
// deps的统一管理
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 收集依赖
if (dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
// 触发
function trigger(traget, key, value, oldValue) {
if (value === oldValue) return
let depsMap = tragetMap.get(target)
if (!depsMap) return
let deps = depsMap.get(key)
if (!deps) return
deps.forEach(effect => effect())
}
function effect(fn) {
const effect = function () {
// 清理上一次的缓存
cleanup(effect)
try {
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1] || null
}
}
effect.raw = fn
effect.deps = []
return effect
}
// 重置依赖
function cleanup(effect) {
const { deps } = effect
for (let dep of deps) dep.delete(effect)
deps.length = 0
}
6、哪里可以优化
其实可以优化的地方就在于这个cleanup
,每当effect
再次执行的时候,都要先将上一次收集过的清空掉,重新进行收集,这么做的目的其实是为了避免上一次收集到的依赖,本次不需要去收集的情况所导致的依赖收集错误
但是大部分场景中依赖的变动其实是相对较小的,并不需要如此大刀阔斧的进行全部清空,再次收集。
我们可以通过提前标记旧的依赖
,当执行完effect
之后,再标记新的依赖
,通过新旧对比,来判断依赖是否需要进行清理和保留。
那么应对这种effect
的层叠调用,同一个ReactiveDat
a的属性,可能应用在多个effect
中,这种一对多的情况我们该如何进行精准判断呢?
答案:位掩码
三、位掩码
除了+ - * /
之外,我们还有一种位运算。
|、&、 ~ 、<<、>>
这些位运算符
假设我们给每一位的1赋予意义,我们就可以通过按位符与&
,来判断当前的值是否有此权限,vue3中就是用这个方法区分的component
,element
,SVG
...
const ELEM = 1
const SVG = 1 << 2
const COMPONENT = 1 << 3
const FRAGMENG = 1 << 4
const PORTAL = 1 << 5
function isElement(vnode) {
return vnode.type & ELEM > 0
}
四、Vue3.2是如何优化的
上文提到过effect
是嵌套调用的,所以我们用effectTrackDepth
来记录目前这个effect在第几层
,每当有effect
执行effectTrackDepth++
,每当effect
执行完毕effectTrackDepth--
。
再通过trackOpBit
作为它位标记,可以理解为唯一ID,具体为 trackOpBit = 1 << effectTrackDepth
。
对于dep
我们也需要改造一下,原来的dep就只是一个set
,我们在此基础上加上两个属性,用来标记该属性上次和本次在哪些effect中使用过,再通过对比进行删除和新增。
由于一个reactiveData的属性
可能会用到多个effect
中,所以我们通过按位或
给dep打标记,又因为每个effect的位标记各不相同
,在通过按位与
判断得出的值是否大于零
,这样就可以分辨出这个值到底都在哪些effect中用过了。
举个例子
improt { reactive, effect } from '@vue/reactivity'
const data = reactive({ a: 1 })
effect(() => { // effectTrackDepth = 0 trackOpbit = 1 << 0
console.log(data.a) // data => 'a' => dep.tag |= trackOpbit dep.tag = 1
effect(() => { // effectTrackDepth = 1 trackOpbit = 1 << 1
console.log(data.a + 1) // data => 'a' => dep.tag |= trackOpbit dep.tag = 3
})
})
// 最后我们可以通过 dep.tag & 2 > 0 来判断该dep是否在特定的effect中使用过
1、 改造Dep
Dep我们仅仅需要给原本的Set增加两个属性即可
原本的track方法也需要改变一点
// effect层级
const effectTrackDepth = 0
// 位标记
const trackOpBit = 1
// 收集
function track(target, key) {
// deps的统一管理
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
// 收集依赖
let shouldTrack = false
if (!newTracked(dep)) {
// 打上新标记
dep.n |= trackOpBit
shouldTrack = !wasTrack(dep) // 原本没有
}
if (shouldTrack) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
// 创建Dep
function createDep() {
const dep = new Set()
dep.w = 0 // 旧标记
dep.n = 0 // 新标记
return dep
}
// 判断原来是否标记过
function wasTracked(dep) {
return (dep.w & trackOpBit) > 0
}
// 判断本次是否标记过
function newTracked(dep) {
return (dep.n & trackOpBit) > 0
}
2、 改造effect
Effect我们需要做几个事情
- 在effect执行前,先将effectTrackDepth++
- 将原本收集到的dep打上自己的标记,作为旧标记
- 执行期间通过track给dep.n打上新标记
- 执行结束开始对比dep.w 和 dep.n,整理依赖
- effectTrackDepth--
ok 我们来看代码
// 判断原来是否标记过
function wasTracked(dep) {
return (dep.w & trackOpBit) > 0
}
// 判断本次是否标记过
function newTracked() {
return (dep.n & trackOpBit) > 0
}
function initDepMarkers({ deps }) {
deps.forEach(dep => (dep.w |= trackOpBit))
}
function finalizeDepMarkers(effect) {
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let dep of deps) {
if (wasTrack(dep) && !newTrack(dep)) {
// 之前收集到了这次没有
dep.delete(effect)
} else {
deps[ptr++] = dep
}
// 重置,为了下一次执行做准备
deps.w &= ~trackOpBit
deps.n &= ~trackOpBit
}
deps.length = ptr
}
}
function effect(fn) {
const effect = function () {
try {
// 标记effect层级
trackOpBit = 1 << ++effectTrackDepth
// 给之前收集到的依赖打上旧标记
initDepMarkers(effect)
effectStack.push((activeEffect = effect))
return fn()
} finally {
// 执行完effect,看一下需要删除那些依赖添加哪些依赖
finalizeDepMarkers(effect)
trackOpBit = 1 << --effectTrackDepth
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1] || null
}
}
effect.raw = fn
effect.deps = []
return effect
}
3、整体代码
// 原始数据和proxy数据的映射
const proxyMap = new WeakMap()
// 依赖统一管理 { target => key => dep }
const targetMap = new WeakMap()
// effect执行栈
const effectStack = []
// 栈顶
const activeEffect = null
// effect层级
const effectTrackDepth = 0
// 位标记
const trackOpBit = 1
function reactive(target) {
// 如果已经reactive过,直接拿缓存
if (proxyMap.has(target)) return proxyMap.get(target)
// 简单类型无法proxy,直接返回值即可
if (typeof target !== 'object') return target
const proxy = new Proxy(target, {
get,
set
})
proxy
return proxy
}
function get(target, key, reciver) {
const res = Reflect.get(target, key, reciver)
// 收集
track(target, key)
if (isObject(res)) {
// 递归proxy
res = reactive(res)
}
return res
}
function set(target, key, value, reciver) {
const oldValue = traget[key]
const res = Reflect.set(target, key, value, reciver)
// 触发
trigger(target, key, value, oldValue)
return res
}
// 收集
function track(target, key) {
// deps的统一管理
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
// 收集依赖
let shouldTrack = false
if (!newTracked(dep)) {
// 打上新标记
dep.n |= trackOpBit
shouldTrack = !wasTrack(dep) // 原本没有
}
if (shouldTrack) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
// 触发
function trigger(traget, key, value, oldValue) {
if (value === oldValue) return
let depsMap = tragetMap.get(target)
if (!depsMap) return
let deps = depsMap.get(key)
if (!deps) return
deps.forEach(effect => effect())
}
// 创建Dep
function createDep() {
const dep = new Set()
dep.w = 0 // 旧标记
dep.n = 0 // 新标记
return dep
}
// 判断原来是否标记过
function wasTracked(dep) {
return (dep.w & trackOpBit) > 0
}
// 判断本次是否标记过
function newTracked() {
return (dep.n & trackOpBit) > 0
}
function initDepMarkers({ deps }) {
deps.forEach(dep => (dep.w |= trackOpBit))
}
function finalizeDepMarkers(effect) {
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let dep of deps) {
if (wasTrack(dep) && !newTrack(dep)) {
// 之前收集到了这次没有
dep.delete(effect)
} else {
deps[ptr++] = dep
}
// 重置,为了下一次执行做准备
deps.w &= ~trackOpBit
deps.n &= ~trackOpBit
}
deps.length = ptr
}
}
function effect(fn) {
const effect = function () {
try {
// 标记effect层级
trackOpBit = 1 << ++effectTrackDepth
// 给之前收集到的依赖打上旧标记
initDepMarkers(effect)
effectStack.push((activeEffect = effect))
return fn()
} finally {
// 执行完effect,看一下需要删除那些依赖添加哪些依赖
finalizeDepMarkers(effect)
trackOpBit = 1 << --effectTrackDepth
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1] || null
}
}
effect.raw = fn
effect.deps = []
return effect
}
4、还需要注意什么
到此为止其实我们已经实现的很不错了,但是有什么我们没有考虑的呢,其实是有的
位掩码是通过二进制的位数来做判断,那么二进制的长度是无限的吗?
显然不是。在JS中number是32位
,其中一位还要作为正负号,所以我们也只有31位
可用,如果effect的层叠超过31层的话,我们该怎么办呢?
那就只能走我们原来的cleanup方法啦。
五、为什么要看源码
最后的碎碎念,这次在看新vue代码的时候,我其实很震惊的,作为很普通的一个研发,我们大部分的时间都是花费在实现公司的业务。
我在知道vue更新到3.2后,我第一次开始看vue3的源码,我也仅仅只看了reactivity,短短的时间,vue就又更新了7个小版本,可见速度之快,读的速度都赶不上更新的速度,代码一直在变,你花了半天读的代码没准哪天就迭代掉了,那看的意义是什么呢?
我经常被问的哑口无言。你会用vue不就可以了吗?花大把时间去理解源码真的有必要吗?所以理由到底是什么呢?
其实我觉得大部分人看源码的原因都是为了应试,我一开始去看vue2.x的理由也是这个。
当然也有很多大佬说,看了是为了学设计模式,学习一些高级技巧,学习其中的思想,并用到自己的项目中,我听到这些的时候我很认同。
但我自认为做到这一步很难,我也努力在自己的项目中去寻找场景,但是始终没有用上我在vue中学到的所谓“思想”
那对于我这种前端渣渣看源码的意义是什么呢?
当我看到reactive中proxy的时候,我其实突然顿悟了。很多场景我们用不到很多原生的API,比如Proxy。我甚至都不会用它,不理解他的运行机制。
但是当我为了搞懂reactive中的Proxy去查MDN,去写小例子测试proxy功能的时候,我突然觉得这些高深的开源框架,不仅能教你高大上的设计思想,也同时可以帮你夯实前端基础。
源码就像本书一样,雅俗共赏,高手可以去学设计模式,小菜鸟也可以从中夯实基础,慢慢转变成高手。
就这样~大家共勉~