vue源码中的nextTick是怎样实现的

news2024/11/25 7:06:15

一、Vue.nextTick 内部逻辑

在执行 initGlobalAPI(Vue) 初始化 Vue 全局 API 中,这么定义 Vue.nextTick

function initGlobalAPI(Vue) {
    //...
    Vue.nextTick = nextTick;
}

可以看出是直接把 nextTick 函数赋值给 Vue.nextTick,就可以了,非常简单。

二、vm.$nextTick 内部逻辑

Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this)
};

可以看出是 vm.$nextTick 内部也是调用 nextTick 函数。

三、前置知识

nextTick 函数的作用可以理解为异步执行传入的函数,这里先介绍一下什么是异步执行,从 JS 运行机制说起。

1、JS 运行机制

JS 的执行是单线程的,所谓的单线程就是事件任务要排队执行,前一个任务结束,才会执行后一个任务,这就是同步任务,为了避免前一个任务执行了很长时间还没结束,那下一个任务就不能执行的情况,引入了异步任务的概念。JS 运行机制简单来说可以按以下几个步骤。

  • 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  • 主线程之外,还存在一个任务队列(task queue)。只要异步任务有了运行结果,会把其回调函数作为一个任务添加到任务队列中。
  • 一旦执行栈中的所有同步任务执行完毕,就会读取任务队列,看看里面有那些任务,将其添加到执行栈,开始执行。
  • 主线程不断重复上面的第三步。也就是常说的事件循环(Event Loop)。

2、异步任务的类型

nextTick 函数异步执行传入的函数,是一个异步任务。异步任务分为两种类型。

主线程的执行过程就是一个 tick,而所有的异步任务都是通过任务队列来一一执行。任务队列中存放的是一个个的任务(task)。规范中规定 task 分为两大类,分别是宏任务(macro task)和微任务 (micro task),并且每个 macro task 结束后,都要清空所有的 micro task。

用一段代码形象介绍 task的执行顺序。

for (macroTask of macroTaskQueue) {
    handleMacroTask();
    for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
    }
}

在浏览器环境中,
常见的创建 macro task 的方法有

  • setTimeout、setInterval、postMessage、MessageChannel(队列优先于setTimeiout执行)
  • 网络请求IO
  • 页面交互:DOM、鼠标、键盘、滚动事件
  • 页面渲染
    常见的创建 micro task 的方法
  • Promise.then
  • MutationObserve
  • process.nexttick

nextTick 函数要利用这些方法把通过参数 cb 传入的函数处理成异步任务。

三、 nextTick 函数

var callbacks = [];
var pending = false;
function nextTick(cb, ctx) {
    var _resolve;
    callbacks.push(function() {
        if (cb) {
            try {
                cb.call(ctx);
            } catch (e) {
                handleError(e, ctx, 'nextTick');
            }
        } else if (_resolve) {
            _resolve(ctx);
        }
    });
    if (!pending) {
        pending = true;
        timerFunc();
    }
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(function(resolve) {
            _resolve = resolve;
        })
    }
}

可以看到在 nextTick 函数中把通过参数 cb 传入的函数,做一下包装然后 push 到 callbacks 数组中。

然后用变量 pending 来保证执行一个事件循环中只执行一次 timerFunc()

最后执行 if (!cb && typeof Promise !== 'undefined'),判断参数 cb 不存在且浏览器支持 Promise,则返回一个 Promise 类实例化对象。例如 nextTick().then(() => {}),当 _resolve 函数执行,就会执行 then 的逻辑中。

来看一下 timerFunc 函数的定义,先只看用 Promise 创建一个异步执行的 ztimerFunc 函数 。参考 Vue面试题详细解答

var timerFunc;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function() {
        p.then(flushCallbacks);
        if (isIOS) {
            setTimeout(noop);
        }
    };
}

在其中发现 timerFunc 函数就是用各种异步执行的方法调用 flushCallbacks 函数。

