nextTick用过吗?讲一讲实现思路吧
点击关注公众号,“技术干货”及时达!
源码实现思路(面试高分回答) ??面试官问我 Vue 的 nextTick 原理是怎么实现的,我这样回答:
在调用 this.$nextTick(cb) 之前:
存在一个 callbacks 数组,用于存放所有的 cb 回调函数。存在一个 flushCallbacks 函数,用于执行 callbacks 数组中的所有回调函数。存在一个 timerFunc 函数,用于将 flushCallbacks 函数添加到任务队列中。当调用 this.nextTick(cb) 时:
nextTick 会将 cb 回调函数添加到 callbacks 数组中。判断在当前事件循环中是否是第一次调用 nextTick:如果是第一次调用,将执行 timerFunc 函数,添加 flushCallbacks 到任务队列。如果不是第一次调用,直接下一步。如果没有传递 cb 回调函数,则返回一个 Promise 实例。
根据上述描述,对应的`流程图`如下:
graph TD
A["this.$nextTick(callback)"] -- B[将回调函数 callback 放入到数组 callbacks 中]
B -- C[判断是否是第一次调用 nextTick]
C --|是| D[执行 timerFunc, 将 flushCallbacks 添加到任务队列]
C --|否| F[如果没有 cb, 则retrun Promise]
D -- F
F -- 结束
如果上面的描述没有很理解。没关系,花几分钟跟着我下面来,看完下面的源码逐行讲解,你一定能够清晰地向别人讲出你的思路!
nextTick思路详解 ???♂??1. 核心代码 ??下面用十几行代码,就已经可以基本实现「nextTick」的功能(默认浏览器支持「Promise」)
//存储所有的cb回调函数
constcallbacks=
/*类似于节流的标记位,标记是否处于节流状态。防止重复推送任务*/
letpending=false;
/*遍历执行数组callbacks中的所有存储的cb回调函数*/
functionflushCallbacks(){
//重置标记,允许下一个nextTick调用
pending=false;
/*执行所有cb回调函数*/
for(leti=0;icallbacks.length;i++){
callbacks[i]();
}
//清空回调数组,为下一次调用做准备
callbacks.length=0;
}
functionnextTick(cb){
//将回调函数cb添加到callbacks数组中
callbacks.push(()={
cb();
//第一次使用nextTick时,pending为false,下面的代码才会执行
if(!pending){
//改变标记位的值,如果有flushCallbacks被推送到任务队列中去则不需要重复推送
pending=true;
//使用Promise机制,将flushCallbacks推送到任务队列
Promise.resolve().then(flushCallbacks);
}
}
「测试一下:」
letmessage='初始消息';
nextTick(()={
message='更新后的消息';
console.log('回调:',message);//输出2:更新后的消息
});
console.log('测试开始:',message);//输出1:初始消息
如果你想要应付面试官,能手写这部分核心原理就已经差不多啦。
如果你想彻底掌握它,请继续跟着我来!!!?????♂
2. nextTick() 返回promise ??我们在开发中,会使用await this.$nextTick();让其下面的代码全部变成异步代码。
比如写成这样:
awaitthis.$nextTick();
......
......
//或者
this.$nextTick().then(()={
......
})
核心就是nextTick()如果没有参数,则返回一个promise
constcallbacks=
letpending=false;
functionflushCallbacks(){
pending=false;
for(leti=0;icallbacks.length;i++){
callbacks[i]();
}
callbacks.length=0;
}
functionnextTick(cb){
//用于存储Promise的resolve函数
let_resolve;
callbacks.push(()={
/*------------------新增start------------------*/
//如果有cb回调函数,将cb存储到callbacks
if(cb){
cb();
}elseif(_resolve){
//如果参数cb不存在,则保存promise的的成功回调resolve
_resolve();
}
/*------------------新增end------------------*/
if(!pending){
pending=true;
Promise.resolve().then(flushCallbacks);
}
/*------------------新增start------------------*/
if(!cb){
returnnewPromise((resolve,reject)={
//保存resolve到callbacks数组中
_resolve=resolve;
}
/*------------------新增end------------------*/
}
「测试一下:」
asyncfunctiontestNextTick(){
letmessage="初始消息";
nextTick(()={
message="更新后的消息";
console.log("传入回调:",message);//输出1:初始消息
//不传入回调的情况
awaitnextTick();//nextTick返回Promise
console.log("未传入回调后:",message);//输出2:更新后的消息
}
//运行测试
testNextTick();
3. 判断浏览器环境 ??为了防止浏览器不支持 「Promise」,「Vue」 选择了多种 API 来实现兼容 「nextTick」:
Promise -- MutationObserver -- setImmediate -- setTimeout
「Promise」 (微任务):
如果当前环境支持 「Promise」,「Vue」 会使用 Promise.resolve().then(flushCallbacks)
「MutationObserver」 (微任务):
如果不支持 「Promise」,支持 「MutationObserver」。「Vue」 会创建一个 「MutationObserver」 实例,通过监听文本节点的变化来触发执行回调函数。
「setImmediate」 (宏任务):
如果前两者都不支持,支持 「setImmediate」。则:setImmediate(flushCallbacks)
注意:「setImmediate」 在绝大多数浏览器中不被支持,但在 「Node.js」 中是可用的。
「setTimeout」 (宏任务):
如果前面所有的都不支持,那你的浏览器一定支持 「setTimeout」!!!
终极方案:setTimeout(flushCallbacks, 0)
//存储所有的回调函数
constcallbacks=
/*类似于节流的标记位,标记是否处于节流状态。防止重复推送任务*/
letpending=false;
/*遍历执行数组callbacks中的所有存储的cb回调函数*/
functionflushCallbacks(){
//重置标记,允许下一个nextTick调用
pending=false;
/*执行所有cb回调函数*/
for(leti=0;icallbacks.length;i++){
callbacks[i]();//依次调用存储的回调函数
}
//清空回调数组,为下一次调用做准备
callbacks.length=0;
}
//判断最终支持的API:Promise/MutationObserver/setImmediate/setTimeout
lettimerFunc;
if(typeofPromise!=="undefined"){
//创建一个已resolve的Promise实例
varp=Promise.resolve();
//定义timerFunc为使用Promise的方式调度flushCallbacks
timerFunc=()={
//使用p.then方法将flushCallbacks推送到微任务队列
p.then(flushCallbacks);
}elseif(
typeofMutationObserver!=="undefined"&&
MutationObserver.toString()==="[objectMutationObserverConstructor]"
){
/*新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数。
在DOM变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),
即textNode.data=String(counter)时便会加入该回调*/
varcounter=1;//用于切换文本节点的值
varobserver=newMutationObserver(flushCallbacks);//创建MutationObserver实例
vartextNode=document.createTextNode(String(counter));//创建文本节点
observer.observe(textNode,{
characterData:true,//监听文本节点的变化
//定义timerFunc为使用MutationObserver的方式调度flushCallbacks
timerFunc=()={
counter=(counter+1)%2;//切换counter的值(0或1)
textNode.data=String(counter);//更新文本节点以触发观察者
}elseif(typeofsetImmediate!=="undefined"){
/*使用setImmediate将回调推入任务队列尾部*/
timerFunc=()={
setImmediate(flushCallbacks);//将flushCallbacks推送到宏任务队列
}else{
/*使用setTimeout将回调推入任务队列尾部*/
timerFunc=()={
setTimeout(flushCallbacks,0);//将flushCallbacks推送到宏任务队列
}
functionnextTick(cb){
//用于存储Promise的解析函数
let_resolve;
//将回调函数cb添加到callbacks数组中
callbacks.push(()={
//如果有cb回调函数,将cb存储到callbacks
if(cb){
cb();
}elseif(_resolve){
//如果参数cb不存在,则保存Promise的成功回调resolve
_resolve();
}
//第一次使用nextTick时,pending为false,下面的代码才会执行
if(!pending){
//改变标记位的值,如果有nextTickHandler被推送到任务队列中去则不需要重复推送
pending=true;
//调用timerFunc,将flushCallbacks推送到合适的任务队列
timerFunc(flushCallbacks);
}
//如果没有cb且环境支持Promise,则返回一个Promise
if(!cbtypeofPromise!=="undefined"){
returnnewPromise((resolve)={
//保存resolve到callbacks数组中
_resolve=resolve;
}
}
你真的太牛了,居然几乎全部看完了!
Vue纯源码上面的代码实现,对于 「nextTick」 功能已经非常完整了,接下来我将给你展示出 「Vue」 中实现 「nextTick」 的完整源码。无非是加了一些判断变量是否存在的判断。看完上面的讲解,我相信聪明的你一定能理解 「Vue」 实现 「nextTick」 的源码了吧!??
//存储所有的cb回调函数
constcallbacks=
/*类似于节流的标记位,标记是否处于节流状态。防止重复推送任务*/
letpending=false;
/*遍历执行数组callbacks中的所有存储的cb回调函数*/
functionflushCallbacks(){
pending=false;//重置标记,允许下一个nextTick调用
constcopies=callbacks.slice(0);//复制当前的callbacks数组
callbacks.length=0;//清空callbacks数组
for(leti=0;icopies.length;i++){
copies[i]();//执行每一个存储的回调函数
}
}
//判断是否为原生实现的函数
functionisNative(Ctor){
//如Promise.toString()为'functionPromise(){[nativecode]}'
returntypeofCtor==="function"/nativecode/.test(Ctor.toString());
}
//判断最终支持的API:Promise/MutationObserver/setImmediate/setTimeout
lettimerFunc;
if(typeofPromise!=="undefined"isNative(Promise)){
constp=Promise.resolve();//创建一个已解决的Promise实例
timerFunc=()={
p.then(flushCallbacks);//使用p.then将flushCallbacks推送到微任务队列
//在某些有问题的UIWebView中,Promise.then并不会完全失效,
//但可能会陷入一种奇怪的状态:回调函数被添加到微任务队列中,
//但队列并没有被执行,直到浏览器需要处理其他工作,比如定时器。
//因此,我们可以通过添加一个空的定时器来“强制”执行微任务队列。
if(isIOS)setTimeout(()=//解决iOS的bug,推迟空函数的执行(如果不理解,建议忽略)
}elseif(
typeofMutationObserver!=="undefined"&&
(isNative(MutationObserver)||
MutationObserver.toString()==="[objectMutationObserverConstructor]")
){
letcounter=1;//用于切换文本节点的值
constobserver=newMutationObserver(flushCallbacks);//创建MutationObserver实例
consttextNode=document.createTextNode(String(counter));//创建文本节点
observer.observe(textNode,{
characterData:true,//监听文本节点的变化
//定义timerFunc为使用MutationObserver的方式调度flushCallbacks
timerFunc=()={
counter=(counter+1)%2;//切换counter的值(0或1)
textNode.data=String(counter);//更新文本节点以触发观察者
}elseif(typeofsetImmediate!=="undefined"isNative(setImmediate)){
timerFunc=()={
setImmediate(flushCallbacks);//使用setImmediate推送到任务队列
}else{
timerFunc=()={
setTimeout(flushCallbacks,0);//使用setTimeout推送到宏任务队列
}
functionnextTick(cb,ctx){
let_resolve;//用于存储Promise的解析函数
//将回调函数cb添加到callbacks数组中
callbacks.push(()={
if(cb){
try{
cb.call(ctx);//执行传入的回调函数
}catch(e){
handleError(e,ctx,"nextTick");//错误处理
}
}elseif(_resolve){
_resolve(ctx);//解析Promise
}
//第一次使用nextTick时,pending为false,下面的代码才会执行
if(!pending){
pending=true;//改变标记位的值
timerFunc();//调用timerFunc,调度flushCallbacks
}
//如果没有cb且环境支持Promise,则返回一个Promise
if(!cbtypeofPromise!=="undefined"){
returnnewPromise((resolve)={
_resolve=resolve;//存储解析函数
}
}
总结通过这样分成三步、循序渐进的方式,我们深入探讨了 「nextTick」 的原理和实现机制。希望这篇文章能够对你有所帮助,让你在前端开发的道路上更加得心应手!??
点击关注公众号,“技术干货”及时达!
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线