nextTick源码无偿带看&详解

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

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

纯享版代码

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

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)

如果pendingfalse,则将其置为 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以上的 WebViewMutationObserver 有 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的运行环境。

  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

    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再也不会执行了?

    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]。

详细注释版代码

原文链接:https://juejin.cn/post/7328242736028893235 作者:ZhZhXuan

(0)
上一篇 2024年1月26日 下午5:14
下一篇 2024年1月27日 上午10:05

相关推荐

发表回复

登录后才能评论