vue中的任务队列和异步更新策略(任务队列,微任务,宏任务)
目录
- 事件循环
- 任务队列
- 如何理解微任务和宏任务?
- 深究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会出现频繁渲染视图造成页面卡顿,极端的消耗资源。所以异步更新大大提升了性能, 并且数据更新很高效体验并没有降低。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。