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