来看一下flushCallbacks 函数

var callbacks = [];
var pending = false;
function flushCallbacks() {
    pending = false;
    var copies = callbacks.slice(0);
    callbacks.length = 0;
    for (var i = 0; i < copies.length; i++) {
        copies[i]();
    }
}

执行 pending = false 使下个事件循环中能nextTick 函数中调用 timerFunc 函数。

执行 var copies = callbacks.slice(0);callbacks.length = 0; 把要异步执行的函数集合 callbacks 克隆到常量 copies,然后把 callbacks 清空。

然后遍历 copies 执行每一项函数。回到 nextTick 中是把通过参数 cb 传入的函数包装后 push 到 callbacks 集合中。来看一下怎么包装的。

function() {
    if (cb) {
        try {
            cb.call(ctx);
        } catch (e) {
            handleError(e, ctx, 'nextTick');
        }
    } else if (_resolve) {
        _resolve(ctx);
    }
}

逻辑很简单。若参数 cb 有值。在 try 语句中执行 cb.call(ctx) ,参数 ctx 是传入函数的参数。
如果执行失败执行 handleError(e, ctx, 'nextTick')

若参数 cb 没有值。执行 _resolve(ctx),因为在nextTick 函数中如何参数 cb 没有值,会返回一个 Promise 类实例化对象,那么执行 _resolve(ctx),就会执行 then 的逻辑中。

到这里 nextTice 函数的主线逻辑就很清楚了。定义一个变量 callbacks,把通过参数 cb 传入的函数用一个函数包装一下,在这个中会执行传入的函数,及处理执行失败和参数 cb 不存在的场景,然后 添加到 callbacks。调用 timerFunc 函数,在其中遍历 callbacks 执行每个函数,因为 timerFunc 是一个异步执行的函数,且定义一个变量 pending来保证一个事件循环中只调用一次 timerFunc 函数。这样就实现了 nextTice 函数异步执行传入的函数的作用了。

那么其中的关键还是怎么定义 timerFunc 函数。因为在各浏览器下对创建异步执行函数的方法各不相同,要做兼容处理,下面来介绍一下各种方法。

1、Promise 创建异步执行函数

if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function() {
        p.then(flushCallbacks);
        if (isIOS) {
            setTimeout(noop);
        }
    };
    isUsingMicroTask = true;
}

执行 if (typeof Promise !== 'undefined' && isNative(Promise)) 判断浏览器是否支持 Promise,

其中 typeof Promise 支持的话为 function ,不是 undefined,故该条件满足,这个条件好理解。

来看另一个条件,其中 isNative 方法是如何定义,代码如下。

function isNative(Ctor) {
    return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

Ctor 是函数类型时,执行 /native code/.test(Ctor.toString()),检测函数 toString 之后的字符串中是否带有 native code 片段,那为什么要这么监测。这是因为这里的 toString 是 Function 的一个实例方法,如果是浏览器内置函数调用实例方法 toString 返回的结果是function Promise() { [native code] }

若浏览器支持,执行 var p = Promise.resolve()Promise.resolve() 方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。

那么在 timerFunc 函数中执行 p.then(flushCallbacks) 会直接执行 flushCallbacks 函数,在其中会遍历去执行每个 nextTick 传入的函数,因 Promise 是个微任务 (micro task)类型,故这些函数就变成异步执行了。

执行 if (isIOS) { setTimeout(noop)} 来在 IOS 浏览器下添加空的计时器强制刷新微任务队列。

2、MutationObserver 创建异步执行函数

if (!isIE && typeof MutationObserver !== 'undefined' &&
    (isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
    var counter = 1;
    var observer = new MutationObserver(flushCallbacks);
    var textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
        characterData: true
    });
    timerFunc = function() {
        counter = (counter + 1) % 2;
        textNode.data = String(counter);
    };
    isUsingMicroTask = true;
}

