vue中的任务队列和异步更新策略(任务队列,微任务,宏任务)

Rhoda ·
更新时间:2024-11-10
· 1944 次阅读

目录

事件循环

任务队列

如何理解微任务和宏任务?

深究Vue异步更新策略原理

事件循环

JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。

为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,Event Loop 的方案应用而生。

Event Loop 包含两类:

一类是基于 Browsing Context

一种是基于 Worker

二者的运行是独立的,也就是说,每一个 JavaScript 运行的"线程环境"都有一个独立的 Event Loop,每一个 Web Worker 也有一个独立的 Event Loop。

任务队列

vue 数据驱动视图是数据改变,视图异步等待所有数据变化完成,统一进行视图更新。

既然是异步就有顺序和优先级, 异步任务队列是那种顺序执行 ?

tip: 微任务优先级高于宏任务 MDN 介绍

任务队列主要分为两种:

1、microtasks(微任务):

Promise :ES6的异步处理方案

process.nextTick(vue.nextTick) :下轮tick更新机制,

Mutation Observer API: DOM改变 监听 API

2、macrotasks(宏任务也称任务):

setTimeout() : 延时器

setInterval(): 计时器

setImmediate:node.js 回调函数延迟执行,process.nextTicl() 方法十分类似

process.nextTick()中的回调函数执行的优先级要高于setImmediate().这里的原因在于事件循环对观察者的检查是有先后顺序的,process.nextTick()属于idle观察者,setImmediate()属于check观察者。在每一个轮循环检查中,idle观察者先于I/O观察者,I/O观察者先于check观察者。

在具体实现上,process.nextTick()的回调函数保存在一个数组中,setImmediate()的结果则是保存在链表中。在行为上,process.nextTick()在每轮循环中会将该数组中的回调函数全部执行完,而setImmediate()在每轮循环中执行链表中的一个回调函数

I/O :系统IO(input/output)

UI render :页面渲染

requestAnimationFrame():异步动画渲染 ,浏览器调用指定的函数以在下次重绘之前更新动画

如何理解微任务和宏任务?

创造代码的人也是人,他们的灵感多数来自于生活。我们这里打个比方(朴灵老师也这样比喻),javascript处理异步就像去餐馆吃饭,服务员会先为顾客下单,下完单把顾客点的单子给后厨让其准备,然后就去服务下一位顾客,,而不是一直等待在出餐口。

javascript将顾客下单的行为进行了细分。无外乎两种酒水类和非酒水类。对应着我们javascript中的macroTask和microTask。

但是在不同场景下的步骤是不一样的,就像西餐和中餐。西餐划分的非常详细:头盘->汤->副菜->主菜->蔬菜类菜肴->甜品->咖啡,中餐就相对简单许多:凉菜->热菜->汤。

任务队列,下面看几个示例, 输出内容便是执行顺序:

任务队列,下面看几个示例, 输出内容便是执行顺序:

setTimeout(()=>{ console.log('1') Promise.resolve().then(function() { console.log('2') }) }, 0) setTimeout(()=>{ console.log('3') Promise.resolve().then(function() { console.log('4') }) }, 0) setTimeout(function() {console.log('6')}, 0) requestAnimationFrame(function(){ console.log('5') }) setTimeout(function() {console.log('7')}, 0) new Promise(function executor(resolve) { console.log('1') resolve() console.log('2') }).then(function() { console.log('4') }) console.log('3') console.log('1'); setTimeout(() => { console.log('5'); process.nextTick(() => console.log('6')); }, 0); process.nextTick(() => { console.log('3'); process.nextTick(() => console.log('4')); }); console.log('2');

这样就可以理解Vue的异步更新策略运行机制

