浏览器的事件循环
单线程
浏览器是多线程架构,主要包含了4个进程。
我们前端主要需要了解的就是Renderer Process,而它也主要包含了5个线程
从图中我们看出js引擎是单线程的,也就是说同一时间js引擎只能做一件事,其他的任务只能排队。
同步和异步
Javascript单线程任务被分为同步任务和异步任务
何为同步?就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回。按照这个定义,其实绝大多数函数都是同步调用。
异步的概念和同步相对。当一个异步功能调用发出后,调用者不能立刻得到结果。当该异步功能完成后,通过状态、通知或回调来通知调用者。
这边有个经典的响水壶事件, 帮助大家理解这些概念。
宏任务和微任务
在JavaScript中,除了广义的同步任务和异步任务,还可以细分,一种是宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask)
二者执行顺序流程图如下:
那哪些宏任务,哪些是微任务呢?
宏任务: script(整体代码),setTimeout, setInterval, setImmediate,I/O, UI rendering
微任务: promise, Object.observe, MutationObserver
再谈事件循环
先看一句话:JavaScript has a runtime model based on an event loop(JavaScript 有一个基于事件循环的并发模型)
再看一张图:
js引擎在执行我们的代码的时候有一个执行栈的东西,是一个栈结构。任务执行前都会被压入栈中,当执行结束之后就会被弹出。在任务执行的时候也会发生其他的函数调用,那么其他的函数也会被压入栈中,执行结束后同样会被弹出。在这个过程中也会出现异步调用,比如ajax或setTimeout等,当请求有了结果或者倒计时结束,对应的回调函数会被注册到任务队列中等待主线程调用。当执行栈清空后事件循环将会处理任务队列中的下一个任务如果还有的话)。
事件循环介绍完了, 我们再通过一段代码分析一下
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
setTimeout(() => {
console.log(3);
}, 1000);
new Promise(resolve => {
console.log(4);
resolve();
}).then(() => {
console.log(5);
}).then(() => {
console.log(6);
});
console.log(7);
Promise和Generator
Promise
MDN上有这样一句话:Promise是一个对象,代表着一个异步操作的最终完成或者失败。
一个Promise有三种状态:
- 待定(pending) :初始状态,既没有被兑现,也没有被拒绝。
- 已兑现(fulfilled) :意味着操作成功完成。
- 已拒绝(rejected) :意味着操作失败。 Promise只能从pending变为fulfilled或rejected, 且一旦状态发生改变,那么状态便不可以再改变。
在Promise出现之前,我们想做多重的异步操作,会导致回调地狱的问题:
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
Promise出现之后我们有了更加优雅的写法
doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
我们发现Promise不经使得写法变的优雅,同时代码的可读性和可维护性都有了提升。
Generator
字面意思理解是“生成器”的意思,它其实和Promise一样是ES6出现的新语法.同样是异步编程的一个解决方案。
Generator对象的原型上定义了3个方法:
- Generator.prototype.next(): 该方法返回一个包含属性
done和value的对象。该方法也可以通过接受一个参数用以向生成器传值 - Generator.prototype.return(): 该方法返回给定的值并结束生成器
- Generator.prototype.throw()方法用来向生成器抛出异常,并恢复生成器的执行,返回带有
done及value两个属性的对象
我们简单的看一下他的用法
function* gen() {
const x = yield 1
const y = yield 2 + x
return y
}
const itor = gen()
itor.next() // { value:1, done: false }
itor.next(1) // { value:3, done: false }
itor.next(3) // { value:3, done: true }
从代码中我们可以看出一些有意思的点, 这个函数执调用后,他的函数体不会立即执行,而是需要我们调用next方法函数才会开始执行,并且会停止在yield处,等待下次next调用后再继续往下执行。这种交出函数的控制权并且在将来继续恢复执行就是协程的实现。
协程
我们先看下面这张图
稍微解读一下这张图
- 线程是进程中的执行体,同样协程就是线程自己创建的执行体。
- 协程是由线程调度的,为了实现协程的切换,线程需要记录协程的控制信息,包括标识符、执行栈的位置、执行入口、执行现场等等。
- 因为用户程序无法操作内核空间,所以只能给协程分配用户栈,而操作系统对协程一无所知,所以协程又叫做用户态线程。
Async/Await
介绍
async/await异步编程的终极解决方法,可以让我编写异步代码像写同步代码一样。
- async async 函数是使用
async关键字声明的函数,并且其中允许使用await关键字 - await
await操作符用于等待一个Promise对象。它只能在异步函数async function中使用.
简单看下async/await使用
function f1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("f1");
resolve();
}, 1000);
});
}
function f2() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("f2");
resolve();
}, 1000);
});
}
function f3() {
console.log("f3");
}
async function run() {
await f1();
await f2();
f3();
}
run();
// f1 f2 f3
co函数库
async和await本质上是一个语法糖,基于Generator和Promise。接下来我们看一下co函数库基于Generator和Promise的实现。
function co(gen) {
var ctx = this;
var args = Array.prototype.slice.call(arguments, 1);
// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function (resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}
/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(
new TypeError(
'You may only yield a function, promise, generator, array, or object, ' +
'but the following object was passed: "' +
String(ret.value) +
'"'
)
);
}
});
}
上面async/await实现的例子,我们可以同样使用co函数库实现
// npm install co
const co = require("co");
function f1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("f1");
resolve();
}, 1000);
});
}
function f2() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("f2");
resolve();
}, 1000);
});
}
function f3() {
console.log("f3");
}
function run() {
co(function* () {
yield f1();
yield f2();
f3();
});
}
run();
// f1 f2 f3
总结
本文重事件循环入手, 再去一一列举前端的异步处理方案(回调、Promise、Generator、async/await)。