MutationObserver() 创建并返回一个新的 MutationObserver 它会在指定的 DOM 发生变化时被调用,IE11浏览器才兼容,故干脆执行 !isIE 排除 IE浏览器。执行 typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) 判断,其原理在上面已介绍过了。执行 MutationObserver.toString() === '[object MutationObserverConstructor]') 这是对 PhantomJS 浏览器 和 iOS 7.x版本浏览器的支持情况进行判断。

执行 var observer = new MutationObserver(flushCallbacks),创建一个新的 MutationObserver 赋值给常量 observer, 并且把 flushCallbacks 作为回到函数传入,当 observer 指定的 DOM 要监听的属性发生变化时会调用 flushCallbacks 函数。

执行 var textNode = document.createTextNode(String(counter)) 创建一个文本节点。

执行 var counter = 1counter 做文本节点的内容。

执行 observer.observe(textNode, { characterData: true }),调用 MutationObserver 的实例方法 observe 去监听 textNode 文本节点的内容。

这里很巧妙利用 counter = (counter + 1) % 2 ,让 counter 在 1 和 0 之间变化。再执行 textNode.data = String(counter) 把变化的 counter 设置为文本节点的内容。这样 observer 会监测到它所观察的文本节点的内容发生变化,就会调用 flushCallbacks 函数,在其中会遍历去执行每个 nextTick 传入的函数,因 MutationObserver 是个微任务 (micro task)类型,故这些函数就变成异步执行了。

3、setImmediate 创建异步执行函数

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    timerFunc = function() {
        setImmediate(flushCallbacks);
    };
} 

setImmediate 只兼容 IE10 以上浏览器,其他浏览器均不兼容。其是个宏任务 (macro task),消耗的资源比较小

4、setTimeout 创建异步执行函数

timerFunc = function() {
    setTimeout(flushCallbacks, 0);
}

兼容 IE10 以下的浏览器,创建异步任务,其是个宏任务 (macro task),消耗资源较大。

5、创建异步执行函数的顺序

Vue 历来版本中在 nextTick 函数中实现 timerFunc 的顺序时做了几次调整,直到 2.6+ 版本才稳定下来

第一版的 nextTick 函数中实现 timerFunc 的顺序为 PromiseMutationObserversetTimeout

在2.5.0版本中实现 timerFunc 的顺序改为 setImmediateMessageChannelsetTimeout
在这个版本把创建微任务的方法都移除,原因是微任务优先级太高了,其中一个 issues 编号为 #6566, 情况如下:

<div class="header" v-if="expand"> // block 1
    <i @click="expand = false;">Expand is True</i> // element 1
</div>
<div class="expand" v-if="!expand" @click="expand = true;"> // block 2
    <i>Expand is False</i> // element 2
</div>

按正常逻辑 点击 element 1 时,会把 expand 置为 false,block 1 不会显示,而 block 2 会显示,在点击 block 2 ,会把 expand 置为 false,那么 block 1 会显示。

当时实际情况是 点击 element 1 ,只会显示 block 1。这是为什么,什么原因引起这个BUG。Vue 官方是这么解释的

点击事件是宏任务,<i>上的点击事件触发 nextTick(微任务)上的第一次更新。在事件冒泡到外部div之前处理微任务。在更新过程中,将向外部div添加一个click侦听器。因为DOM结构相同,所以外部div和内部元素都被重用。事件最终到达外部div,触发由第一次更新添加的侦听器,进而触发第二次更新。为了解决这个问题,您可以简单地给两个外部div不同的键,以强制在更新期间替换它们。这将阻止接收冒泡事件。

当然当时官方还是给出了解决方案,把 timerFunc 都改为用创建宏任务的方法实现,其顺序是 setImmediateMessageChannelsetTimeout,这样 nextTick 是个宏任务。

点击事件是个宏任务,当点击事件执行完后触发的 nextTick(宏任务)上的更新,只会在下一个事件循环中进行,这样其事件冒泡早已执行完毕。就不会出现 BUG 中的情况。

