电子商务网站的开发流程包括wordpress换编辑器
电子商务网站的开发流程包括,wordpress换编辑器,医院网站模板免费下载,主机屋空间安装织梦后台程序后怎么弄成淘宝客网站大家好#xff0c;我是若川。上一篇写的是#xff1a;初学者也能看懂的 Vue3 源码中那些实用的基础工具函数。今天再分享一篇 Vue 3.2 的文章。学习源码整体架构系列、年度总结、JS基础系列背景Vue 3 正式发布距今已经快一年了#xff0c;相信很多小伙伴已经在生产环境用上了… 大家好我是若川。上一篇写的是初学者也能看懂的 Vue3 源码中那些实用的基础工具函数。今天再分享一篇 Vue 3.2 的文章。学习源码整体架构系列、年度总结、JS基础系列背景Vue 3 正式发布距今已经快一年了相信很多小伙伴已经在生产环境用上了 Vue 3 了。如今Vue.js 3.2 已经正式发布而这次 minor 版本的升级主要体现在源码层级的优化对于用户的使用层面来说其实变化并不大。其中一个吸引我的点是提升了响应式的性能:More efficient ref implementation (~260% faster read / ~50% faster write)~40% faster dependency tracking~17% less memory usage翻译过来就是 ref API 的读效率提升约为 260%写效率提升约为 50% 依赖收集的效率提升约为 40%同时还减少了约 17% 的内存使用。这简直就是一个吊炸天的优化啊因为要知道响应式系统是 Vue.js 的核心实现之一对它的优化就意味着对所有使用 Vue.js 开发的 App 的性能优化。而且这个优化并不是 Vue 官方人员实现的而是社区一位大佬 basvanmeurs 提出的相关的优化代码在 2020 年 10 月 9 号就已经提交了但由于对内部的实现改动较大官方一直等到了 Vue.js 3.2 发布才把代码合入。这次 basvanmeurs 提出的响应式性能优化真的让尤大喜出望外不仅仅是大大提升了 Vue 3 的运行时性能还因为这么核心的代码能来自社区的贡献这就意味着 Vue 3 受到越来越多的人关注一些能力强的开发人员参与到核心代码的贡献可以让 Vue 3 走的更远更好。我们知道相比于 Vue 2Vue 3 做了多方面的优化其中一部分是数据响应式的实现由 Object.defineProperty API 改成了 Proxy API。当初 Vue 3 在宣传的时候官方宣称在响应式实现的性能上做了优化那么优化体现在哪些方面呢有部分小伙伴认为是 Proxy API 的性能要优于 Object.defineProperty 的其实不然实际上 Proxy 在性能上是要比 Object.defineProperty 差的详情可以参考 Thoughts on ES6 Proxies Performance 这篇文章而我也对此做了测试结论同上可以参考这个 repo。既然 Proxy 慢为啥 Vue 3 还是选择了它来实现数据响应式呢因为 Proxy 本质上是对某个对象的劫持这样它不仅仅可以监听对象某个属性值的变化还可以监听对象属性的新增和删除而 Object.defineProperty 是给对象的某个已存在的属性添加对应的 getter 和 setter所以它只能监听这个属性值的变化而不能去监听对象属性的新增和删除。而响应式在性能方面的优化其实是体现在把嵌套层级较深的对象变成响应式的场景。在 Vue 2 的实现中在组件初始化阶段把数据变成响应式时遇到子属性仍然是对象的情况会递归执行 Object.defineProperty 定义子对象的响应式而在 Vue 3 的实现中只有在对象属性被访问的时候才会判断子属性的类型来决定要不要递归执行 reactive这其实是一种延时定义子对象响应式的实现在性能上会有一定的提升。因此相比于 Vue 2Vue 3 确实在响应式实现部分做了一定的优化但实际上效果是有限的。而 Vue.js 3.2 这次在响应式性能方面的优化是真的做到了质的飞跃接下来我们就来上点硬菜从源码层面分析具体做了哪些优化以及这些优化背后带来的技术层面的思考。响应式实现原理所谓响应式就是当我们修改数据后可以自动做某些事情对应到组件的渲染就是修改数据后能自动触发组件的重新渲染。Vue 3 实现响应式本质上是通过 Proxy API 劫持了数据对象的读写当我们访问数据时会触发 getter 执行依赖收集修改数据时会触发 setter 派发通知。接下来我们简单分析一下依赖收集和派发通知的实现Vue.js 3.2 之前的版本。依赖收集首先来看依赖收集的过程核心就是在访问响应式数据的时候触发 getter 函数进而执行 track 函数收集依赖let shouldTrack true
// 当前激活的 effect
let activeEffect
// 原始数据对象 map
const targetMap new WeakMap()
function track(target, type, key) {if (!shouldTrack || activeEffect undefined) {return}let depsMap targetMap.get(target)if (!depsMap) {// 每个 target 对应一个 depsMaptargetMap.set(target, (depsMap new Map()))}let dep depsMap.get(key)if (!dep) {// 每个 key 对应一个 dep 集合depsMap.set(key, (dep new Set()))}if (!dep.has(activeEffect)) {// 收集当前激活的 effect 作为依赖dep.add(activeEffect)// 当前激活的 effect 收集 dep 集合作为依赖activeEffect.deps.push(dep)}
}
分析这个函数的实现前我们先想一下要收集的依赖是什么我们的目的是实现响应式就是当数据变化的时候可以自动做一些事情比如执行某些函数所以我们收集的依赖就是数据变化后执行的副作用函数。track 函数拥有三个参数其中 target 表示原始数据type 表示这次依赖收集的类型key 表示访问的属性。track 函数外部创建了全局的 targetMap 作为原始数据对象的 Map它的键是 target值是 depsMap作为依赖的 Map这个 depsMap 的键是 target 的 key值是 dep 集合dep 集合中存储的是依赖的副作用函数。为了方便理解可以通过下图表示它们之间的关系因此每次执行 track 函数就是把当前激活的副作用函数 activeEffect 作为依赖然后收集到 target 相关的 depsMap 对应 key 下的依赖集合 dep 中。派发通知派发通知发生在数据更新的阶段核心就是在修改响应式数据时触发 setter 函数进而执行 trigger 函数派发通知:const targetMap new WeakMap()
function trigger(target, type, key) {// 通过 targetMap 拿到 target 对应的依赖集合const depsMap targetMap.get(target)if (!depsMap) {// 没有依赖直接返回return}// 创建运行的 effects 集合const effects new Set()// 添加 effects 的函数const add (effectsToAdd) {if (effectsToAdd) {effectsToAdd.forEach(effect {effects.add(effect)})}}// SET | ADD | DELETE 操作之一添加对应的 effectsif (key ! void 0) {add(depsMap.get(key))}const run (effect) {// 调度执行if (effect.options.scheduler) {effect.options.scheduler(effect)}else {// 直接运行effect()}}// 遍历执行 effectseffects.forEach(run)
}
trigger 函数拥有三个参数其中 target 表示目标原始对象type 表示更新的类型key 表示要修改的属性。trigger 函数 主要做了四件事情从 targetMap 中拿到 target 对应的依赖集合 depsMap创建运行的 effects 集合根据 key 从 depsMap 中找到对应的 effect 添加到 effects 集合遍历 effects 执行相关的副作用函数。因此每次执行 trigger 函数就是根据 target 和 key从 targetMap 中找到相关的所有副作用函数遍历执行一遍。在描述依赖收集和派发通知的过程中我们都提到了一个词副作用函数依赖收集过程中我们把 activeEffect当前激活副作用函数作为依赖收集它又是什么接下来我们来看一下副作用函数的庐山真面目。副作用函数那么什么是副作用函数在介绍它之前我们先回顾一下响应式的原始需求即我们修改了数据就能自动做某些事情举个简单的例子import { reactive } from vue
const counter reactive({num: 0
})
function logCount() {console.log(counter.num)
}
function count() {counter.num
}
logCount()
count()
我们定义了响应式对象 counter然后在 logCount 中访问了 counter.num我们希望在执行 count 函数修改 counter.num 值的时候能自动执行 logCount 函数。按我们之前对依赖收集过程的分析如果logCount 是 activeEffect 的话那么就可以实现需求但显然是做不到的因为代码在执行到 console.log(counter.num) 这一行的时候它对自己在 logCount 函数中的运行是一无所知的。那么该怎么办呢其实只要我们运行 logCount 函数前把 logCount 赋值给 activeEffect 就好了activeEffect logCount
logCount()
顺着这个思路我们可以利用高阶函数的思想对 logCount 做一层封装function wrapper(fn) {const wrapped function(...args) {activeEffect fnfn(...args)}return wrapped
}
const wrappedLog wrapper(logCount)
wrappedLog()
wrapper 本身也是一个函数它接受 fn 作为参数返回一个新的函数 wrapped然后维护一个全局变量 activeEffect当 wrapped 执行的时候把 activeEffect 设置为 fn然后执行 fn 即可。这样当我们执行 wrappedLog 后再去修改 counter.num就会自动执行 logCount 函数了。实际上 Vue 3 就是采用类似的做法在它内部就有一个 effect 副作用函数我们来看一下它的实现// 全局 effect 栈
const effectStack []
// 当前激活的 effect
let activeEffect
function effect(fn, options EMPTY_OBJ) {if (isEffect(fn)) {// 如果 fn 已经是一个 effect 函数了则指向原始函数fn fn.raw}// 创建一个 wrapper它是一个响应式的副作用的函数const effect createReactiveEffect(fn, options)if (!options.lazy) {// lazy 配置计算属性会用到非 lazy 则直接执行一次effect()}return effect
}
function createReactiveEffect(fn, options) {const effect function reactiveEffect() {if (!effect.active) {// 非激活状态则判断如果非调度执行则直接执行原始函数。return options.scheduler ? undefined : fn()}if (!effectStack.includes(effect)) {// 清空 effect 引用的依赖cleanup(effect)try {// 开启全局 shouldTrack允许依赖收集enableTracking()// 压栈effectStack.push(effect)activeEffect effect// 执行原始函数return fn()}finally {// 出栈effectStack.pop()// 恢复 shouldTrack 开启之前的状态resetTracking()// 指向栈最后一个 effectactiveEffect effectStack[effectStack.length - 1]}}}effect.id uid// 标识是一个 effect 函数effect._isEffect true// effect 自身的状态effect.active true// 包装的原始函数effect.raw fn// effect 对应的依赖双向指针依赖包含对 effect 的引用effect 也包含对依赖的引用effect.deps []// effect 的相关配置effect.options optionsreturn effect
}
结合上述代码来看effect 内部通过执行 createReactiveEffect 函数去创建一个新的 effect 函数为了和外部的 effect 函数区分我们把它称作 reactiveEffect 函数并且还给它添加了一些额外属性我在注释中都有标明。另外effect 函数还支持传入一个配置参数以支持更多的 feature这里就不展开了。reactiveEffect 函数就是响应式的副作用函数当执行 trigger 过程派发通知的时候执行的 effect 就是它。按我们之前的分析reactiveEffect 函数只需要做两件事情让全局的 activeEffect 指向它 然后执行被包装的原始函数 fn。但实际上它的实现要更复杂一些首先它会判断 effect 的状态是否是 active这其实是一种控制手段允许在非 active 状态且非调度执行情况则直接执行原始函数 fn 并返回。接着判断 effectStack 中是否包含 effect如果没有就把 effect 压入栈内。之前我们提到只要设置 activeEffect effect 即可那么这里为什么要设计一个栈的结构呢其实是考虑到以下这样一个嵌套 effect 的场景import { reactive} from vue
import { effect } from vue/reactivity
const counter reactive({ num: 0, num2: 0
})
function logCount() { effect(logCount2) console.log(num:, counter.num)
}
function count() { counter.num
}
function logCount2() { console.log(num2:, counter.num2)
}
effect(logCount)
count()
我们每次执行 effect 函数时如果仅仅把 reactiveEffect 函数赋值给 activeEffect那么针对这种嵌套场景执行完 effect(logCount2) 后activeEffect 还是 effect(logCount2) 返回的 reactiveEffect 函数这样后续访问 counter.num 的时候依赖收集对应的 activeEffect 就不对了此时我们外部执行 count 函数修改 counter.num 后执行的便不是 logCount 函数而是 logCount2 函数最终输出的结果如下num2: 0
num: 0
num2: 0
而我们期望的结果应该如下num2: 0
num: 0
num2: 0
num: 1
因此针对嵌套 effect 的场景我们不能简单地赋值 activeEffect应该考虑到函数的执行本身就是一种入栈出栈操作因此我们也可以设计一个 effectStack这样每次进入 reactiveEffect 函数就先把它入栈然后 activeEffect 指向这个 reactiveEffect 函数接着在 fn 执行完毕后出栈再把 activeEffect 指向 effectStack 最后一个元素也就是外层 effect 函数对应的 reactiveEffect。这里我们还注意到一个细节在入栈前会执行 cleanup 函数清空 reactiveEffect 函数对应的依赖 。在执行 track 函数的时候除了收集当前激活的 effect 作为依赖还通过 activeEffect.deps.push(dep) 把 dep 作为 activeEffect 的依赖这样在 cleanup 的时候我们就可以找到 effect 对应的 dep 了然后把 effect 从这些 dep 中删除。cleanup 函数的代码如下所示function cleanup(effect) {const { deps } effectif (deps.length) {for (let i 0; i deps.length; i) {deps[i].delete(effect)}deps.length 0}
}
为什么需要 cleanup 呢如果遇到这种场景templatediv v-ifstate.showMsg{{ state.msg }}/divdiv v-else{{ Math.random()}}/divbutton clicktoggleToggle Msg/buttonbutton clickswitchViewSwitch View/button
/template
scriptimport { reactive } from vueexport default {setup() {const state reactive({msg: Hello World,showMsg: true})function toggle() {state.msg state.msg Hello World ? Hello Vue : Hello World}function switchView() {state.showMsg !state.showMsg}return {toggle,switchView,state}}}
/script
结合代码可以知道这个组件的视图会根据 showMsg 变量的控制显示 msg 或者一个随机数当我们点击 Switch View 的按钮时就会修改这个变量值。假设没有 cleanup在第一次渲染模板的时候activeEffect 是组件的副作用渲染函数因为模板 render 的时候访问了 state.msg所以会执行依赖收集把副作用渲染函数作为 state.msg 的依赖我们把它称作 render effect。然后我们点击 Switch View 按钮视图切换为显示随机数此时我们再点击 Toggle Msg 按钮由于修改了 state.msg 就会派发通知找到了 render effect 并执行就又触发了组件的重新渲染。但这个行为实际上并不符合预期因为当我们点击 Switch View 按钮视图切换为显示随机数的时候也会触发组件的重新渲染但这个时候视图并没有渲染 state.msg所以对它的改动并不应该影响组件的重新渲染。因此在组件的 render effect 执行之前如果通过 cleanup 清理依赖我们就可以删除之前 state.msg 收集的 render effect 依赖。这样当我们修改 state.msg 时由于已经没有依赖了就不会触发组件的重新渲染符合预期。响应式实现的优化前面分析了响应式实现原理看上去一切都很 OK那么这里面还有哪些可以值得优化的点呢?依赖收集的优化目前每次副作用函数执行都需要先执行 cleanup 清除依赖然后在副作用函数执行的过程中重新收集依赖这个过程牵涉到大量对 Set 集合的添加和删除操作。在许多场景下依赖关系是很少改变的因此这里存在一定的优化空间。为了减少集合的添加删除操作我们需要标识每个依赖集合的状态比如它是不是新收集的还是已经被收集过的。所以这里需要给集合 dep 添加两个属性export const createDep (effects) {const dep new Set(effects)dep.w 0dep.n 0return dep
}
其中 w 表示是否已经被收集n 表示是否新收集。然后设计几个全局变量effectTrackDepth、trackOpBit、maxMarkerBits。其中 effectTrackDepth 表示递归嵌套执行 effect 函数的深度trackOpBit 用于标识依赖收集的状态maxMarkerBits 表示最大标记的位数。接下来看它们的应用function effect(fn, options) {if (fn.effect) {fn fn.effect.fn}// 创建 _effect 实例 const _effect new ReactiveEffect(fn)if (options) {// 拷贝 options 中的属性到 _effect 中extend(_effect, options)if (options.scope)// effectScope 相关处理逻辑recordEffectScope(_effect, options.scope)}if (!options || !options.lazy) {// 立即执行_effect.run()}// 绑定 run 函数作为 effect runnerconst runner _effect.run.bind(_effect)// runner 中保留对 _effect 的引用runner.effect _effectreturn runner
}class ReactiveEffect {constructor(fn, scheduler null, scope) {this.fn fnthis.scheduler schedulerthis.active true// effect 存储相关的 deps 依赖this.deps []// effectScope 相关处理逻辑recordEffectScope(this, scope)}run() {if (!this.active) {return this.fn()}if (!effectStack.includes(this)) {try {// 压栈effectStack.push((activeEffect this))enableTracking()// 根据递归的深度记录位数trackOpBit 1 effectTrackDepth// 超过 maxMarkerBits 则 trackOpBit 的计算会超过最大整形的位数降级为 cleanupEffectif (effectTrackDepth maxMarkerBits) {// 给依赖打标记initDepMarkers(this)}else {cleanupEffect(this)}return this.fn()}finally {if (effectTrackDepth maxMarkerBits) {// 完成依赖标记finalizeDepMarkers(this)}// 恢复到上一级trackOpBit 1 --effectTrackDepthresetTracking()// 出栈effectStack.pop()const n effectStack.length// 指向栈最后一个 effectactiveEffect n 0 ? effectStack[n - 1] : undefined}}}stop() {if (this.active) {cleanupEffect(this)if (this.onStop) {this.onStop()}this.active false}}
}
可以看到effect 函数的实现做了一定的修改和调整内部使用 ReactiveEffect 类创建了一个 _effect 实例并且函数返回的 runner 指向的是 ReactiveEffect 类的 run 方法。也就是执行副作用函数 effect 函数时实际上执行的就是这个 run 函数。当 run 函数执行的时候我们注意到 cleanup 函数不再默认执行在封装的函数 fn 执行前首先执行 trackOpBit 1 effectTrackDepth 记录 trackOpBit然后对比递归深度是否超过了 maxMarkerBits如果超过通常情况下不会则仍然执行老的 cleanup 逻辑如果没超过则执行 initDepMarkers 给依赖打标记来看它的实现const initDepMarkers ({ deps }) {if (deps.length) {for (let i 0; i deps.length; i) {deps[i].w | trackOpBit // 标记依赖已经被收集}}
}
initDepMarkers 函数实现很简单遍历 _effect 实例中的 deps 属性给每个 dep 的 w 属性标记为 trackOpBit 的值。接下来会执行 fn 函数在就是副作用函数封装的函数比如针对组件渲染fn 就是组件渲染函数。当 fn 函数执行时候会访问到响应式数据就会触发它们的 getter进而执行 track 函数执行依赖收集。相应的依赖收集的过程也做了一些调整function track(target, type, key) {if (!isTracking()) {return}let depsMap targetMap.get(target)if (!depsMap) {// 每个 target 对应一个 depsMaptargetMap.set(target, (depsMap new Map()))}let dep depsMap.get(key)if (!dep) {// 每个 key 对应一个 dep 集合depsMap.set(key, (dep createDep()))}const eventInfo (process.env.NODE_ENV ! production)? { effect: activeEffect, target, type, key }: undefinedtrackEffects(dep, eventInfo)
}function trackEffects(dep, debuggerEventExtraInfo) {let shouldTrack falseif (effectTrackDepth maxMarkerBits) {if (!newTracked(dep)) {// 标记为新依赖dep.n | trackOpBit // 如果依赖已经被收集则不需要再次收集shouldTrack !wasTracked(dep)}}else {// cleanup 模式shouldTrack !dep.has(activeEffect)}if (shouldTrack) {// 收集当前激活的 effect 作为依赖dep.add(activeEffect)// 当前激活的 effect 收集 dep 集合作为依赖activeEffect.deps.push(dep)if ((process.env.NODE_ENV ! production) activeEffect.onTrack) {activeEffect.onTrack(Object.assign({effect: activeEffect}, debuggerEventExtraInfo))}}
}
我们发现当创建 dep 的时候是通过执行 createDep 方法完成的此外在 dep 把前激活的 effect 作为依赖收集前会判断这个 dep 是否已经被收集如果已经被收集则不需要再次收集了。此外这里还会判断这 dep 是不是新的依赖如果不是则标记为新的。接下来我们再来看 fn 执行完后的逻辑finally {if (effectTrackDepth maxMarkerBits) {// 完成依赖标记finalizeDepMarkers(this)}// 恢复到上一级trackOpBit 1 --effectTrackDepthresetTracking()// 出栈effectStack.pop()const n effectStack.length// 指向栈最后一个 effectactiveEffect n 0 ? effectStack[n - 1] : undefined
}
在满足依赖标记的条件下需要执行 finalizeDepMarkers 完成依赖标记来看它的实现const finalizeDepMarkers (effect) {const { deps } effectif (deps.length) {let ptr 0for (let i 0; i deps.length; i) {const dep deps[i]// 曾经被收集过但不是新的依赖需要删除if (wasTracked(dep) !newTracked(dep)) {dep.delete(effect)}else {deps[ptr] dep}// 清空状态dep.w ~trackOpBitdep.n ~trackOpBit}deps.length ptr}
}
finalizeDepMarkers 主要做的事情就是找到那些曾经被收集过但是新的一轮依赖收集没有被收集的依赖从 deps 中移除。这其实就是解决前面提到的需要 cleanup 场景的问题在新的组件渲染过程中没有访问到的响应式对象那么它的变化不应该触发组件的重新渲染。以上就实现了依赖收集部分的优化可以看到相比于之前每次执行 effect 函数都需要先清空依赖再添加依赖的过程现在的实现会在每次执行 effect 包裹的函数前标记依赖的状态过程中对于已经收集的依赖不会重复收集执行完 effect 函数还会移除掉已被收集但是新的一轮依赖收集中没有被收集的依赖。优化后对于 dep 依赖集合的操作减少了自然也就优化了性能。响应式 API 的优化响应式 API 的优化主要体现在对 ref、computed 等 API 的优化。以 ref API 为例来看看它优化前的实现function ref(value) {return createRef(value)
}const convert (val) isObject(val) ? reactive(val) : valfunction createRef(rawValue, shallow false) {if (isRef(rawValue)) {// 如果传入的就是一个 ref那么返回自身即可处理嵌套 ref 的情况。return rawValue}return new RefImpl(rawValue, shallow)
}class RefImpl {constructor(_rawValue, _shallow false) {this._rawValue _rawValuethis._shallow _shallowthis.__v_isRef true// 非 shallow 的情况如果它的值是对象或者数组则递归响应式this._value _shallow ? _rawValue : convert(_rawValue)}get value() {// 给 value 属性添加 getter并做依赖收集track(toRaw(this), get /* GET */, value)return this._value}set value(newVal) {// 给 value 属性添加 setterif (hasChanged(toRaw(newVal), this._rawValue)) {this._rawValue newValthis._value this._shallow ? newVal : convert(newVal)// 派发通知trigger(toRaw(this), set /* SET */, value, newVal)}}
}
ref 函数返回了 createRef 函数执行的返回值而在 createRef 内部首先处理了嵌套 ref 的情况如果传入的 rawValue 也是个 ref那么直接返回 rawValue接着返回 RefImpl 对象的实例。而 RefImpl 内部的实现主要是劫持它的实例 value 属性的 getter 和 setter。当访问一个 ref 对象的 value 属性会触发 getter 执行 track 函数做依赖收集然后返回它的值当修改一个 ref 对象的 value 值则会触发 setter 设置新值并且执行 trigger 函数派发通知如果新值 newVal 是对象或者数组类型那么把它转换成一个 reactive 对象。接下来我们再来看 Vue.js 3.2 对于这部分的实现相关的改动class RefImpl {constructor(value, _shallow false) {this._shallow _shallowthis.dep undefinedthis.__v_isRef truethis._rawValue _shallow ? value : toRaw(value)this._value _shallow ? value : convert(value)}get value() {trackRefValue(this)return this._value}set value(newVal) {newVal this._shallow ? newVal : toRaw(newVal)if (hasChanged(newVal, this._rawValue)) {this._rawValue newValthis._value this._shallow ? newVal : convert(newVal)triggerRefValue(this, newVal)}}
}
主要改动部分就是对 ref 对象的 value 属性执行依赖收集和派发通知的逻辑。在 Vue.js 3.2 版本的 ref 的实现中关于依赖收集部分由原先的 track 函数改成了 trackRefValue来看它的实现function trackRefValue(ref) {if (isTracking()) {ref toRaw(ref)if (!ref.dep) {ref.dep createDep()}if ((process.env.NODE_ENV ! production)) {trackEffects(ref.dep, {target: ref,type: get /* GET */,key: value})}else {trackEffects(ref.dep)}}
}
可以看到这里直接把 ref 的相关依赖保存到 dep 属性中而在 track 函数的实现中会把依赖保留到全局的 targetMap 中let depsMap targetMap.get(target)
if (!depsMap) {// 每个 target 对应一个 depsMaptargetMap.set(target, (depsMap new Map()))
}
let dep depsMap.get(key)
if (!dep) {// 每个 key 对应一个 dep 集合depsMap.set(key, (dep createDep()))
}
显然track 函数内部可能需要做多次判断和设置逻辑而把依赖保存到 ref 对象的 dep 属性中则省去了这一系列的判断和设置从而优化性能。相应的ref 的实现关于派发通知部分由原先的 trigger 函数改成了 triggerRefValue来看它的实现function triggerRefValue(ref, newVal) {ref toRaw(ref)if (ref.dep) {if ((process.env.NODE_ENV ! production)) {triggerEffects(ref.dep, {target: ref,type: set /* SET */,key: value,newValue: newVal})}else {triggerEffects(ref.dep)}}
}function triggerEffects(dep, debuggerEventExtraInfo) {for (const effect of isArray(dep) ? dep : [...dep]) {if (effect ! activeEffect || effect.allowRecurse) {if ((process.env.NODE_ENV ! production) effect.onTrigger) {effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))}if (effect.scheduler) {effect.scheduler()}else {effect.run()}}}
}
由于直接从 ref 属性中就拿到了它所有的依赖且遍历执行不需要执行 trigger 函数一些额外的查找逻辑因此在性能上也得到了提升。trackOpBit 的设计细心的你可能会发现标记依赖的 trackOpBit在每次计算时采用了左移的运算符 trackOpBit 1 effectTrackDepth并且在赋值的时候使用了或运算deps[i].w | trackOpBit
dep.n | trackOpBit
那么为什么这么设计呢因为 effect 的执行可能会有递归的情况通过这种方式就可以记录每个层级的依赖标记情况。在判断某个 dep 是否已经被依赖收集的时候使用了 wasTracked 函数const wasTracked (dep) (dep.w trackOpBit) 0
通过与运算的结果是否大于 0 来判断这就要求依赖被收集时嵌套的层级要匹配。举个例子假设此时 dep.w 的值是 2说明它是在第一层执行 effect 函数时创建的但是这时候已经执行了嵌套在第二层的 effect 函数trackOpBit 左移两位变成了 42 4 的值是 0那么 wasTracked 函数返回值为 false说明需要收集这个依赖。显然这个需求是合理的。可以看到如果没有 trackOpBit 位运算的设计你就很难去处理不同嵌套层级的依赖标记这个设计也体现了 basvanmeurs 大佬非常扎实的计算机基础功力。总结一般在 Vue.js 的应用中对响应式数据的访问和修改都是非常频繁的操作因此对这个过程的性能优化将极大提升整个应用的性能。大部分人去看 Vue.js 响应式的实现可能目标最多就是搞明白其中的实现原理而很少去关注其中实现是否是最优的。而 basvanmeurs 大佬能对提出这一系列的优化的实现并且手写了一个 benchmark 工具来验证自己的优化非常值得我们学习。希望你看完这篇文章除了点赞在看转发三连之外也可以去看看原贴看看他们的讨论相信你会收获更多。前端的性能优化永远是一个值得深挖的方向希望在日后的开发中不论是写框架还是业务你都能够经常去思考其中可能存在的优化的点。参考资料[1] Vue.js 3.2 升级介绍: https://blog.vuejs.org/posts/vue-3.2.html[2] basvanmeurs GitHub 地址https://github.com/basvanmeurs[3] 相关 PR 讨论地址https://github.com/vuejs/vue-next/pull/2345[4] Thoughts on ES6 Proxies Performance: https://thecodebarbarian.com/thoughts-on-es6-proxies-performance[5] Proxy-vs-DefineProperty repo: https://github.com/ustbhuangyi/Proxy-vs-DefineProperty[6 ]benchmark 工具: https://github.com/basvanmeurs/vue-next-benchmarks最近组建了一个湖南人的前端交流群如果你是湖南人可以加我微信 ruochuan12 私信 湖南 拉你进群。推荐阅读我在阿里招前端该怎么帮你可进面试群我读源码的经历面对 this 指向丢失尤雨溪在 Vuex 源码中是怎么处理的老姚浅谈怎么学JavaScript················· 若川简介 ·················你好我是若川毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》多篇在知乎、掘金收获超百万阅读。从2014年起每年都会写一篇年度总结已经写了7篇点击查看年度总结。同时活跃在知乎若川掘金若川。致力于分享前端开发经验愿景帮助5年内前端人走向前列。识别上方二维码加我微信、拉你进源码共读群今日话题略。欢迎分享、收藏、点赞、在看我的公众号文章~
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/90494.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!