纯享版代码
先看一下纯享版代码,看不懂没关系,下面无偿带看&详解
export const nextTick = (function () {
var callbacks = []
var pending = false
var timerFunc
function nextTickHandler () {
pending = false
var copies = callbacks.slice(0)
callbacks = []
for (var i = 0; i < copies.length; i++) {
copies[i]()
}
}
/* istanbul ignore if */
if (typeof MutationObserver !== 'undefined' && !hasMutationObserverBug) {
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(counter)
observer.observe(textNode, {
characterData: true
})
timerFunc = function () {
counter = (counter + 1) % 2 // (1+1)%2=0 (0+1)%2=1 求余数
textNode.data = counter
}
} else {
// webpack attempts to inject a shim for setImmediate
// if it is used as a global, so we have to work around that to
// avoid bundling unnecessary code.
const context = inBrowser
? window
: typeof global !== 'undefined' ? global : {}
timerFunc = context.setImmediate || setTimeout
}
return function (cb, ctx) {
var func = ctx
? function () { cb.call(ctx) }
: cb
callbacks.push(func)
if (pending) return
pending = true
timerFunc(nextTickHandler, 0)
}
})()
解析
1、函数返回值
看一个函数首先看返回值
。nextTick
的返回值如下:
return function (cb, ctx) {
var func = ctx
? function () { cb.call(ctx) }
: cb
callbacks.push(func)
if (pending) return
pending = true
timerFunc(nextTickHandler, 0)
}
可以看到,返回了一个匿名函数
。
首先看函数的参数
:cb
是一个函数
,见名思义为callback
的缩写。后面 cb.call(ctx) 也印证了这点。ctx
是一个 执行上下文
,如this
一般的执行上下文
,是context
的缩写。
然后看函数内部:
1.1、定义func
var func = ctx
? function () { cb.call(ctx) }
: cb
如果传入了参数 ctx
(执行上下文),则创建一个新函数 func
,该函数将传入的cb
回调函数放在ctx
执行上下文上执行。
如果没有传入ctx
,则直接将 cb
函数赋值给 func
。
(此时cb
并未执行,此步骤仅规范cb
即将执行的时候的执行上下文
)
1.2、
callbacks.push(func)
将步骤1中定义好的func
函数加入到callbacks
里面(看起来像是一个回调函数数组)
1.3、
if (pending) return
pending
(汉译:“待处理的”)。
如果待处理则直接返回???不确定,再看看。
1.4、
pending = true
timerFunc(nextTickHandler, 0)
如果pending
为false
,则将其置为 true
,并调用方法:timerFunc(nextTickHandler, 0)
.
现在的疑问变为:
timerFunc、nextTickHandler 是什么?
2、timerFunc、nextTickHandler 是什么?
2.1、定义变量
回看函数开头,可以溯一下源:
var callbacks = []
var pending = false
var timerFunc
function nextTickHandler () {
...
}
可以看到三个var
定义了三个变量,我们要找的timerFunc
也在其中,并且timerFunc
是声明,但未定义,更没有执行。
后面定义了一个函数,就是我们要找的nextTickHandler
。声明,定义,但未执行。所以我们也不细看,等用到再说。接着向下看。
2.2、if…else代码块
if (typeof MutationObserver !== 'undefined' && !hasMutationObserverBug) {
...
} else {
...
}
是一个if…else
句式,先看下里面的判断条件
:
首先是MutationObserver
,想要详细了解可以点进这个链接 MDN # MutationObserver,
简略总结其作用就是:监视DOM在某方面的更改
,在目标DOM发生属性、新增、删除、文本修改等变化的时候,可以做出相应的响应
。
后面的!hasMutationObserverBug
也不作详解,从参考文章中引入一下对该变量的解释:
ios9.3以上的
WebView
的MutationObserver
有 bug ,所以在
hasMutationObserverBug
中存放了是否是这种情况
总结一下,该判断的目的:
保证MutationObserver
存在且可用(没有bug)。
2.3、解析if…else代码块
我们可以大致看到,if...else
代码块里面的代码,是在不同环境(支持 MutationObserver 的环境、不支持的环境)下的操作处理。
既如此,两个代码块里面实现的功能大同小异,只是具体实现细节有差别。
我们分开来看:
2.3.1、解析 if
先看 if
代码块里面的代码
var counter = 1
首先定义了一个计数器 counter
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(counter)
observer.observe(textNode, {
characterData: true
})
这段如同这里( MDN # MutationObserver)的例子
创建一个观察器实例observer
并传入回调函数nextTickHandler
(这里用到了nextTickHandler,先不详解,在2.4里面详解,别着急),
通过JS创建一个DOM实例(文本节点)
,其文本内容为counter
的值。
以{ characterData: true }
配置开始观察目标节点textNode
。
characterData
可选
当为true
时,监听声明的target
节点上所有字符的变化。
timerFunc = function () {
counter = (counter + 1) % 2 // (1+1)%2=0 (0+1)%2=1 求余数
textNode.data = counter
}
在函数开头声明但未定义
的timerFunc
在此处也被声明了值:一个函数
。
作用是:
通过变更counter
的值,来变更textNode
的文本。
而观察器observer
始终观测目标节点textNode
的 “所有字符的变化” 。
因此触发观察器的回调函数nextTickHandler
。
总结:
通过变更 counter 的值,触发回调函数 nextTickHandler
目的:timerFunc
是为了将nextTickHandler
放进微任务队列中执行
参考文章中说的原话:MutationObserver(MO的回调放进的是microtask的任务队列中的)
2.3.2、解析else
const context = inBrowser
? window
: typeof global !== 'undefined' ? global : {}
timerFunc = context.setImmediate || setTimeout
初始化执行上下文➕定义timerFunc
函数。
首先检查是否在浏览器环境中(inBrowser 是 Boolean 类型),
如果是,则将window
作为执行上下文。
否则,检查是否在 Node.js 等非浏览器环境中,
如果是,则使用 global
对象作为上下文;
如果以上两者都不是,则使用一个空对象
作为上下文。
没有满足第一个 if 的条件,则退而求其次,用 setImmediate/setTimeout 实现功能。
setImmediate方法是Node.js环境特有的。setTimeout在几乎所有JavaScript环境中都被支持。
timerFunc 其实就是 setImmediate/setTimeout。
timerFunc 接收的参数其实就是setImmediate/setTimeout 接收的参数。
总结:timerFunc
是 context.setImmediate || setTimeout
目的:timerFunc
是为了将通过 timerFunc(cb) 执行的 cb 放进宏任务队列中执行
2.3.3、年终大总结——timerFunc作用
timerFunc作用 |
---|
timerFunc 是为了将函数 通过某种手段放进宏/微任务队列中执行(变成异步任务 ) |
2.4、nextTickHandler
之前看到定义但未使用的函数nextTickHandler
,也没细说,这里就详细说说:
先看定义的 nextTickHandler :
function nextTickHandler () {
pending = false
var copies = callbacks.slice(0)
callbacks = []
for (var i = 0; i < copies.length; i++) {
copies[i]()
}
}
解读一下代码内容:
将 pending
置为false
。
将callbacks
浅拷贝为copies
后,将原数组callbacks
置为空数组[]
。
循环遍历执行copies
中的方法
总结:
主要内容是 循环执行 copies 中的函数。
2.4.1、哪里用到了 nextTickHandler
在看哪里用到了 nextTickHandler:
我们分成两条线来看:
【第一条线】,在 if
代码块中,首先出现了nextTickHandler
,但是要注意被调用可不是在这里。
出现 nextTickHandler:
(OK Fine,让我们把代码放过来重新看一遍)
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(counter)
observer.observe(textNode, {
characterData: true
})
她会在观察器目标对象发生变化时的作为观察器的回调函数被调用,
而仅当timerFunc
被调用时,目标对象才会发生变化。
即在此处调用时,目标对象才会发生变化:
if (pending) return
pending = true
timerFunc(nextTickHandler, 0)
形如timerFunc()
的调用即可。
(后面解释为什么代码里会给timerFunc加上参数、写成“timerFunc(nextTickHandler, 0)
”的“冗余”样子)
【第二条线】,在else
代码块中。虽然没有直接调用nextTickHandler
,但是定义了timerFunc
:
(OK Fine,让我们再把代码放过来重新看一遍)
const context = inBrowser
? window
: typeof global !== 'undefined' ? global : {}
timerFunc = context.setImmediate || setTimeout
和第一条线一样,只有调用 timerFunc(nextTickHandler, 0)
时,
对比一下【第一条线】和【第二条线】的调用方式,放一起对比一下:timerFunc()
timerFunc(nextTickHandler, 0)
二者是有差别的。
二者的“并集”为【第二条线】的写法,所以在最终调用的时候才会采用timerFunc(nextTickHandler, 0)
的调用方式。
当【第一条线】采用这种调用方式时,两个参数是不起作用的,虽然传了,但是没有用到。
而timerFunc(nextTickHandler, 0)
可以等同于setImmediate(nextTickHandler, 0)
或者setTimeout(nextTickHandler, 0)
来看待。
这样是不是就很眼熟了?
其中第一个参数是回调函数、第二个参数是时间。
3、补充说明
补充一下没提到的东西:
callbacks
表示异步回调队列
(姑且这么称呼它)
pending
是一个标志位,用于表示当前是否有待执行的回调函数。它的作用是控制异步回调队列的执行。
为了让我粗浅的语言能把内容讲的更清晰,我如上文分成两条线来说:
首先是 【第一条线】:
export const nextTick = (function () {
...
var pending = false
...
function nextTickHandler () {
pending = false
...
callbacks = []
// ...执行callbacks异步回调队列里的函数...
}
/* istanbul ignore if */
if (/* ...判断是否支持MutationObserver... */) {
// ...凭空创建一个节点(DOM),并监听文本变化...
// ...当变化时,触发 nextTickHandler 函数...
timerFunc = function () {
// ...更改目标节点textNode的文本内容,目的:触发nextTickHandler函数...
}
} else {
...
}
return function (cb, ctx) {
// ...将目标函数加入异步回调队列callbacks里面...
callbacks.push(func)
if (pending) return
pending = true
// 这里我用timerFunc()替代了timerFunc(nextTickHandler, 0)。毕竟里面的参数也没用
timerFunc()
}
})()
序号 | 操作 | pending | callbacks |
---|---|---|---|
1 | 初始化pending | false | 空数组 [] |
2 | 调用nextTick 向其中传入第一个目标函数cb-->func (后面直接用func 指代) |
false | 空数组 [] |
3 | callbacks.push(func) (不执行if(pending) return ) |
false | 数组中存放一个回调函数 [func] |
4 | 操作pending | true | 数组中存放一个回调函数 [func] |
— 分割线 — | — | — | |
5 | 执行timerFunc() ,触发文本变化,进而触发观察器回调函数nextTickHandler |
true | 数组中存放一个回调函数 [func] |
6 | 在nextTickHandler 中,操作pending |
false | 数组中存放一个回调函数 [func] |
7 | 清空callbacks ,将原本的内容备份到 cbs 放置。依次执行原异步执行队列cbs 中的回调函数 func |
false | 空数组 [] |
可以看到,按照一开始说的 【pending是一个标志位,用来表示当前是否有待执行的回调函数】这个说法,在代码中的体现本应该是清空callbacks
之后,将pending
置为false。
也就是说第6步和第7步应该反过来。
所以现在我们有了一个疑问:为什么要在执行回调函数 func
之前就将pending
置为 false
?
为什么要在执行回调函数 func
之前就将pending
置为 false
?
在探讨这个问题之前,我们明确几个概念:
callbacks
异步回调队列,存放的是期望被异步执行的回调函数 func()。是代码中的概念。
关于执行栈、执行队列、主线程,我按照个人理解总结一下是:执行队列
中存放异步任务
,执行栈
中存放同步任务
。
主线程从执行栈中取任务执行。
当执行栈为空,就会从执行队列中取出一个任务放到执行栈中,
之后主线程继续从执行栈中取任务执行。
想看专业一点的说法在这里(我反正不乐的看):
对这方面不太了解也可以看看我这篇文章: # 由浅入深彻底理解JS事件循环Event Loop
执行栈和主线程是 JavaScript 中两个关键概念,他们共同构成JavaScript的运行环境。
- 执行栈(Execution Stack):是JavaScript运行时管理函数调用的一种数据结构。遵循先进先出原则。当JavaScript引擎执行一个函数时,会创建一个对应的执行上下文,并推入执行栈中。当函数执行结束后,对应的执行上下文会从执行栈中弹出。执行栈用于追踪代码的执行顺序,保证代码的执行是有序的。
- 执行队列(Task Queue):是等待被 JavaScript引擎执行的任务列表。这些任务可以是异步操作产生的回调函数、定时器的回调、事件处理函数等。执行队列中的任务会按照先进先出的顺序等待执行,当主线程的执行栈为空时,JavaScript引擎会从执行队列中取出任务执行。
- 主线程(Main Thread):是JavaScript执行环境中的一个概念,负责执行JavaScript代码以及处理与浏览器交互的任务。在浏览器环境中,主线程负责执行JavaScript代码、处理用户交互事件、渲染页面等任务。JavaScript是单线程语言,同一时间只能执行一个任务。主线程负责管理执行栈中的任务,按照执行栈的顺序执行函数调用。如果执行栈为空,则会从执行队列中获取下一个任务执行。
另,为了防止混淆callbacks异步回调队列
和执行队列
这两个概念,后续用callbacks
指代异步回调队列
。
“
执行队列
“应该指的是浏览器
提供的用于管理异步任务的数据结构,
“callbacks 数组
“是Vue
中用于管理待执行回调函数的数据结构。
它们是两个不同的概念。
OK,现在回到代码:
callbacks 中的函数会依次进入执行队列中,在合适的时机依次进入执行栈、在主线程上执行。
回到问题【为什么要在执行回调函数 func 之前就将 pending 置为 false?】:
是因为在执行回调函数func
之前,要确保没有新的异步任务添加到执行队列
中。要保证在 func
执行期间 callbacks
的稳定,在回调函数执行期间,不会有新的函数进入。
我们首先假定一个场景。
我们通过nextTick
依次调用a、b、c
三个函数。此时a、b、c
被存放在 callbacks
中。在b
函数中,我们通过nextTick
调用一个新的函数b1
。此时 b1
会在什么时机执行呢?
其次我们依次假设pending = false
在for
循环之前和之后的情况。
- 假设一: 如果将
pending = false
放在for
循环之前(回调函数func
执行之前),如下代码。
那么在当通过copies[i]()
执行函数a、b、c
的时候,pending
为false
。
当b
函数中有一个新的nextTick(b1)
进来的时候,就会走callbacks.push(func); pending = true; timerFunc()
这段代码。
b1
被存放在callbacks
里面,同时也会立即执行timerFunc()
,即调用copies[i]()
、即调用b1
function nextTickHandler () { * pending = false var copies = callbacks.slice(0) callbacks = [] for (var i = 0; i < copies.length; i++) { copies[i]() } }
- 假设二: 如果将
pending = false
放在for
循环之后(回调函数func
执行之后),如下代码。
那么在当通过copies[i]()
执行函数a、b、c
的时候,pending
为true
。
当b
函数中有一个新的nextTick(b1)
进来的时候,就会走callbacks.push(func); if (pending) return
这段代码。
b1
会被存放在callbacks
里面,但是不会立即调用。什么时机会调用呢?在下一次调用nextTick
的时候。
那如果我后续再也不调用nextTick
了,是不是b1
再也不会执行了?function nextTickHandler () { var copies = callbacks.slice(0) callbacks = [] for (var i = 0; i < copies.length; i++) { copies[i]() } * pending = false }
新的问题:callbacks被重置为空数组[],那为什么copies[b1]没有顶替[a,b,c]
问题有点抽象,是我在想要搞清楚pending
用途的时候想不通的点。
场景还是复用刚才“a、b、c、b1”的场景,我也不过多赘述了。
详细描述一下我的问题:
在第一轮执行中,copies
中有a、b、c
三个回调,当执行到b
回调的时候,b
回调过程中又调用nextTick(b1)
加入一个b1
回调,
此时(即b
函数还未执行结束、c
函数还未执行之时,pending 为 false
,callbacks为空数组[]、 copies为数组[a,b,c] )会执行【 pending = true; timerFunc()】
这段代码。
这时候会触发 nextTickHandler 函数。
在 nextTickHandler 中,置pending 为 false
后,浅拷贝一组callbacks
为copies
,
这时这个浅拷贝操作不就相当于把 copies 从 [a,b,c] 变成 [b1] 了吗?c
还没有执行应该怎么办?
答案:
看我不糊涂了,nextTickHandler作为一个函数、copies作为其中的一个局部变量,每次进入函数都会有一个新的copies存放。互不关联。
既如此,我们趁机分析一下nextTick是按照什么样的顺序执行回调函数们的。
其实很简单,for
循环中的代码是同步代码,相当于 [a,b,c] 三个函数同时、依次被放入执行栈
中(假设当调用a、b、c回调的时候执行栈中为空)。按照事件循环的机制,她们三个一定是依次执行的。
我们前面也分析了,timerFunc
这个方法是通过某种手段让回调函数放到宏/微任务队列中执行(将目标函数变成异步任务)。
那么当 b 回调中发起一个nextTick(b1)
的时候,b1 会作为异步任务被放置到执行队列
中。当执行栈
为空的时候,即 [a,b,c] 三个函数都执行完毕之后,主线程会将执行队列中的[b1]拿到执行栈中执行。
所以 [a,b,c,b1] 四个函数的执行顺序就是 [a,b,c,b1]。
详细注释版代码
原文链接:https://juejin.cn/post/7328242736028893235 作者:ZhZhXuan