但是过不久,实现 timerFunc 的顺序又改为 PromiseMutationObserversetImmediatesetTimeout,在任何地方都使用宏任务会产生一些很奇妙的问题,其中代表 issue 编号为 #6813,代码就打出来,可以看这里。
这里有两个关键的控制

  • 媒体查询,当页面宽度大于 1000px 时,li 显示类型为行内框,小于1000px时,显示类型为块级元素。
  • 监听页面缩放,当页面宽度小于 1000px 时,ul 用 v-show="showList" 控制隐藏。

初始状态:

当快速拖动网页边框缩小页面宽度时,会先显示下面第一张图,然后快速的隐藏,而不是直接隐藏。

那为出现这种BUG,首先要了解一个概念,UI Render (UI渲染)的执行时机,如下所示:

    1. macro 取一个宏任务。
    1. micro 清空微任务队列。
    1. 判断当前帧是否值得更新,否则重新进入1步骤
    1. 一帧欲绘制前,执行requestAnimationFrame队列任务。
    1. UI更新,执行 UI Render。
    1. 如果宏任务队列不为空,重新进入步骤

这个过程也比较好理解,之前执行监听窗口缩放是个宏任务,当窗口大小小于 1000px 时,showList 会变为 flase ,会触发一个 nextTick 执行,而其是个宏任务。在两个宏任务之间,会进行 UI Render ,这时,li 的行内框设置失效,展示为块级框,在之后的 nextTick 这个宏任务执行了,再一次 UI Render 时,ul 的 display 的值切换为 none,列表隐藏。

所以 Vue 觉得用微任务创建的 nextTick 可控性还可以,不像用宏任务创建的 nextTick 会出现不可控场景。

在 2.6 + 版本中采用一个时间戳来解决 #6566 这个BUG,设置一个变量 attachedTimestamp,在执行传入 nextTick 函数中的 flushSchedulerQueue 函数时,执行 currentFlushTimestamp = getNow() 获取一个时间戳赋值给变量 currentFlushTimestamp,然后再监听 DOM 上事件前做个劫持。其在 add 函数中实现。

function add(name, handler, capture, passive) {
    if (useMicrotaskFix) {
        var attachedTimestamp = currentFlushTimestamp;
        var original = handler;
        handler = original._wrapper = function(e) {
            if (
                e.target === e.currentTarget ||
                e.timeStamp >= attachedTimestamp ||
                e.timeStamp <= 0 ||
                e.target.ownerDocument !== document
            ) {
                return original.apply(this, arguments)
            }
        };
    }
    target.addEventListener(
        name,
        handler,
        supportsPassive ? {
            capture: capture,
            passive: passive
        } : capture
    );
}

执行 if (useMicrotaskFix)useMicrotaskFix 在用微任务创建异步执行函数时置为 true

执行 var attachedTimestamp = currentFlushTimestamp 把 nextTick 回调函数执行时的时间戳赋值给变量 attachedTimestamp,然后执行 if(e.timeStamp >= attachedTimestamp),其中 e.timeStamp DOM 上的事件被触发时的时间戳大于 attachedTimestamp,这个事件才会被执行。

为什么呢,回到 #6566 BUG 中。由于micro task的执行优先级非常高,在 #6566 BUG 中比事件冒泡还要快,就会导致此 BUG 出现。当点击 i标签时触发冒泡事件比 nextTick 的执行还早,那么 e.timeStampattachedTimestamp 小,如果让冒泡事件执行,就会导致 #6566 BUG,所以只有冒泡事件的触发比 nextTick 的执行晚才会避免此 BUG,故 e.timeStampattachedTimestamp 大才能执行冒泡事件。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/68738.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

leetcode-每日一题-二进制表示中质数个计算置位(简单,popcount算法)