created() { this.textDemo() }, methods: { textDemo() { console.log(1) setTimeout(() => { // macroTask console.log(4) setTimeout(() => { // macroTask console.log(8) }, 0) this.$nextTick(() => { // microTask console.log(5) }) Promise.resolve().then(function() { // microTask console.log(7) }) this.$nextTick(() => { // microTask console.log(6) }) }, 0) this.$nextTick(() => { // microTask console.log(3) }) console.log(2) }, }

到此我们已经知道代码是如何运行的了, 如果你想要更深入理解, 请继续向下阅读。

深究Vue异步更新策略原理

我们先看一个示例:

<template>   <div>     <div ref="test">{{test}}</div>     <button @click="handleClick">tet</button>   </div> </template> <script> export default {     data () {         return {             test: 'begin'         };     },     methods () {         handleClick () {             this.test = 'end';             console.log(this.$refs.test.innerText);// 结果输出 begin         }     } } </script>

通过上面示例可以看出 Vue是异步执行DOM更新, 更新会缓冲到队列中, 在nextTick 集中刷新队列并执行实际 (已去重的) 工作

当然新版 this.$nextTick 有一些变化 。从Vue 2.5+开始,抽出来了一个单独的文件next-tick.js来执行它。

其大概的意思就是:在Vue 2.4之前的版本中,nextTick 几乎都是基于 microTask 实现的(具体可以看文章nextTick一节),但是由于 microTask 的执行优先级非常高,在某些场景之下它甚至要比事件冒泡还要快,就会导致一些诡异的问题;但是如果全部都改成 macroTask,对一些有重绘和动画的场景也会有性能的影响。所以最终 nextTick 采取的策略是默认走 microTask,对于一些DOM的交互事件,如 v-on绑定的事件回调处理函数的处理,会强制走 macroTask。

具体做法就是,在Vue执行绑定的DOM事件时,默认会给回调的handler函数调用withMacroTask方法做一层包装,它保证整个回调函数的执行过程中,遇到数据状态的改变,这些改变而导致的视图更新(DOM更新)的任务都会被推到macroTask。

源码:

function add$1 (event, handler, once$$1, capture, passive) { handler = withMacroTask(handler); if (once$$1) { handler = createOnceHandler(handler, event, capture); } target$1.addEventListener( event, handler, supportsPassive ? { capture: capture, passive: passive } : capture ); } /** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a Task instead of a MicroTask. */ function withMacroTask (fn) { return fn._withTask || (fn._withTask = function () { useMacroTask = true; var res = fn.apply(null, arguments); useMacroTask = false; return res }) }

最后,写一段DEMO验证一下 :

<template>     <div>         <button @click="handleClick">change</button>     </div> </template> <script> export default {    created() {       this.handleClick() //  得出结果 : 2 3 1 4   },    methods: {        handleClick() {          setTimeout(() => { // macroTask            console.log(4)          }, 0)          this.$nextTick(() => { // microTask            console.log(2)          })          Promise.resolve().then(function() { // microTask            console.log(1)          })          this.$nextTick(() => { // microTask            console.log(3)          })        }    } } </script>

在Vue 2.5+中,这段代码的输出顺序是1 - 2 - 3 - 4, 而 Vue 2.4 和 不通过DOM 输出 2 - 3 - 1 - 4。nextTick执行顺序的差异正好说明了上面的改变。

tips: 所以这里需要留意遇到DOM操作, 同步执行受阻或者节点内容未及时更新可以使用 this.$nextTick 等待一下在执行下面的操作。

<div id="example"> <audio ref="audio" :src="url"></audio> <span ref="url"></span> <button @click="changeUrl">click me</button> </div> <script> const musicList = [ 'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112003137.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3', 'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112002493.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3', 'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112004168.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3' ]; var vm = new Vue({ el: '#example', data: { index: 0, url: '' }, methods: { changeUrl() { this.index = (this.index + 1) % musicList.length this.url = musicList[this.index]; this.$refs.audio.play(); } } }); </script>

毫无悬念,这样肯定是会报错的:

Uncaught (in promise) DOMException: The element has no supported sources.

原因就在于audio.play()是同步的,而这个时候DOM更新是异步的,src属性还没有被更新,结果播放的时候src属性为空,就报错了。

解决办法就是在play的操作加上this.$nextTick()。

this.$nextTick(function() { this.$refs.audio.play(); });

异步更新有什么好处?

<template> <div> <div>{{test}}</div> </div> </template> <script> export default { data () { return { test: 0 }; }, mounted () { for(let i = 0; i < 1000; i++) { this.test++; } } } </script>

上面的例子非常直观,可以这么理解当数据更新同步操作DOM会出现频繁渲染视图造成页面卡顿,极端的消耗资源。所以异步更新大大提升了性能, 并且数据更新很高效体验并没有降低。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持软件开发网。



VUE 队列 更新 异步 微任务

需要 登录 后方可回复, 如果你还没有账号请 注册新账号