概述
我曾多次去了解事件循环,想看清楚它的全貌,但直至如今我掌握的只是其中的一点皮毛。写这篇文章主要是再次学习与记录它。
文章主要介绍部分面试题、事件循环概述、primise中异步机制asap、vue中nextTick、Mutation observer等。
浏览器事件循环
由于js是一门单线程的语言,运行时所有的程序都在会一个执行队列中等待被执行。在js中将程序分为同步任务和异步任务,在运行时同步任务会直接进入主线程中执行形成一个执行栈,当栈中程序执行完,系统就会去读取异步任务队列(事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放一个事件回调),比如ajax、settimeout、click事件等。而上述过程的不断重复运作就成了事件循环。具体的可见下图:
我们来看一段经典的例子吧
//进入主线程
console.log('script start');
//直接输出script start
//挂起
setTimeout(function () {
console.log('setTimeout');
}, 0);
//等待被执行 primose执行完后 输出setTimeout
//挂起
Promise.resolve()
.then(function () {
console.log('promise1');
}).then(function () {
console.log('promise2');
});
//等待被执行script end 输出后 输出 promise1 promise2
//进入主线程
console.log('script end');
//直接输出script end
程序开始执行,如果不是异步直接执行,如果是异步代码进入异步队列挂起,当同步任务执行完,开始执行异步任务,当异步的回调结束返回值时,开始执行返回内容。
这段代码执行的结果为:script start script end promise1 promise2 setTimeout。很奇怪的是settimeout为什么会在promise后面呢?这就涉及到宏任务和微任务了。
宏任务&微任务
宏任务(macrotask)
在ECMAScript中,macrotask也被称为task
我们可以将每次执行栈执行的代码当做是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每一个宏任务会从头到尾执行完毕,不会执行其他。 常见的宏任务有:
- 主代码块
- setTimeout
- setInterval
- requestAnimationFrame
- /O、UI 交互事件
- setImmediate(Node.js 环境)
微任务
微任务是在es6 Promise出现时,浏览器新规定的一个概念。在ECMAScript中,microtask也被称为jobs。
我们已经知道宏任务结束后,会执行渲染,然后执行下一个宏任务, 而微任务可以理解成在当前宏任务执行后立即执行的任务。常见的微任务有:
- Promise
- MutaionObserver
- process.nextTick(Node.js 环境)
- Object.observe
异步任务运行机制
在事件循环中,每一次的循环我们称为tick,而其中tick的关键点为:
- 在 tick 中选择最先进入队列的任务( oldest task ),如果有则执行(一次)
- 检查是否存在 jobs ,如果存在则不停地执行,直至清空jobs Queue
- 更新 render
- 主线程重复执行上述步骤 图解如下
相关的例子以及常见面试题
监听div的点击
<div class="outer">
<div class="inner"></div>
</div>
<script>
let outer = document.querySelector('.outer');
let inner = document.querySelector('.inner');
new MutationObserver(function () {
console.log('mutate');
}).observe(outer, {
attributes: true,
});
function onClick() {
console.log('click');
setTimeout(function () {
console.log('timeout');
}, 0);
Promise.resolve().then(function () {
console.log('promise');
});
outer.setAttribute('data-random', Math.random());
}
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
</script>
<style>
.outer {
height: 200px;
width: 200px;
background: lightgray;
display: flex;
justify-content: center;
align-items: center;
}
.inner {
height: 100px;
width: 100px;
background: gray;
}
</style>
- 如果我们点击inner会发生什么呢?
- 触发事件onclike输出:click
- settimeout入宏任务队列挂起
- promise进入微任务队里挂起
- dom监听器监听到事件,进入微任务队列挂起
- primise出栈输入:promise
- 监听器出栈输出:mutate
- 父div触发onclick事件输出:click
- promise进入微任务队里挂起
- dom监听器监听到事件,进入微任务队列挂起
- primise出栈输入:promise
- 监听器出栈输出:mutate
- setimeout出栈输入:timeout 但是在不同浏览器中,可能结果不一样哦,因为浏览器内核,对异步机制有不一样的处理。
下面在举几个例子我就不一一解析了,大家自己分析判断吧
例子一:
async function one() {
console.log('1')
await two()
console.log('1 end')
}
function two() {
setTimeout(() => {
console.log('2')
})
}
one()
setTimeout(() => {
console.log('setTimeout')
})
new Promise((resolve, reject) => {
console.log('promise')
resolve()
}).then(function () {
console.log('then')
})
console.log('end')
运行结果:
例子二:
async function async1() {
console.log('async1 start');
await async2();
setTimeout(() => {
console.log('setTimeout1')
});
}
async function async2() {
setTimeout(() => {
console.log('setTimeout2')
});
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout3')
});
async1();
new Promise((resolve) => {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});
console.log('script end');
答案:
我想看完这二个面试题,对基本的事件循环机制应该能了解清晰了,但是更核心的问题是,js的异步机制到底是这么运行的呢?promise底层原理以及异步机制如何实现是用settimeout类似的方法吗?
promise原理以及异步机制
promise是一个解决异步的对象,其行为需要满足promiseA+规范。其中a+规范部分如下所述
术语
-
henable是一个定义了
then方法的对象或函数,文中译作“拥有then方法”; -
值(value)指任何 JavaScript 的合法值(包括
undefined, thenable 和 promise); -
异常(exception)是使用
throw语句抛出的一个值。 -
据因(reason)表示一个 promise 的拒绝原因。
-
Promise 的状态
一个 Promise 的当前状态必须为以下三种状态中的一种:等待态(Pending) 、执行态(Fulfilled)和拒绝态(Rejected) 。
-
等待态(Pending)处于等待态时,promise 需满足以下条件:可以迁移至执行态或拒绝态
-
执行态(Fulfilled)处于执行态时,promise 需满足以下条件: 不能迁移至其他任何状态,必须拥有一个不可变的终值
-
拒绝态(Rejected)处于拒绝态时,promise 需满足以下条件:不能迁移至其他任何状态,必须拥有一个不可变的据因
这里的不可变指的是恒等(即可用 === 判断相等),而不是意味着更深层次的不可变(**译者注:**盖指当 value 或 reason 不是基本值时,只要求其引用地址相等,但属性值可被更改)。
Then 方法
一个 promise 必须提供一个 then 方法以访问其当前值、终值和据因。
promise 的 then 方法接受两个参数:
promise.then(onFulfilled, onRejected)
大家可以参考下primise a+的规范
简单实现(来自于掘金作者‘’ssh_晨曦时梦见兮”的实现代码)
function Promise(fn) {
this.cbs = [];
const resolve = (value) => {
setTimeout(() => {
this.data = value;
this.cbs.forEach((cb) => cb(value));
});
}
fn(resolve);
}
Promise.prototype.then = function (onResolved) {
return new Promise((resolve) => {
this.cbs.push(() => {
const res = onResolved(this.data);
if (res instanceof Promise) {
res.then(resolve);
} else {
resolve(res);
}
});
});
};
这是比较经典的实现链式调用的方法,我也是看了很久才看懂的。
官方核心源码
'use strict';
var asap = require('asap/raw');
function noop() {}
// States:
//
// 0 - pending
// 1 - fulfilled with _value
// 2 - rejected with _value
// 3 - adopted the state of another promise, _value
//
// once the state is no longer pending (0) it is immutable
// All `_` prefixed properties will be reduced to `_{random number}`
// at build time to obfuscate them and discourage their use.
// We don't use symbols or Object.defineProperty to fully hide them
// because the performance isn't good enough.
// to avoid using try/catch inside critical functions, we
// extract them to here.
var LAST_ERROR = null;
var IS_ERROR = {};
function getThen(obj) {
try {
return obj.then;
} catch (ex) {
LAST_ERROR = ex;
return IS_ERROR;
}
}
function tryCallOne(fn, a) {
try {
return fn(a);
} catch (ex) {
LAST_ERROR = ex;
return IS_ERROR;
}
}
function tryCallTwo(fn, a, b) {
try {
fn(a, b);
} catch (ex) {
LAST_ERROR = ex;
return IS_ERROR;
}
}
module.exports = Promise;
function Promise(fn) {
if (typeof this !== 'object') {
throw new TypeError('Promises must be constructed via new');
}
if (typeof fn !== 'function') {
throw new TypeError('Promise constructor's argument is not a function');
}
this._deferredState = 0;
this._state = 0;
this._value = null;
this._deferreds = null;
if (fn === noop) return;
doResolve(fn, this);
}
Promise._onHandle = null;
Promise._onReject = null;
Promise._noop = noop;
Promise.prototype.then = function(onFulfilled, onRejected) {
if (this.constructor !== Promise) {
return safeThen(this, onFulfilled, onRejected);
}
var res = new Promise(noop);
handle(this, new Handler(onFulfilled, onRejected, res));
return res;
};
function safeThen(self, onFulfilled, onRejected) {
return new self.constructor(function (resolve, reject) {
var res = new Promise(noop);
res.then(resolve, reject);
handle(self, new Handler(onFulfilled, onRejected, res));
});
}
function handle(self, deferred) {
while (self._state === 3) {
self = self._value;
}
if (Promise._onHandle) {
Promise._onHandle(self);
}
if (self._state === 0) {
if (self._deferredState === 0) {
self._deferredState = 1;
self._deferreds = deferred;
return;
}
if (self._deferredState === 1) {
self._deferredState = 2;
self._deferreds = [self._deferreds, deferred];
return;
}
self._deferreds.push(deferred);
return;
}
handleResolved(self, deferred);
}
function handleResolved(self, deferred) {
asap(function() {
var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
if (cb === null) {
if (self._state === 1) {
resolve(deferred.promise, self._value);
} else {
reject(deferred.promise, self._value);
}
return;
}
var ret = tryCallOne(cb, self._value);
if (ret === IS_ERROR) {
reject(deferred.promise, LAST_ERROR);
} else {
resolve(deferred.promise, ret);
}
});
}
function resolve(self, newValue) {
// Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure
if (newValue === self) {
return reject(
self,
new TypeError('A promise cannot be resolved with itself.')
);
}
if (
newValue &&
(typeof newValue === 'object' || typeof newValue === 'function')
) {
var then = getThen(newValue);
if (then === IS_ERROR) {
return reject(self, LAST_ERROR);
}
if (
then === self.then &&
newValue instanceof Promise
) {
self._state = 3;
self._value = newValue;
finale(self);
return;
} else if (typeof then === 'function') {
doResolve(then.bind(newValue), self);
return;
}
}
self._state = 1;
self._value = newValue;
finale(self);
}
function reject(self, newValue) {
self._state = 2;
self._value = newValue;
if (Promise._onReject) {
Promise._onReject(self, newValue);
}
finale(self);
}
function finale(self) {
if (self._deferredState === 1) {
handle(self, self._deferreds);
self._deferreds = null;
}
if (self._deferredState === 2) {
for (var i = 0; i < self._deferreds.length; i++) {
handle(self, self._deferreds[i]);
}
self._deferreds = null;
}
}
function Handler(onFulfilled, onRejected, promise){
this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
this.onRejected = typeof onRejected === 'function' ? onRejected : null;
this.promise = promise;
}
/**
* Take a potentially misbehaving resolver function and make sure
* onFulfilled and onRejected are only called once.
*
* Makes no guarantees about asynchrony.
*/
function doResolve(fn, promise) {
var done = false;
var res = tryCallTwo(fn, function (value) {
if (done) return;
done = true;
resolve(promise, value);
}, function (reason) {
if (done) return;
done = true;
reject(promise, reason);
});
if (!done && res === IS_ERROR) {
done = true;
reject(promise, LAST_ERROR);
}
}
这里有时间我在给大家解析官方的代码(最近比较忙 凑个时间把重点的写写,有时间在回头改改)
关于primise的异步机制asap
asapasap 是 as soon as possible 的简称,在 Node 和浏览器环境下,能将回调函数以高优先级任务来执行(下一个事件循环之前),即把任务放在微任务队列中执行。
其中的核心为:
异步方法是通过 setImmediate 或 process.nextTick 来实现异步执行的任务栈,而 asap 方法是对 rawAsap 方法的进一步封装,通过缓存的 domain 和 try/finally 实现了即使某个任务抛出异常也可以恢复任务栈的继续执行(再次调用rawAsap.requestFlush)。其中主要包含两个源码文件:asap.js和raw.js。而重点都在raw.js的代码中,所以我们只有分析raw.js的代码即可。分析请看注释。
"use strict";
var domain; // The domain module is executed on demand
var hasSetImmediate = typeof setImmediate === "function";
//给外界导出的方法
module.exports = rawAsap;
//如果任务栈为空,则触发requestFlush方法,并始终把将要执行的task添加在任务栈queue的末尾
function rawAsap(task) {
if (!queue.length) {
requestFlush();
flushing = true;
}
// Avoids a function call
queue[queue.length] = task;
}
//任务队列
var queue = [];
var flushing = false;
// 下一个任务在任务队列中执行的位置
var index = 0;
var capacity = 1024;
//通过 while 循环依次去执行任务栈 queue 中的每一个任务
//如果遇到异常直接跳过开始下一个任务的执行
// 并处理内存泄漏的
function flush() {
while (index < queue.length) {
var currentIndex = index;
// 在调用任务之前先设置下一个任务的索引,可以确保再次触发 flush 方法时,跳过异常任务
index = index + 1;
queue[currentIndex].call()
// 防止内存泄露
if (index > capacity) {
for (var scan = 0, newLength = queue.length - index; scan < newLength; scan++) {
queue[scan] = queue[scan + index];
}
queue.length -= index;
index = 0;
}
}
queue.length = 0;
index = 0;
flushing = false;
}
// 设置为 rawAsap 的属性,方便在任务执行异常时再次触发 requestFlush
rawAsap.requestFlush = requestFlush;
//心代码其实就一句:setImmediate(flush),通过 setImmediate 异步执行 flush 方法。
// 而判断 parentDomain 以及设置和恢复 domain 都只是为了当前的 flush 方法不绑定任何域执行。
// 而这里还有一个 hasSetImmediate 判断,是为了做兼容降级处理,如果不存在 setImmediate 方法,
// 则使用 process.nextTick 方法触发异步执行。但使用 process.nextTick 方法有一个缺陷,就是它不能够处理递归。
function requestFlush() {
// 确保 flushing 未绑定到任何域
var parentDomain = process.domain;
if (parentDomain) {
if (!domain) {
// 惰性加载执行 domain 模块
domain = require("domain");
}
domain.active = process.domain = null;
}
if (flushing && hasSetImmediate) {
setImmediate(flush);
} else {
process.nextTick(flush);
}
if (parentDomain) {
domain.active = process.domain = parentDomain;
}
}
其他内容
vue中nextTick与事件循环
在 Vue.js 里是数据驱动视图变化,由于 JS 执行是单线程的,在一个 tick 的过程中,它可能会多次修改数据,但 Vue.js 并不会傻到每修改一次数据就去驱动一次视图变化,它会把这些数据的修改全部 push 到一个队列里,然后内部调用 一次 nextTick 去更新视图,所以数据到 DOM 视图的变化是需要在下一个 tick 才能完成。这便是我们为什么需要vue.nextTick。
但是在使用的过程中我发现:
- 如果代码不涉及异步能正常渲染界面
- 如果页面渲染的数据,在异步中执行(页面已渲染,但异步未返回),nextTick不能更新数据
- 而nextTick中的实现部分使用微任务部分使用宏任务,导致渲染时机不同。 解决方法: 一般使用settimeout将任务变为宏任务即可,但是在某些情况下面。如异步响应时间较长的时候,我们就需要使用监听器去监听数据的变量了,如果数据改变,我们在做相应的事情即可。
DOM 变动观察器(Mutation observer)
MutationObserver是一个内建对象,它观察 DOM 元素,并在检测到更改时触发回调。
MutationObserver 使用简单。
首先,我们创建一个带有回调函数的观察器:
let observer = new MutationObserver(callback);
然后将其附加到一个 DOM 节点:
observer.observe(node, config);
例如,这里有一个 <div>,它具有 contentEditable 特性。该特性使我们可以聚焦和编辑元素。
<div contentEditable id="elem">Click and <b>edit</b>, please</div>
<script>
let observer = new MutationObserver(mutationRecords => {
console.log(mutationRecords); // console.log(the changes)
});
// 观察除了特性之外的所有变动
observer.observe(elem, {
childList: true, // 观察直接子节点
subtree: true, // 及其更低的后代节点
characterDataOldValue: true // 将旧的数据传递给回调
});
</script>
如果我们在浏览器中运行上面这段代码,并聚焦到给定的 <div> 上,然后更改 <b>edit</b> 中的文本,console.log 将显示一个变动:
mutationRecords = [{
type: "characterData",
oldValue: "edit",
target: <text node>,
// 其他属性为空
}];
总结
哈哈哈 感觉自己的水平还是有限啊,整篇文档读下来,更像一个第三方文档。虽然也有不少自己的思考,但是都很浅。希望以后的文章精益求精吧,尽量多一点自己的想法。如果文章中有不当的地方欢迎大家指出错误哦,后期也会去不断维护和更新的。
引用
npm promise包
juejin.cn/post/684490…
jakearchibald.com/2015/tasks-…
juejin.cn/post/684490…
zhuanlan.zhihu.com/p/87684858
github.com/kriskowal/a…
www.ituring.com.cn/article/665…
juejin.cn/post/684490…