从这道题了解到了一个时间复杂度为o(1)的一个计算一个数转换为二进制时1存在的个数问题&#xff0c;很巧妙运用了二分来求解&#xff0c;代码如下 unsigned popcount (unsigned u) {u (u & 0x55555555) ((u >> 1) & 0x55555555);u (u & 0x33333333) ((u…

API接口使用方法(封装好的电商平台)

为了进行此平台API的调用&#xff0c;首先我们需要做下面几件事情。 1、 获取一个KEY。 点击获取 2、 参考API文档里的接入方式和示例。 3、查看测试工具是否有需要的接口&#xff0c;响应实例的返回字段是否符合参数要求。 4、利用平台的文档中心和API测试工具&#xff0c…

自定义网页中被选中文本的样式 CSS selection

文章目录两张图自定义被选中文本的样式::selection 选择器的可填属性::selection 选择器支持的CSS属性 完整版 demo浏览器兼容官网文档两张图 对于选中的文本(准确地说应该是被选中的DOM元素)&#xff0c;浏览器的默认样式&#xff1a;淡青色的背景色。 2. 自定义样式&#xf…

[说明] Doris使用培训

参考文献 apache doris在蜀海供应链数仓建设中的实践 应用实践 | 数仓体系效率全面提升&#xff01;同程数科基于 Apache Doris 的数据仓库建设 一、doris的背景介绍 doris的使用场景 实时/离线一体的数仓 借用一句话&#xff1a; Uniq 模型拳打KUDU、HUDI&#xff0c;Agg…

[LeetCode 1775]通过最少操作数使数组的和相等

题目描述 题目链接&#xff1a;[LeetCode 1775]通过最少操作数使数组的和相等 给你两个长度可能不等的整数数组 nums1 和 nums2 。两个数组中的所有值都在 1 到 6 之间&#xff08;包含 1 和 6&#xff09;。 每次操作中&#xff0c;你可以选择 任意 数组中的任意一个整数&a…

openEuler kubesphere kubekey 安装 ceph csi 及使用实例

按照官方文档,很轻松就可以进行安装, 安装过程中遇到一些疑问, 在安装后也得到清晰的理解 1. /root/ceph-csi-rbd.yaml 这里面的clusterId 按理说应该是ceph的clusterId, 不过官方文档并没有首重强调修改,暂时保持默认 下面6789端口就是我们在物理机上安装的ceph mon的主机i…

PCB设计指南:安规、布局布线、EMC、热设计、工艺

文章目录Part 1 安规距离要求部分一、爬电距离和电气间隙距离要求&#xff1a;Part 2 抗干扰、EMC部分一、长线路抗干扰二、小信号走线尽量远离大电流走线&#xff0c;忌平行&#xff0c;D>2.0mm。三、小信号线处理&#xff1a;电路板布线尽量集中&#xff0c;减少布板面积提…

广州蓝景分享—实用的CSS技巧,助你成为更好的开发者

Hello~~各位小伙伴&#xff0c;相信在前端开发项目中&#xff0c;CSS实现如修改输入占位符样式&#xff0c;多行文本溢出&#xff0c;隐藏滚动条&#xff0c;修改光标颜色&#xff0c;水平和垂直居中等等&#xff0c;这些都是我们非常熟悉的开发场景&#xff01;前端开发者几乎…

HashMap部分源码解析

作者&#xff1a;~小明学编程 文章专栏&#xff1a;Java数据结构 格言&#xff1a;目之所及皆为回忆&#xff0c;心之所想皆为过往 目录 前言 常量字段 构造方法 put方法 确定初始容量 为何我们的数组的大小要是2的n次幂 hash为何要异或其高位 扩容机制 前言 我们在前…

极客时间Kafka - 04 Kafka生产者和消费者拦截器

文章目录1. 什么是拦截器&#xff1f;2. Kafka 拦截器3. 典型使用场景4. 案例分享1. 什么是拦截器&#xff1f; 如果你用过 Spring Interceptor 或是 Apache Flume&#xff0c;那么应该不会对拦截器这个概念感到陌生&#xff0c;其基本思想就是允许应用程序在不修改逻辑的情况…

