网站程序建设,网站建设一条龙全包顶呱呱,数据分析系统搭建,推广网站名是什么Python微信订餐小程序课程视频
https://blog.csdn.net/m0_56069948/article/details/122285951
Python实战量化交易理财系统
https://blog.csdn.net/m0_56069948/article/details/122285941 当我们通过effect将副函数向响应上下文注册后#xff0c;副作用函数内访问响应式对…Python微信订餐小程序课程视频
https://blog.csdn.net/m0_56069948/article/details/122285951
Python实战量化交易理财系统
https://blog.csdn.net/m0_56069948/article/details/122285941 当我们通过effect将副函数向响应上下文注册后副作用函数内访问响应式对象时即会自动收集依赖并在相应的响应式属性发生变化后自动触发副作用函数的执行。
// ./effect.tsexport funciton effectany(fn: () T,options?: ReactiveEffectOptions
): ReactiveEffectRunner {if ((fn as ReactiveEffectRunner).effect) {fn (fn as ReactiveEffectRunner).effect.fn}const \_effect new ReactiveEffect(fn)if (options) {extend(\_effect, options)if (options.scope) recordEffectScope(\_effect, options.scope)}// 默认是马上执行副作用函数收集依赖但可通过lazy属性延迟副作用函数的执行延迟依赖收集。if (!options || !options.lazy) {\_effect.run()}// 类型为ReactiveEffectRunner的runner是一个绑定this的函数const runner \_effect.run.bind(\_effect) as ReactiveEffectRunnerrunner.effect \_effectreturn runner
}
effect函数的代码十分少主要流程是
将基于副作用函数构建ReactiveEffect对象若为默认模式则马上调用ReactiveEffect对象的run方法执行副作用函数。
不过这里我们有几个疑问
ReactiveEffectRunner是什么ReactiveEffect生成的对象究竟是什么显然ReactiveEffect的run方法才是梦开始的地方到底它做了些什么针对配置项scoperecordEffectScope的作用
ReactiveEffectRunner是什么
// ./effect.ts// ReactiveEffectRunner是一个函数而且有一个名为effect的属性且其类型为RectiveEffect
export interface ReactiveEffectRunnerany {(): Teffect: ReactiveEffect
}
ReactiveEffect生成的对象究竟是什么
// 用于记录位于响应上下文中的effect嵌套层次数
let effectTrackDepth 0
// 二进制位每一位用于标识当前effect嵌套层级的依赖收集的启用状态
export left trackOpBit 1
// 表示最大标记的位数
const maxMarkerBits 30const effectStack: ReactiveEffect[] []
let activeEffect: ReactiveEffect | undefinedexport class ReactiveEffectany {// 用于标识副作用函数是否位于响应式上下文中被执行active true// 副作用函数持有它所在的所有依赖集合的引用用于从这些依赖集合删除自身deps: Dep[] []// 默认为false而true表示若副作用函数体内遇到foo.bar 1则无限递归执行自身直到爆栈allowRecurse?: booleanconstructor(public fn: () T,public scheduler: EffectScheduler | null null,scope?: EffectScope | null) {recordEffectScope(this, scope)}run() {/*** 若当前ReactiveEffect对象脱离响应式上下文那么其对应的副作用函数被执行时不会再收集依赖并且其内部访问的响应式对象发生变化时也会自动触发该副作用函数的执行*/if (!this.active) {return this.fn()}// 若参与响应式上下文则需要先压栈if (!effectStack.includes(this)) {try {// 压栈的同时必须将当前ReactiveEffect对象设置为活跃即程序栈中当前栈帧的意义。effectStack.push(activeEffect this)enableTracking()trackOpBit 1 effectTrackDepthif (effectTrackDepth maxMarkerBits) {// 标记已跟踪过的依赖initDepMarkers(this)}else {cleanupEffect(this)}return this.fn()}finally {if (effectTrackDepth maxMarkerBits) {/*** 用于对曾经跟踪过但本次副作用函数执行时没有跟踪的依赖采取删除操作。* 即新跟踪的 和 本轮跟踪过的都会被保留。*/finalizeDepMarkers(this)}trackOpBit 1 --effectTrackDepthresetTracking()// 最后当然弹栈把控制权交还给上一个栈帧咯effectStack.pop()const n effectStack.lengthactiveEffect n 0 ? effectStack[n - 1] : undefined }}/*** 让当前ReactiveEffect对象脱离响应式上下文请记住这是一去不回头的操作哦*/ stop() {if (this.active) {cleanupEffect(this)this.active false}}}
}
为应对嵌套effect内部将当前位于响应上下文的ReactiveEffect对象压入栈结构effectStack: ReactiveEffect[]当当前副作用函数执行后再弹出栈。另外虽然我们通过effect函数将副作用函数注册到响应上下文中但我们仍能通过调用stop方法让其脱离响应上下文。
function cleanupEffect(effect: ReactiveEffect) {const { deps } effectif (deps.length) {// 将当前ReactiveEffect对象从它依赖的响应式属性的所有Deps中删除自己那么当这些响应式属性发生变化时则不会遍历到当前的ReactiveEffect对象for (let i 0; i deps.length; i) {deps[i].delete(effect)}// 当前ReactiveEffect对象不再参与任何响应了deps.length 0}
}
在执行副作用函数前和执行后我们会看到分别调用了enableTracking()和resetTracking()函数它们分别表示enableTracking()执行后的代码将启用依赖收集resetTracking()则表示后面的代码将在恢复之前是否收集依赖的开关执行下去。要理解它们必须结合pauseTracking()和实际场景说明
let shouldTrack true
const trackStack: boolean[] []export function enableTracking() {trackStack.push(shouldTrack)shouldTrack true
}export function resetTracking() {const last trackStack.pop()shouldTrack last undefined ? true : last
}export function pauseTracking() {trackStack.push(shouldTrack)shouldTrack false
}
假设我们如下场景
const values reactive([1,2,3])
effect(() {values.push(1)
})
由于在执行push时内部会访问代理对象的length属性并修改length值因此会导致不断执行该副作用函数直到抛出异常Uncaught RangeError: Maximum call stack size exceeded就是和(function error(){ error() })()不断调用自身导致栈空间不足一样的。而vue/reactivity是采用如下方式处理
;([push, pop, shift, unshift, splice] as const).forEach(key {instrumentations[key] function (this: unknown[], ...args: unknown[]) {pauseTracking()const res (toRaw(this) as any)[key].apply(this, args)resetTracking()return res}
})
即通过pauseTracking()暂停push内部的发生意外的依赖收集即push仅仅会触发以其他形式依赖length属性的副作用函数执行。然后通过resetTracking()恢复到之前的跟踪状态。
最后在执行副作用函数return this.fn()前居然有几句难以理解的语句
try {trackOpBit 1 effectTrackDepthif (effectTrackDepth maxMarkerBits) {initDepMarkers(this)}else {cleanupEffect(this)}return this.fn()
}
finally {if (effectTrackDepth maxMarkerBits) {finalizeDepMarkers(this)}trackOpBit 1 --effectTrackDepth
}
我们可以将其简化为
try {cleanupEffect(this)return this.fn()
}
finally {}
为什么在执行副作用函数前需要清理所有依赖呢我们可以考虑一下如下的情况:
const state reactive({ show: true, values: [1,2,3] })
effect(() {if (state.show) {console.log(state.values)}
})
setTimeout(() {state.values.push(4)
}, 5000)setTimeout(() {state.show false
}, 10000)setTimeout(() {state.values.push(5)
}, 15000)
一开始的时候副作用函数将同时依赖show和values5秒后向values追加新值副作用函数马上被触发重新执行再过10秒后show转变为false那么if(state.show)无论如何运算都不成立此时再对values追加新值若副作用函数再次被触发显然除了占用系统资源外别无用处。 因此在副作用函数执行前都会先清理所有依赖(cleanupEffect的作用)然后在执行时重新收集。
面对上述情况先清理所有依赖再重新收集是必须的但如下情况这种清理工作反而增加无谓的性能消耗
const state reactive({ show: true, values: [1,2,3] })
effect(() {console.log(state.values)
})
vue/reactivity给我们展示了一个非常优秀的处理方式那么就是通过标识每个依赖集合的状态(新依赖和已经被收集过)并对新依赖和已经被收集过两个标识进行对比筛选出已被删除的依赖项。
优化无用依赖清理算法
export type Dep SetReactiveEffect Trackedmarkerstype TrackedMarkers {/*** wasTracked的缩写采用二进制格式每一位表示不同effect嵌套层级中该依赖是否已被跟踪过(即在上一轮副作用函数执行时已经被访问过)*/ w: number/*** newTracked的缩写采用二进制格式每一位表示不同effect嵌套层级中该依赖是否为新增(即在本轮副作用函数执行中被访问过)*/ n: number
}export const createDep (effects) {const dep new SetReactiveEffect(effects) as Dep// 虽然TrackedMarkers标识是位于响应式对象属性的依赖集合上但它每一位仅用于表示当前执行的副作用函数是否曾经访问和正在访问该响应式对象属性dep.w 0dep.n 0return dep
}export const wasTracked (dep: Dep): boolean (dep.w trackOpBit) 0export const newTracked (dep: Dep): boolean (dep.n trackOpBit) 0/*** 将当前副作用函数的依赖标记为 已经被收集*/
export const initDepMarkers ({ deps }: ReactiveEffect) {if (deps.length) {for (let i 0; i deps.length; i) {deps[i].w | trackOpBit}}
}/*** 用于对曾经跟踪过但本次副作用函数执行时没有跟踪的依赖采取删除操作。* 即新跟踪的 和 本轮跟踪过的都会被保留。*/
export const finalizeDepMarkers (effect: ReactiveEffect) {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}// 将w和n中对应的嵌套层级的二进制位置零如果缺少这步后续副作用函数重新执行时则无法重新收集依赖。dep.w ~trackOpBitdep.n ~trackOpBit}// 缩小依赖集合的大小deps.length ptr}
}
// 在位于响应式上下文执行的副作用函数内访问响应式对象属性将通过track收集依赖
export function track(target: object, type: TrackOpTypes, key: unknown) {if (!isTracking()) {return}// targetMap用于存储响应式对象-对象属性的键值对// depsMap用于存储对象属性-副作用函数集合的键值对let depsMap targetMap.get(target)if (!depsMap) {target.set(target, (depsMap new Map()))}let dep depsMap.get(key)if (!dep) {depsMap.set(key, (dep createDep()))}trackEffects(dep)
}// 收集依赖
export function trackEffects(dep: Dep
) {let shouldTrack falseif (effectTrackDepth maxMarkerBits) {// 如果本轮副作用函数执行过程中已经访问并收集过则不用再收集该依赖if (!newTracked(dep)) {dep.n | trackOpBitshouldTrack !wasTracked(dep)}}else {// 对于全面清理的情况如果当前副作用函数对应的ReactiveEffect对象不在依赖集合中则标记为trueshouldTrack !dep.has(activeEffect!)}if (shouldTrack) {dep.add(activeEffect!)activeEffect!.deps.push(dep)}
}
单单从代码实现角度能难理解这个优化方式不如我们从实际的例子出发吧
const runAync fn setTimeout(fn, 1000)const state reactive({ show: true, values: [1,2,3] })
// 1
effect(() {if (state.show) {console.log(state.values)}
})// 2
runAync(() {state.values.push(4)
})// 3
runAync(() {state.show false
})
首次执行副作用函数 a. effectTrackDepth为0因此1 effectTrackDepth得到的effectTrackDepth和trackOpBit均为1但由于此时副作用函数还没有收集依赖因此initDepMarkers函数没有任何效果 b. 访问state.show时由于之前没有收集过响应式对象state的show属性因此会调用createDep创建w和n均为0的依赖集合并调用trackEffects发现newTracked(dep)为未跟踪过则将n设置为1然后开始收集依赖 c. 访问state.values会重复第2步的操作 d. 由于state.show和state.values都是新跟踪的(n为1)因此在finalizeDepMarkers处理后仍然将副作用函数保留在这两个属性对应的依赖集合中。执行state.values.push(4)触发副作用函数变化 a. effectTrackDepth为0因此1 effectTrackDepth得到的effectTrackDepth和trackOpBit均为1此时副作用函数已经收集过依赖因此initDepMarkers将该副作用函数所在的依赖集合都都标记为已收集过(w为1) b. 访问state.show时会调用trackEffects发现newTracked(dep)为未跟踪过(在finalizeDepMarkers中已被置零)则将n设置为1然后开始收集依赖 c. 访问state.values会重复第2步的操作 d. 由于state.show和state.values都是新跟踪的(n为1)因此在finalizeDepMarkers处理后仍然将副作用函数保留在这两个属性对应的依赖集合中。执行state.show false触发副作用函数变化 a. effectTrackDepth为0因此1 effectTrackDepth得到的effectTrackDepth和trackOpBit均为1此时副作用函数已经收集过依赖因此initDepMarkers将该副作用函数所在的依赖集合都都标记为已收集过(w为1) b. 访问state.show时会调用trackEffects发现newTracked(dep)为未跟踪过(在finalizeDepMarkers中已被置零)则将n设置为1然后开始收集依赖 c. 由于state.values没有标记为新跟踪的(n为0)因此在finalizeDepMarkers处理后会将副作用函数从state.values对应的依赖集合中移除仅保留在state.values对应的依赖集合中。
到这里我想大家已经对这个优化有更深的理解了。那么接下来的问题自然而然就是为什么要硬编码将优化算法启动的嵌套层级设置为maxMarkerBits 30
SMI优化原理
首先maxMarkerBits 30表示仅支持effect嵌套31层注释中描述该值是因为想让JavaScript影响使用SMI。那么什么是SMI呢
由于ECMAScript标准约定number数字需要转换为64位双精度浮点数处理但所有数字都用64位存储和处理是十分低效的所以V8内部采用其它内存表示方式如32位然后向外提供64位表现的特性即可。其中数组合法索引范围是[0, 2^32 - 2]V8引擎就是采用32位的方式来存储这些合法的下标数字。另外所有在[0, 2^32 - 2]内的数字都会优先使用32位二进制补码的方式存储。
针对32位有符号位范围内的整型数字V8为其定义了一种特殊的表示法SMI(非SMI的数字则被定义为HeapNumber)而V8引擎针对SMI启用特殊的优化当使用SMI内的数字时引擎不需要为其分配专门的内存实体并会启用快速整型操作。
对于非SMI的数字
let o {x: 42, // SMIy: 4.2 // HeapNumber
}
内存结构为HeapNumber{ value: 4.2, address: 1 }和JSObject{ x: 42, y: 1 }由于x值类型为SMI因此直接存储在对象上而y为HeapNumber则需要分配一个独立的内存空间存放并通过指针让对象的y属性指向HeapNumber实例的内存空间。
然而在修改值时然后x为SMI所以可以原地修改内存中的值而HeapNumber为不可变因此必须再分配一个新的内存空间存放新值并修改o.y中的内存地址。那么在没有启用Mutable HeapNumber时如下代码将产生1.1、1.2和1.33个临时实例。
let o { x: 1.1 }
for (let i 0; i 4; i) {o.x 1;
}
有SMI是带符号位的那么实际存储数字是31位因此设置maxMarkerBits 30且通过if (effectTrackDepth maxMarkerBits)判断层级即当effec嵌套到31层时不再使用无用依赖清理优化算法。而优化算法中采用的是二进制位对上一轮已收集和本轮收集的依赖进行比较从而清理无用依赖。若n和w值所占位数超过31位则内部会采用HeapNumber存储那么在位运算上性能将有所下降。
其实我们还看到若effectTrackDepth等于31时还会执行trackOpBit 1 effectTrackDepth这会导致trackOpBit从SMI的存储方式转换为HeapNumber那是不是可以加个判断修改成下面这样呢
const maxMarkerBit 1 30if (trackOpBit maxMarkerBit ! 1) {trackOpBit 1 effectTrackDepth
}
副作用函数触发器-trigger
由于在讲解优化无用依赖清理算法时已经对track进行了剖析因此现在我们直接分析trigger就好了。
export function trigger(target: object,// set, add, delete, cleartype: TriggerOpTypes,key?: unknown,newValue?: unknown,oldValue?: unknown,oldTarget?: Map | Set
) {const depsMap targetMap.get(target)if (!depsMap) {// 该属性没有被任何副作用函数跟踪过所以直接返回就好了return}/*** 用于存储将要被触发的副作用函数。* 为什么不直接通过类似depsMap.values().forEach(fn fn())执行副作用函数呢* 那是因为副作用函数执行时可能会删除或增加depsMap.values()的元素导致其中的副作用函数执行异常。* 因此用另一个变量存储将要执行的副作用函数集合那么执行过程中修改的是depsMap.values()的元素而正在遍历执行的副作用函数集合结构是稳定的。*/let deps: (Dep | undefined)[] []if (type TriggerOpTypes.CLEAR) {// 对象的所有属性值清空所有依赖该响应式对象的副作用函数都将被触发deps [...depsMap.values()]}else if (key length isArray(target)) {// 若设置length属性那么依赖length属性和索引值大于等于新的length属性值的元素的副作用函数都会被触发depsMap.forEach((dep, key) {if (key length || key (newValue as number)) {deps.push(dep)}})}else {// 将依赖该属性的if (key ! void 0) {// 即使插入的是undefined也没有关系deps.push(depsMap.get(key))}/*** 添加间接依赖的副作用函数* 1. 新增数组新值索引大于数组长度时会导致数组容量被扩充length属性也会发生变化* 2. 新增或删除Set/WeakSet/Map/WeakMap元素时需要触发依赖迭代器的副作用函数* 3. 新增或删除Map/WeakMap元素时需要触发依赖键迭代器的副作用函数* 4. 设置Map/WeakMap元素的值时需要触发依赖迭代器的副作用函数*/ switch(type) {case TriggerOpTypes.ADD:if (!isArray(target)) {// 对于非数组则触发通过迭代器遍历的副作用函数deps.push(depsMap.get(ITERATE\_KEY))if (isMap(target)) {deps.push(depsMap.get(MAP\_KEY\_ITERATE\_KEY))}}else if (isIntegerKey(key)) {// 对数组插入新元素则需要触发依赖length的副作用函数deps.push(depsMap.get(length))}breakcase TriggerOpTypes.DELETE:if (!isArray(target)) {// 对于非数组则触发通过迭代器遍历的副作用函数deps.push(depsMap.get(ITERATE\_KEY))if (isMap(target)) {deps.push(depsMap.get(MAP\_KEY\_ITERATE\_KEY))}}breakcase TriggerOpTypes.SET:// 对于Map/WeakMap需要触发依赖迭代器的副作用函数if (isMap(target)) {deps.push(depsMap.get(ITERATE\_KEY))}}if (deps.length 1) {// 过滤掉undefinedif (deps[0]) {triggerEffects(deps[0])}}else {const effects: ReactiveEffect[] []// 过滤掉undefinedfor (const dep of deps) {if (dep) {effects.push(...dep)}}triggerEffects(createDep(effects))}}
}export function triggerEffects(dep: Dep | ReactiveEffect[]
) {for (const effect of isArray(dep) ? dep : [...dep]) {/*** 必须保证将要触发的副作用函数(effect)不是当前运行的副作用函数(activeEffect)否则将嵌入无限递归。* 假设存在如下情况* let foo reactive({ bar: 1 })* effect(() {* foo.bar foo.bar 1* })* 若没有上述的保障则将会不断递归下去直接爆栈。* * 假如ReactiveEffect对象的allowRecurse设置为true那么表示不对上述问题作防御。*/ if (effect ! activeEffect || effect.allowRecurse) {if (effect.scheduler) {// 若设置有调度器则调用调用器effect.scheduler()}else {// 立即执行副作用函数effect.run()}}}
}
调度器
在上一节的triggerEffects中我们看到默认采用同步方式执行副作用函数若要同步执行数十个副作用函数那么势必会影响当前事件循环主逻辑的执行这时就是调度器闪亮登场的时候了。我们回顾以下petite-vue中提供的调度器吧!
import { effect as rawEffect } from vue/reactivityconst effect (fn) {const e: ReactiveEffectRunner rawEffect(fn, {scheduler: () queueJob(e)})return e
}
// ./scheduler.tslet queued false
const queue: Function[] []
const p Promise.resolve()export const nextTick (fn: () void) p.then(fn)export const queueJob (job: Function) {if (!queue.includes(job)) queue.push(job)if (!queued) {queued truenextTick(flushJobs)}
}const flushJobs () {for (const job of queue) {job()}queue.length 0queued false
}
副作用函数压入队列中并将遍历队列执行其中的副作用函数后清空队列的flushJobs压入micro queue。那么当前事件循环主逻辑执行完后JavaScript引擎将会执行micro queue中的所有任务。
什么是EffectScope
Vue 3.2引入新的Effect scope API可自动收集setup函数中创建的effect、watch和computed等当组件被销毁时自动销毁作用域(scope)和作用域下的这些实例(effect、watch和computed等)。这个API主要是提供给插件或库开发者们使用的日常开发不需要用到它。
还记得petite-vue中的context吗当遇到v-if和v-for就会为每个子分支创建新的block实例和新的context实例而子分支下的所有ReactiveEffect实例都将统一被对应的context实例管理当block实例被销毁则会对对应的context实例下的ReactiveEffect实例统统销毁。
block实例对应是DOM树中动态的部分可以大概对应上Vue组件而context实例就是这里的EffectScope对象了。
使用示例
cosnt scope effectScope()
scope.run(() {const state reactive({ value: 1 })effect(() {console.log(state.value)})
})
scope.stop()
那么effect生成的ReactiveEffect实例是如何和scope关联呢 那就是ReactiveEffect的构造函数中调用的recordEffectScope(this, scope)
export function recordEffectScope(effect: ReactiveEffect,scope?: EffectScope | null
) {// 默认将activeEffectScope和当前副作用函数绑定scope scope || activeEffectScopeif (scope scope.active) {scope.effects.push(effect)}
}
总结
petite-vue中使用*vue/reactivity的部分算是剖析完成了也许你会说vue/reactivity*可不止这些内容啊这些内容我将会在后续的《vue-lit源码剖析》中更详尽的梳理分析敬请期待。 下一篇我们将看看eval中是如何使用new Function和with来构造JavaScript解析执行环境的。 尊重原创转载请注明来自https://blog.csdn.net/fsjohnhuang/p/16163888.html肥仔John