nextTick源码无偿带看&详解

参考文章:Vue源码详解之 nextTick:MutationObserver只是浮云,microtask才是核心!

参考文章:Vue源码详解之 nextTick:MutationObserver只是浮云,microtask才是核心!

纯享版代码

先看一下纯享版代码,看不懂没关系,下面无偿带看&详解

js 复制代码
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的返回值如下:

javascript 复制代码
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

javascript 复制代码
var func = ctx 
    ? function () { cb.call(ctx) } 
    : cb

如果传入了参数 ctx(执行上下文),则创建一个新函数 func ,该函数将传入的cb回调函数放在ctx执行上下文上执行。

如果没有传入ctx,则直接将 cb 函数赋值给 func
(此时cb并未执行,此步骤仅规范cb即将执行的时候的执行上下文

1.2、

go 复制代码
callbacks.push(func)

将步骤1中定义好的func函数加入到callbacks里面(看起来像是一个回调函数数组)

1.3、

kotlin 复制代码
if (pending) return

pending(汉译:"待处理的")。

如果待处理则直接返回???不确定,再看看。

1.4、

ini 复制代码
pending = true
timerFunc(nextTickHandler, 0)

如果pendingfalse,则将其置为 true,并调用方法:timerFunc(nextTickHandler, 0).

现在的疑问变为:
timerFunc、nextTickHandler 是什么?

2、timerFunc、nextTickHandler 是什么?

2.1、定义变量

回看函数开头,可以溯一下源:

csharp 复制代码
var callbacks = []
var pending = false 
var timerFunc
function nextTickHandler () {
    ...
}

可以看到三个var定义了三个变量,我们要找的timerFunc也在其中,并且timerFunc声明,但未定义,更没有执行

后面定义了一个函数,就是我们要找的nextTickHandler声明,定义,但未执行。所以我们也不细看,等用到再说。接着向下看。

2.2、if...else代码块

csharp 复制代码
if (typeof MutationObserver !== 'undefined' && !hasMutationObserverBug) {
    ...
} else {
    ...
}

是一个if...else句式,先看下里面的判断条件

首先是MutationObserver,想要详细了解可以点进这个链接MDN # MutationObserver

简略总结其作用就是:监视DOM在某方面的更改在目标DOM发生属性、新增、删除、文本修改等变化的时候,可以做出相应的响应

后面的!hasMutationObserverBug也不作详解,从参考文章中引入一下对该变量的解释:

ios9.3以上的 WebViewMutationObserver 有 bug ,

所以在 hasMutationObserverBug 中存放了是否是这种情况

总结一下,该判断的目的:

保证MutationObserver存在可用(没有bug)。

2.3、解析if...else代码块

我们可以大致看到,if...else代码块里面的代码,是在不同环境(支持 MutationObserver 的环境、不支持的环境)下的操作处理。

既如此,两个代码块里面实现的功能大同小异,只是具体实现细节有差别。

我们分开来看:

2.3.1、解析 if

先看 if代码块里面的代码

ini 复制代码
var counter = 1

首先定义了一个计数器 counter

javascript 复制代码
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 节点上所有字符的变化。

ini 复制代码
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

javascript 复制代码
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 :

ini 复制代码
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,让我们把代码放过来重新看一遍)

javascript 复制代码
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(counter)
observer.observe(textNode, {
    characterData: true
})

她会在观察器目标对象发生变化时的作为观察器的回调函数被调用,

而仅当timerFunc被调用时,目标对象才会发生变化。

即在此处调用时,目标对象才会发生变化:

ini 复制代码
if (pending) return
pending = true 
timerFunc(nextTickHandler, 0)

形如timerFunc()的调用即可。

(后面解释为什么代码里会给timerFunc加上参数、写成"timerFunc(nextTickHandler, 0)"的"冗余"样子)

【第二条线】 ,在else代码块中。虽然没有直接调用nextTickHandler,但是定义了timerFunc

(OK Fine,让我们再把代码放过来重新看一遍)

javascript 复制代码
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

是一个标志位,用于表示当前是否有待执行的回调函数。它的作用是控制异步回调队列的执行。

为了让我粗浅的语言能把内容讲的更清晰,我如上文分成两条线来说:

首先是 【第一条线】

js 复制代码
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的运行环境。

  1. 执行栈(Execution Stack) :是JavaScript运行时管理函数调用的一种数据结构。遵循先进先出原则。当JavaScript引擎执行一个函数时,会创建一个对应的执行上下文,并推入执行栈中。当函数执行结束后,对应的执行上下文会从执行栈中弹出。执行栈用于追踪代码的执行顺序,保证代码的执行是有序的。
  2. 执行队列(Task Queue):是等待被 JavaScript引擎执行的任务列表。这些任务可以是异步操作产生的回调函数、定时器的回调、事件处理函数等。执行队列中的任务会按照先进先出的顺序等待执行,当主线程的执行栈为空时,JavaScript引擎会从执行队列中取出任务执行。
  3. 主线程(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 = falsefor循环之前之后的情况。

  • 假设一: 如果将pending = false 放在 for循环之前(回调函数func执行之前),如下代码。
    那么在当通过copies[i]()执行函数a、b、c的时候,pendingfalse
    b函数中有一个新的nextTick(b1)进来的时候,就会走callbacks.push(func); pending = true; timerFunc()这段代码。
    b1被存放在callbacks里面,同时也会立即执行 timerFunc(),即调用copies[i]() 、即调用b1

    ini 复制代码
    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的时候,pendingtrue
    b函数中有一个新的nextTick(b1)进来的时候,就会走callbacks.push(func); if (pending) return这段代码。
    b1会被存放在callbacks里面,但是不会立即调用 。什么时机会调用呢?在下一次调用nextTick的时候。
    那如果我后续再也不调用 nextTick了,是不是b1再也不会执行了?

    ini 复制代码
    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 为 falsecallbacks为空数组[]、 copies为数组[a,b,c] )会执行【 pending = true; timerFunc()】这段代码。