无人机边缘计算中的计算卸载——Stackelberg博弈方法论文复现附matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

不会还有人不会热修复吧?

Class流派原理 基本原理:加载类的时候是找element&#xff0c;每个element对于一个dex。我要把我修复的那个类单独放到dex插入dexlist前面&#xff0c;在你做类加载从前往后找优先从你的dex加载加载的就是你修复后的class.这就是 实现代码 通过context拿到pathClassLoader&am…

Qt跨平台截图工具

Qt跨平台截图工具 文章目录Qt跨平台截图工具1、概述2、实现效果3、软件构成4、关键代码5、源代码更多精彩内容&#x1f449;个人内容分类汇总 &#x1f448;&#x1f449;Qt自定义模块、工具&#x1f448; 1、概述 Qt版本&#xff1a;V5.12.5兼容系统&#xff1a; Windows&…

2022,记录与华为的这场会议

一、数据治理团体标准发布会 11月26日&#xff0c;中国计算机用户协会信息科技审计分会联合华为与擎创科技共同举办了“金融行业运维数据治理团体标准应用研讨暨2022年度调研报告线上发布会”。来自国家开发银行、中国建设银行、中国邮政储蓄银行、招商银行、兴业银行、中信银行…

【LeetCode_字符串_逻辑分析】9. 回文数

目录考察点第一次&#xff1a;2022年12月7日10:16:33解题思路代码展示题目描述给你一个整数 x &#xff0c;如果 x 是一个回文整数&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。回文数是指正序&#xff08;从左向右&#xff09;和倒序&#xff08;从右向左…

340页11万字智慧政务大数据资源平台大数据底座数据治理建设方案

目 录 第一章 项目概况 1.1 项目名称 1.2 项目单位 1.3 项目建设依据 1.4 项目建设内容和目标 1.4.1 建设内容 1.4.2 建设目标 1.5 项目投资估算及建设周期 1.5.1 项目投资估算 1.5.2 服务周期 第二章 现状 2.1 项目单位概况 2.1.1 单位职责、内设及下属机构、人员…

【配准图像】

MU-Net: A MULTISCALE UNSUPERVISED NETWORK FOR REMOTE SENSING IMAGE REGISTRATION &#xff08;MU-Net&#xff1a;一种多尺度无监督遥感图像配准网络&#xff09; 多传感器或多模态图像对的配准是许多遥感应用的基础性任务。为了实现高精度、低成本的遥感图像配准&#x…

彻底搞懂JS原型与原型链

说到JavaScript的原型和原型链&#xff0c;相关文章已有不少&#xff0c;但是大都晦涩难懂。本文将换一个角度出发&#xff0c;先理解原型和原型链是什么&#xff0c;有什么作用&#xff0c;再去分析那些令人头疼的关系。 一、引用类型皆为对象 原型和原型链都是来源于对象而…

浅谈Linux内核编程规范与代码风格

1 缩进 Tab的宽度是八个字符&#xff0c;因此缩进的宽度也是八个字符。有些异教徒想让缩进变成四个字符&#xff0c;甚至是两个字符的宽度&#xff0c;这些人和那些把 PI 定义为 3 的人是一个路子的。 注意&#xff1a;缩进的全部意义在于清晰地定义语句块的开始与结束&#…

《MongoDB》Mongo Shell中的基本操作-删除操作一览

前端博主&#xff0c;热衷各种前端向的骚操作&#xff0c;经常想到哪就写到哪&#xff0c;如果有感兴趣的技术和前端效果可以留言&#xff5e;博主看到后会去代替大家踩坑的&#xff5e; 主页: oliver尹的主页 格言: 跌倒了爬起来就好&#xff5e; 来个关注吧&#xff0c;点个赞…