1.Vue.nextTick原理分析
2.Async、微任务源Await 从源码层面解析其工作原理
3.vue3-computed源码解析
4.深入p-limit源码,微任务源如何限制并发数?
5.Vue异步更新机制以及$nextTick原理
6.手写 p-limit,微任务源40 行代码实现并发控制
Vue.nextTick原理分析
了解 Vue 的微任务源 nextTick 是如何实现的,首先需要回顾一下 JavaScript 的微任务源运行机制。
JavaScript 是微任务源1.50火炬引擎源码单线程执行的,通过事件循环机制进行任务调度。微任务源具体流程如下:当执行栈为空时,微任务源检查微任务队列,微任务源执行全部微任务,微任务源之后执行宏任务队列中的微任务源事件。
在 JavaScript 中,微任务源常见的微任务源宏任务包括 setTimeout, MessageChannel, postMessage 和 setImmediate,而微任务则包括 MutationObserver 和 Promise.then。微任务源
Vue 的微任务源 nextTick 实现细节在 2.6. 版本后被单独封装在 src/core/util/next-tick.js 文件中,源码简洁,约 多行。其核心原理在于,同一 tick 内添加的任务会在下一个 tick 执行,避免创建多个异步任务。
当调用 nextTick 时不传入回调函数,会返回一个 Promise 化的调用,当内部 resolve 函数执行,即进入 then 的逻辑。
总结,nextTick 的实现和使用方法在源码分析后变得清晰。在 Vue 开发中,通过 nextTick 可以确保在数据响应变化后获取最新的 DOM 更新,避免了同步操作导致的渲染冲突问题。
Async、Await 从源码层面解析其工作原理
深入理解 Async 和 Await 的工作原理,往往需要从源码层面进行剖析。使用 Babel 进行转换后,可以清晰地发现 Async 和 Await 实际上借助了 switch-case 和 promise,实现对流程的控制。以一个使用 Async 和 Await 的函数为例,我们仅关注核心部分代码。
经过 Babel 转换后的 name 函数,可以被拆分为三个主要部分:await 部分、return 部分以及 async 流程控制的语音交友系统源码结束部分(即 case "end")。这个拆分使得流程控制变得更为直观。在流程控制中,每一步执行后,都会等待合适的时机进入下一次执行。
这个“合适的时机”并非由 Async 内部决定,而是由执行的内容决定。例如,在发送异步请求后,只有在请求返回后才会进入下一个 case。
为了实现流程控制,需要借助 regenerator-runtime 这个 generator、Async 函数的运行时。它负责将 name 函数进行包装,并添加流程控制所需的信息。如 _context,以及用于流程控制的关键 helper,如 _asyncToGenerator 和 asyncGeneratorStep。通过这些辅助工具,再在 regenerator-runtime 的基础上进行一层包装,最终得到一个可以执行的函数。这个函数实际执行时,会调用封装后的函数。
在封装后的函数中,async1、async2 等实际上是在执行最终的封装函数内部的调用。这里的第三步是 Async 函数的核心机制。在 Promise.resolve(value).then(_next) 中,value 是每个分段最后的 case 返回的值。如果 value 是一个 Promise,那么在它 resolved 后,会将其.then添加到微任务队列。如果 value 不是一个 Promise,则直接添加,因为.then是一个微任务,当执行到它时,会调用_next,从而开始执行下一个 case。
经过转换后的代码展示了封装后的函数内容,最终执行的工作证源码是封装后的函数,因此说 async1、async2 执行实际上是执行封装后的函数。在封装后的函数内部,会调用 async1、async2。
vue3-computed源码解析
在 Vue 3 中,理解 computed 源码有助于深入掌握其工作原理。版本为 3.2.,通过单例测试和官网文档,我们了解到 computed 的主要特性是基于 getter 函数创建,类似于一个只读的响应式值,其更新依赖于传入的 getter 函数,而非直接修改.value属性。其核心逻辑与 ref 类似,利用 dep 和 trackRefValue/triggerRefValue 函数实现响应式。
计算属性的实现分为两种:通过 computed 函数或 deferredComputed。两者都是 ref 类型,但 deferredComputed 在 effect 中具有异步特性,只有在下一次微任务中才会更新。在 Vue 中,通过ComputedRefImpl 对象管理计算属性,它使用 _value 和 _dirty 机制实现懒加载,当数据改变时,会触发收集函数并更新缓存值。
在源码中,可以看到计算属性的 getter 被包装在 effect 中,依赖数据变化时会通过调度器来触发收集。但需要注意的是,当在 effect 内先改变依赖,再改变外部的计算属性,可能会导致异常。对于 deferredComputed,其调度器更为复杂,会在下一次微任务执行时处理异步更新。
虽然 deferredComputed 的处理存在一些特殊情况,如在微任务期间的值比较问题,但 Vue 通过缓存相关 effect 的值,以及 hasCompareTarget 变量,确保了异步更新的c库链表源码正确性。至此,我们已经详细了解了 Vue3 computed 的源码实现,接下来可以继续探索 effectScope 的源码。
上一章:vue3-ref源码解析
下一章:vue3-effectScope源码解析
深入p-limit源码,如何限制并发数?
并发处理在现代编程中扮演着至关重要的角色,尤其在异步操作和并行任务处理中。虽然JavaScript是单线程执行的,但它通过Promise.all等API实现了并发效果,允许同时处理多个异步操作。
Promise.all是Promise库中的一个关键函数,它接受一个Promise数组作为参数。此函数会等待所有给定的Promise实例全部完成或其中一个失败,然后返回一个新Promise的数组结果。如果所有Promise都成功,则返回所有成功结果的数组;如果一个或多个Promise被拒绝,则返回第一个拒绝的Promise的reason。
然而,有时并发操作需要被限制。过多的并发请求可能给服务器带来压力,影响性能。这时候,p-limit库就显得尤为重要,它允许我们为并发操作设置一个上限。
p-limit提供了pLimit函数来定义并发限制。使用pLimit时,你可以传入一个数量参数,这个参数决定了同时可以执行的异步任务数量。函数返回一个新函数,该函数接收需要并发执行的异步任务。当执行队列中的任务数量达到上限时,新传入的任务会被加入队列,等待前面的任务释放资源后执行。
p-limit的实现中,核心在于初始化一个计数器和一个任务队列。队列采用了yocto-queue库实现,它提供了一个基于链表的队列结构。在并发处理过程中,p-limit通过enqueue函数将异步任务入队,并在队列中管理任务的iapp群发源码执行顺序和限制。
enqueue函数负责将异步任务入队,同时对任务进行包装和控制,确保任务在队列中按顺序执行,且不会超过指定的并发限制。这通过使用async函数实现,以确保等待下一个微任务的到来,从而在异步更新的activeCount值上进行比较,以维持并发限制。
在实际执行时,每个任务的执行由run函数控制。此函数在内部管理并发计数,并在任务完成后执行下一个任务,确保并发限制被严格遵守。enqueue、run和next三个函数协同工作,构成了p-limit中一个动态、有限的异步任务执行流程。
此外,p-limit还包含了辅助函数用于管理任务状态,如获取当前执行任务数量(activeCount)、队列中等待任务数量(pendingCount)以及清空任务队列(clearQueue)。这些功能共同协作,确保并发处理既高效又可控。
通过p-limit库,开发人员能够轻松实现异步操作的并发控制,优化性能并防止服务器过载。了解其内部机制,能更好地利用并发处理技术,提升应用响应速度和用户体验。
Vue异步更新机制以及$nextTick原理
对于Vue的内部工作机制,很多人可能会对异步更新机制和$nextTick的原理感到好奇。今天,我们将深入探讨这两个关键点。
Vue的更新策略是异步的:一旦数据变化,Vue会将这些改变放入一个队列,只有在下一个事件循环(也称为tick)中才会执行实际的更新操作。这样做可以避免不必要的重复计算和DOM操作,提高了性能。例如,即使一个watcher多次被触发,它只会被添加到队列一次。
当你在数据变化后立即尝试获取DOM,可能会发现获取的是旧数据,这是因为DOM更新是异步的,直到下一个事件循环结束,Vue才会真正渲染出更新后的视图。这就是$nextTick的作用,它会在下一次事件循环中执行,确保数据已经更新完毕。
Vue的DOM更新实际上是批量进行的,这有助于优化性能。例如,即使同时更新多个数据,DOM也会在一次渲染中完成。在实际操作中,$nextTick确保了更新过程的同步性,尤其是在处理宏任务和微任务时。
事件循环机制是理解这一切的关键,JavaScript的任务分为同步和异步,Vue利用微任务的特性,确保DOM在更新完成后立即可见。在Vue源码中,$nextTick实际是通过promise.then或MutationObserver等机制实现异步回调。
总结来说,Vue的异步更新机制通过队列缓存数据变化,直到事件循环的下一次渲染,以提高性能。$nextTick则是这个机制的一个工具,用于确保在用户代码执行完毕后,DOM能够快速响应最新的数据变化。
手写 p-limit, 行代码实现并发控制
前端代码中,经常涉及异步逻辑的处理,这类逻辑可能串行执行,也可能并行执行。并行执行的逻辑通常需要进行并发控制,这是编程中常见需求,也是面试题的常考点。
通常我们会使用 p-limit 这样的工具来实现并发控制,例如,下面这段逻辑就是几个异步操作同时执行,且最大并发数限制为2。
那如何自行实现并发控制功能呢?
首先,需要创建一个函数,该函数接收并发数量参数,并返回一个添加并发任务的函数,我们将其命名为 generator。
在 generator 中,我们将添加的并发任务放入队列中,同时记录当前执行中的异步任务数量。
当任务入队后,会检查是否达到了并发上限,如未达到,继续执行更多任务。
具体实现逻辑如下,当任务执行时,计数并改变返回的 promise 状态,然后执行完成后,减少活跃任务数量并执行下一个任务,以此确保并发数限制。
现在,我们有了一段仅行代码的并发控制实现。
接下来,通过测试代码验证其效果。
测试代码使用 setTimeout 和 promise 实现,设置不同的延迟时间,并发数设置为2。经过测试,结果符合预期:首先并发执行前两个任务,当第一个任务执行完成2秒后,又执行了一个任务,再过一秒,所有任务执行完毕,同时执行了两个任务。
通过测试,我们确认实现了并发控制功能。
回顾整个实现过程,其实就是在队列中保存任务,初始时一次性执行最大并发数的任务,然后每完成一个任务即执行下一个。
此实现实现过程相对简单,但可以进一步优化,比如暴露并发数、提供任务队列清理功能等。
优化后的代码如下,使用 Object.defineProperties 定义只读属性 activeCount 和 pendingCount,并提供清理任务队列的函数。同时,对传入参数进行校验,确保其为整数且非负,Infinity 亦被允许。
优化还涉及确保并发数量准确控制,确保在所有微任务执行完毕后再获取 activeCount。这可以通过在关键逻辑中加入 await Promise.resolve() 实现。
实现并发控制功能的完整代码已通过余行代码实现,这便是 p-limit 源码的简化版本。感兴趣的同学可以自行尝试实现。
总结,js 代码在处理异步逻辑时,常需实现串行、并行执行,并进行并发控制。通过队列管理任务,初始时批量执行最大并发数的任务,每完成一个任务即执行下一个,确保并发控制的实现。此外,确保获取任务数量的准确性,需要在所有微任务执行完毕后获取 activeCount。通过余行代码即可实现并发控制功能,这与 p-limit 的实现原理相似,有兴趣的开发者可以自行尝试。
从规范面解读:Promises/A+规范与浏览器Promise规范有何区别?
前言
Promise是一种优秀的异步解决方案,其原生实现更是面试中的爆点,提到Promise实现,我们首先会想起Promises/A+规范,大多数教程中都是按照Promises/A+规范来实现Promise。
小包也是Promises/A+圣经的执行者之一,但小包心中一直有个好奇,遵循Promises/A+规范实现的Promise与ES6-Promise能有什么区别呐?
文章中的测试代码选取小包基于Promises/A+规范实现的原生Promise
学习本文,你能收获:
进一步完善原生Promise的实现
更进一步理解Promise与microTask之间的关系
promise的成功值valuePromises/A+规范只提供了value的定义,并没有详细说明如何处理不同类型的value值:
“value”isanylegalJavaScriptvalue(including?undefined,athenable,orapromise).value可以是任意合法的JavaScript值,包括undefined、具备then接口的对象或者promise
但ECMAScript规范对不同类型的value做了细致的处理。
红框部分我们可以看出,ES6规范会根据resolution(相当于Promises/A+规范中的value)类型选取不同的执行方案。
判断resolution是否为Object,如果不是,直接执行FulfillPromise
如果是Object,试探是否存在then接口
判断then是否可执行(abruptcompletion可以理解为非正常值)
如果then可执行,将then方法放入事件队列中。
PromiseResolveThenableJob:该job使用传入的thenable的then方法来解决promise。
一句话总结上面的过程:如果value值为可thenable对象或者promise,ES6会采用该thenable的状态。
小包举个栗子:
const?p?=?new?Promise((resolve)?=>?{ ?resolve(1);});const?p1?=?new?Promise((resolve)?=>?{ ?resolve(p);});p1.then((d)?=>?console.log(d));p1接收的成功值value为Promisep,p状态为fulfilled,这种情况下ES6中会采取p的状态及value,因此最终打印1。
我们将p更换为具备thenable对象,结果也是类似的。
//?类?promise?对象const?p1?=?{ ?a:?1,?then(onFulfilled,?onReject)?{ onFulfilled(this.a);?},};const?p2?=?new?Promise((resolve)?=>?{ ?resolve(p1);});//?1p2.then((d)?=>?console.log(d));Promises/A+没有对此进行规范,因此当传入的value为thenable对象时,会原封不动的输出。
那我们应该如何完善这部分代码呐?我们需要对value值进行解析,如果value可thenable,则采纳他的状态和值,递归进行上述步骤,直至value不可thenable。(这里与resolvePromise部分递归解析onFulfilled函数的返回值是类似的)
const?resolve?=?(value)?=>?{ ?if?(typeof?value?===?"object"?&&?value?!=?null)?{ try?{ ?const?then?=?value.then;?if?(typeof?then?===?"function")?{ return?then.call(value,?resolve,?reject);?}}?catch?(e)?{ ?return?reject(e);}?}?if?(this.status?===?PENDING)?{ this.value?=?value;this.status?=?FULFILLED;this.onFulfilledCallbacks.forEach((cb)?=>?cb(this.value));?}};Promise与microTaskPromises/A+规范中其实并没有将Promise对象与microTask挂钩,规范是这么说的:
Here“platformcode”meansengine,environment,andpromiseimplementationcode.Inpractice,thisrequirementensuresthat?onFulfilled?and?onRejected?executeasynchronously,aftertheeventloopturninwhich?then?iscalled,andwithafreshstack.Thiscanbeimplementedwitheithera“macro-task”mechanismsuchas?setTimeout?or?setImmediate,orwitha“micro-task”mechanismsuchas?MutationObserver?or?process.nextTick.Sincethepromiseimplementationisconsideredplatformcode,itmayitselfcontainatask-schedulingqueueor“trampoline”inwhichthehandlersarecalled.
Promises/A+规范中表示then方法可以通过setTimeout或setImediate等宏任务机制实现,也可以通过MutationObserver或process.nextTick等微任务机制实现。
但经过大量面试题洗礼的我们知道浏览器中的Promise.then典型的微任务。既然都学到这里了,小包索性就打破砂锅问到底,找到Promise与microTask挂钩的根源。
谁规定了Promise是microTask标准读起来属实有些无聊,但好在小包找到了最终的答案。
首先小包先入为主的以为,Promise的详细规定应该都位于ECMAScript制定的规范中,但当小包进入标准后,全局搜索micro,竟然只搜索到三个Microsoft。讲实话,小包是震惊的,ECMAScript并没有规定Promise是microTask。
ECMAScript规范中,最接近的是下面两段表达:
The?host-defined?abstractoperationHostEnqueuePromiseJobtakesarguments?job?(a?Job?AbstractClosure)and?realm?(a?RealmRecord?or?null)andreturns?unused.Itschedules?job?tobeperformedatsomefuturetime.The?AbstractClosures?usedwiththisalgorithmareintendedtoberelatedtothehandlingofPromises,orotherwise,tobescheduledwithequalprioritytoPromisehandlingoperations.
JobsarescheduledforexecutionbyECMAScripthostenvironments.ThisspecificationdescribesthehosthookHostEnqueuePromiseJobtoscheduleonekindofjob;hostsmaydefineadditionalabstractoperationswhichschedulejobs.SuchoperationsacceptaJobAbstractClosureastheparameterandscheduleittobeperformedatsomefuturetime.Theirimplementationsmustconformtothefollowingrequirements:
上面两句话意思大约是:ECMAScript中将Promise看作一个job(作业),HostEnqueuePromiseJob是用来调度Promise作业的方法,这个方法会在未来某个时间段执行,具体执行与Promise的处理函数或者与Promise处理操作相同的优先级有关。
那何处将Promise规定为microTask呐?---HTML标准
HTML标准中指出:
JavaScriptcontainsan?implementation-defined?HostEnqueuePromiseJob(job,?realm)abstractoperationtoschedulePromise-relatedoperations.HTMLschedulestheseoperationsinthemicrotaskqueue.
上述标准的最后一句话指出,HTML将在microqueue中安排这些操作。破案了,原来是HTML标准中将Promise规定为microTask。(为什么会是HTML进行规定,小包还没有探究出来)
更深入的区别,请参考月夕大佬:V8Promise源码全面解读
后语我是?战场小包?,一个快速成长中的小前端,希望可以和大家一起进步。
如果喜欢小包,可以在?掘金?关注我,同样也可以关注我的小小公众号——小包学前端。
一路加油,冲向未来!!!
疫情早日结束人间恢复太平原文:/post/