这时候会触发 nextTickHandler 函数。

在 nextTickHandler 中,置pending 为 false后,浅拷贝一组callbackscopies
这时这个浅拷贝操作不就相当于把 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]。

详细注释版代码

js 复制代码
export const nextTick = (function () {
    var callbacks = []
    var pending = false // pending 是一个标志位,用于表示当前是否有待执行的回调函数。它的作用是控制异步回调队列的执行。
    var timerFunc
    function nextTickHandler () {
        pending = false
        // 之所以要slice复制一份出来是因为有的cb执行过程中又会往callbacks中加入内容
        // 比如$nextTick的回调函数里又有$nextTick
        // 这些是应该放入到下一个轮次的nextTick去执行的,
        // 所以拷贝一份当前的,遍历执行完当前的即可,避免无休止的执行下去
        var copies = callbacks.slice(0)
        callbacks = []
        for (var i = 0; i < copies.length; i++) {
            copies[i]()
        }
    }

    /* istanbul ignore if */
    // ios9.3以上的 WebView 的 MutationObserver 有 bug,所以在 hasMutationObserverBug 中存放了是否是这种情况
    /* 如果浏览器支持 MutationObserver 而且没有已知的 bug,它会使用 MutationObserver 在 DOM 更新后触发 nextTickHandler。 */
    if (typeof MutationObserver !== 'undefined' && !hasMutationObserverBug) {
        // 这if代码块部分最主要的就是 在执行 timerFunc 的时候把 nextTickHandler 放到微任务队列里面
        var counter = 1
        // 创建了一个MO,这个MO监听了一个新创建的文本节点的文本内容变化,同时监听到变化时的回调就是nextTickHandler
        // 创建一个MutationObserver,observer监听到dom改动之后后执行回调nextTickHandler
        var observer = new MutationObserver(nextTickHandler)
        var textNode = document.createTextNode(counter)
        // 调用MutationObserver的接口,观测文本节点的字符内容
        observer.observe(textNode, {
            characterData: true
        })
        // 每次执行timerFunc都会让文本节点的内容在0/1之间切换,
        // 不用true/false可能是有的浏览器对于文本节点设置内容为true/false有bug?
        // 切换之后将新值赋值到那个我们MutationObserver观测的文本节点上去
        /* 值的切换会触发MutationObserver的监听函数 */
        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.
        // webpack默认会在代码中插入setImmediate的垫片
        // 没有MutationObserver就优先用setImmediate,不行再用setTimeout
        /*
        * 上下文环境,要么指代浏览器-window、要么指代node运行-global
        * 没有满足第一个 if 的条件,则退而求其次,用 setImmediate/setTimeout 实现功能。
        * setImmediate方法是Node.js环境特有的。setTimeout在几乎所有JavaScript环境中都被支持。
        * timerFunc 其实就是 setImmediate/setTimeout
        * timerFunc 接收的参数其实就是 setImmediate/setTimeout 接收的参数
        * */
        const context = inBrowser
            ? window
            : typeof global !== 'undefined' ? global : {}
        timerFunc = context.setImmediate || setTimeout
    }
    /*
    * nextTickHandler 遍历cb数组,把需要执行的cb给拿出来一个个执行了
    * 也就是把传入的回调放入 cb 数组当中,然后执行 timerFunc(nextTickHandler, 0) ,其实是执行 timerFunc() ,
    * 后面传入的两参数没用,在浏览器不支持MO的情况 timerFunc 才回退到 setTimeout ,那俩参数才有效果。
    * timerFunc 就是把那个被 MO 监听的文本节点改一下它的内容,这样我改了文本内容,
    * MO 就会在当前的所有同步代码完成之后执行回调,从而执行数据更新到 DOM 上之后的任务。
    * */
    // 返回值:一个函数
    // 接受一个回调函数和一个上下文(可选)
    return function (cb, ctx) {
        // func:nickTick之后想要执行的动作函数(cb函数)
        var func = ctx
            ? function () { cb.call(ctx) }
            : cb
        // 把这个cb加入回调函数数组中
        callbacks.push(func)
        // 如果回调函数数组(异步回调队列)里面有需要执行的函数,则直接返回,不做进一步操作。
        // 这是为了避免重复执行异步回调队列中的函数。会等待下一次微任务队列中执行。
        if (pending) return
        // 如果回调函数数组(异步回调队列)里面没有有需要执行的函数,但是新加入了一个cb函数,那么现在有了要执行的函数,则将pending置为true。
        pending = true
        // 将cb函数加入真正的异步回调队列中。
        timerFunc(nextTickHandler, 0)
    }
})()

// this.$nextTick( callBack )
相关推荐
鑫~阳7 分钟前
html + css 淘宝网实战
前端·css·html
Catherinemin11 分钟前
CSS|14 z-index
前端·css
2401_882727572 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
NoneCoder2 小时前
CSS系列(36)-- Containment详解
前端·css
anyup_前端梦工厂2 小时前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand2 小时前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL2 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
六卿2 小时前
react防止页面崩溃
前端·react.js·前端框架
z千鑫3 小时前
【前端】详解前端三大主流框架:React、Vue与Angular的比较与选择
前端·vue.js·react.js
m0_748256143 小时前
前端 MYTED单篇TED词汇学习功能优化
前端·学习