浏览器的渲染进程是多线程的,以下:javascript
而js由于防止对DOM的操做产生混乱,所以它是单线程的。单线程就是一次只能只能一个任务,有多个任务的话须要一个个的执行,为了解决异步事件,js引擎产生了Event Loop机制。java
js引擎不是独立运行的,它运行在宿主环境中,咱们常见的即是浏览器,可是随着发展,nodej.s已经进入了服务器的领域,js还渗透到了其余的一些领域。这些宿主环境每一个人都提供了各自的事件循环机制。node
那么什么是事件循环机制呢?js是单线程的,单线程就是一次只能只能一个任务,有多个任务的话须要一个个的执行,为了解决异步事件,js引擎产生了Event Loop机制。js中任务执行时会有任务队列,setTimeout是在设定的时间后加到任务队列的尾部。所以它虽然是定时器,可是在设定的时间结束时,回调函数是否执行取决于任务队列的状态。换个通俗点的话来讲,setTimeout是一个“不太准确”的定时器。面试
直到ES6中,js中才从本质上改变了在哪里管理事件循环,ES6精确得制定了事件循环的工做细节,其中最主要的缘由是Promise的引入,这使得对事件循环队列调度的运行能直接进行精细的控制,而不像上面说到的”不太准确“的定时器。ajax
(1)对每一个宏任务而言,内部有一个都有一个微任务shell
(2)引入微任务的初衷是为了解决异步回调的问题npm
采用改方式,那么执行回调的时机应该是在前面全部的宏任务完成以后,假若如今的任务队列很是长,那么回调迟迟得不到执行,形成应用卡顿。编程
为了规避第一种方式中的这样的问题,V8 引入了第二种方式,这就是微任务的解决方式。在每个宏任务中定义一个微任务队列,当该宏任务执行完成,会检查其中的微任务队列,若是为空则直接执行下一个宏任务,若是不为空,则依次执行微任务,执行完成才去执行下一个宏任务。json
(3)常见的微任务有:数组
咱们来看一个常见的面试题:
console.log('start');
setTimeout(() => {
console.log('timeout');
});
Promise.resolve().then(() => {
console.log('resolve');
});
console.log('end');
复制代码
再看一个例子:
Promise.resolve().then(()=>{
console.log('Promise1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
});
setTimeout(()=>{
console.log('setTimeout1')
Promise.resolve().then(()=>{
console.log('Promise2')
})
},0);
console.log('start');
// start
// Promise1
// setTimeout1
// Promise2
// setTimeout2
复制代码
接下来从js异步发展的历史来学习异步的相关知识
回调是js中最基础的异步模式。
listen("click", function handle(evt){
setTimeout(function request(){
ajax("...", function response(test){
if (text === "hello") {
handle();
} else {
request();
}
})
}, 500)
})
复制代码
这种代码经常被成为回调地狱, 有时候也叫毁灭金字塔。由于多个异步操做造成了强耦合,只要有一个操做须要修改,只要有一个操做须要修改,它的上层回调函数和下层回调函数就须要跟着修改,想要理解、更新或维护这样的代码十分的困难。
有的回调函数不是由你本身编写的,也不是在你直接的控制下的。多数状况下是第三方提供的。这种称位控制反转,就i是把本身程序的一部分执行控制交给了第三方。而你的代码和第三方工具之间没有一份明确表达的契约。会形成大量的混乱逻辑,致使信任链彻底断裂。
回调函数的两个缺陷:回调地狱和缺少可信任性。Promise解决了这两个问题。
Promise简单来讲就是一个容器,里面保存着某个将来才会结束的事件(一般是一个异步操做)的结果。
Promise至关于购餐时的订单号,当咱们付钱购买了想要的食物后,便会拿到小票。这时餐厅就在厨房后面为你准备可口的午饭,你在等待的过程当中能够作点其余的事情,好比看个视频,打个游戏。当服务员喊道咱们的订单时,咱们就能够拿着小票去前台换咱们的午饭。固然有时候,前台会跟你说你点的鸡腿没有了。这就是Promise的工做方式。
ES6规定,Promise对象是一个构造函数,用来生成Promise实例。
var promise = new Promise(function(resolvem reject) {
// some code
if (/*异步操做成功*/) {
resolve(value);
} else {
reject(error);
}
})
复制代码
Promise实例生成之后,可使用then方法分别指定Resolved状态和Rejected状态的回调函数
promise.then(function(value) {
// success
}, function(error) {
// failure
})
复制代码
then方法接受两个参数:第一个回调函数是Promise状态变为Resolved时调用的,第二个是Promise状态变成Rejected时调用
第二个参数是可选的,不必定要提供
两个函数都接受Promise对象传出去的值作参数。
reject函数传递的参数一半时Error对象的实例,表示抛出错误。
resolve函数除了传递正常值之外,还能够传递一个Promise实例
var p1 = new Promise(function(resolve, reject) {
//...
});
// 这种状况下,p1的状态决定了p2的状态。p2必须等到p1的状态变为resolve或reject才会执行回调函数
var p2 = new Promise(function(resolve, reject) {
//...
resolve(p1);
});
复制代码
then方法是定义在原型对象Promise.prototype上的。它的做用是为Promise实例添加改变状态时的回调函数。
then方法接受两个参数:第一个回调函数是Promise状态变为Resolved时调用的,第二个是Promise状态变成Rejected时调用
then方法返回的是一个新的Promise实例。所以能够采用链式的写法。
promise((resolve, reject) => {
// ...
}).then(() => {
// ...
}).then(() => {
// ...
})
复制代码
采用链式的写法能够指定一组按照次序调用的回调函数。若是前一个回调函数返回了一个Promise实例,那么后一个回调函数就会等待该Promise对象状态的变化再被调用。
promise((resolve, reject) => {
// ...
}).then(() => {
// ...
return new Promise((resolve, reject) => {
// ...
})
}).then((comments) => {
console.log("resolved: ", comments)
}, (err) => {
console.log("rejected: ", err)
})
// 或者能够写的更加简洁一些
promise((resolve, reject) => {
// ...
})
.then(() => new Promise((resolve, reject) => {...})
.then(
comments => console.log("resolved: ", comments),
err => console.log("rejected: ", err)
)
复制代码
Promise.prototype.catch()是方法.then(null, rejection)的别名,用于指定发生错误时的回调函数。
getJSON('/post.json').then((posts) => {
// ....
}).catch((error) => {
console.log("发生错误", error);
})
复制代码
getJSON返回一个Promise对象,若是该对象变成Resolved则会调用then()方法
若是异步发生错误或者then方法发生错误,则会被catch捕捉
Promise在resolve语句后面再抛出错误不会被捕获,由于Promise的状态一旦改变就不会再改变了。
var promise = new Promise((resolve, reject) => {
resolve('ok');
throw new Error('test')
})
promise
.then((value) => {console.log(value)})
.catch((error) => {console.log(error)})
复制代码
Promise对象的错误具备“冒泡”的性质,会一直向后传递,直到被捕获为止。也就是说,错误老是会被下一个catch捕获。通常来讲不要再then中定义第二个函数,而老是用catch方法。
var promise = new Promise((resolve, reject) => {
resolve('ok');
throw new Error('test')
})
// 不推荐
promise
.then(
(value) => {console.log(value)},
(error) => {console.log(error)}
)
//推荐
promise
.then((value) => {console.log(value)})
.catch((error) => {console.log(error)})
复制代码
和传统的try/catch不一样,若是没有使用catch指定错误处理的回调函数,promise对象抛出的错误不会传递到外层代码,即不会有任何反应
catch返回的也是一个Promise对象,后面还能够跟then
不管Promise对象的回调链是以then方法结束仍是以catch方法结束,只要最后一个方法抛出错误,都有可能没法捕捉到(由于Promise内部的错误不会冒泡到全局)。为此能够提供一个done()方法,他老是在回调链的尾部,保证抛出任何可能出现的错误。
asyncFunc ()
.then(f1)
.catch(f2)
.then(f3)
.done()
复制代码
它的源码实现很简单:
Promise.prototypr.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch(function(reason){
// 抛出一个全局错误
setTimeout(() => {throw reason}, 0)
})
}
复制代码
finally方法用于指定无论Promise对象最后如何都会执行的操做。他与done方法的最大区别在于它接受一个回调函数做为参数,该函数无论怎么样都会执行。来看看它的实现方式。
Promise.prototype.finally = function (callback) {
let P = this.constructor
// 巧妙的使用Promise.resolve方法,达到无论前面的Promise状态是fulfilled仍是rejected,都会执行回调函数
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => throw reason)
)
}
复制代码
Promise.all方法用于将多个Promise实例包装成一个新的Promise实例
var p = Promise.all([p1, p2, p3])
复制代码
p一、p二、p3都是Promise实例,若是不是,则会使用Promise.resolve方法,将参数转化为Promise实例,再进行处理
该方法的参数不必定是要数组,但必需要有Iterator接口,且每一个组员都是Promise实例
p的状态由p一、p二、p3决定
var promises = [2, 3, 4, 5, 6, 7].map((id) => {
return getJSON(`/post/${id}.json`)
})
Promise.all(promises).then((posts) => {
//...
}).catch((error) => {
//...
})
复制代码
若是做为参数的Promise实例自身定义了catch方法,那么它被rejected时并不会出发Promise.all()的catch方法
const p1 = new Promise((resolve, reject) => {
resolve('hello')
})
.then(result => result)
.catch(e => e)
const p2 = new Promise(resolve, reject) => {
throw new Error('error')
})
.then(result => result)
.catch(e => e)
const p3 = new Promise(resolve, reject) => {
throw new Error('error')
})
.then(result => result)
// p2的catch返回了一个新的Promise实例,该实例的最终状态是resolved
Promise.all([p1, p2])
.then(result => result)
.catch(e => e)
// ["hello", Error: error]
// p3没有本身的catch,因此错误被Promise.all的catch捕获倒了
Promise.all([p1, p3])
.then(result => result)
.catch(e => e)
// Error: error
复制代码
Promise.race方法用于将多个Promise实例包装成一个新的Promise实例
var p = Promise.race([p1, p2, p3])
复制代码
p一、p二、p3都是Promise实例,若是不是,则会使用Promise.resolve方法,将参数转化为Promise实例,再进行处理
该方法的参数不必定是要数组,但必需要有Iterator接口,且每一个组员都是Promise实例
p的状态由p一、p二、p3决定,只要p一、p二、p3有一个实例率先改变状态,p的状态就会跟着改变。率先改变状态的实例的返回值传递给p的回调函数。
Promise.resolve方法将现有对象转换成Promise对象,分为如下四种状况:
Promise.resolve不作任何改变
thenable对象是指具备then方法的对象
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
}
let p1 = Promise.resolve(thenable)
p1.then(function(value) {
console.log(value) // 42
})
复制代码
Promise.resolve会将这个对象转换成Promise对象,而后当即执行thenable对象的then方法
该状况下,Promise.resolve返回一个新的Promise对象,状态为Resolved
var p = Promise.resolve('hello');
p.then((s) => {
console.log(s)
})
// hello
复制代码
此状况下,Promise.resolve方法返回一个Resolved状态的Promise对象
console.log('start');
setTimeout(() => {
console.log('timeout');
});
Promise.resolve().then(() => {
console.log('resolve');
});
console.log('end');
复制代码
(1)先执行同步队列的任务,所以先打印start和end (2)setTimeout 做为一个宏任务放入宏任务队列 (3)Promise.then做为一个为微任务放入到微任务队列 (4)Promise.resolve()将Promise的状态变为已成功,即至关于本次宏任务执行完,检查微任务队列,发现一个Promise.then, 执行 (5)接下来进入到下一个宏任务——setTimeout, 执行
Promise.reject方法会返回一个新的Promise实例,状态为Rejected
与Promise.resolve不一样,Promise.reject会原封不动的将其参数做为reject的理由传递给后续的方法,所以没有那么多的状况分类
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
}
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
//true
复制代码
Promise解决了回调函数的回调地狱的问题,可是Promise最大的问题是代码的冗余,原来的任务被Promise包装后,不管什么操做,一眼看过去都是许多then的堆积,原来的语义变得很不清楚。
传统的编程语言中早有异步编程的解决方案,其中一个叫作协程,意思为多个线程相互做用,完成异步任务。它的运行流程以下:
function *asyncJob () {
// ...
var f = yield readFile(fileA);
// ...
}
复制代码
它最大的优势就是,代码写法很像同步操做。
Generator函数是协程在ES6中最大的实现,最大的特色就是能够交出函数的执行权。
整个Generator函数就是一个封装的异步任务容器,异步操做须要用yield代表。Generator他能封装异步任务的缘由以下:
上面代码的Generator函数的语法相关已经在上一篇博客中总结了,不能理解此处能够前往复习。
Generator函数是一个异步操做的容器,它的自动执行须要一种机制,当异步操做有告终果,这种机制须要自动交回执行权,有两种方法能够作到:
回调函数:将异步操做包装成Thunk函数,在回调函数里面交回执行权
Promise对象:将异步操做包装成Promise对象,使用then方法交回执行权
参数的求值策略有两种,一种是传值调用,另外一种是传名调用
- 传值调用,在参数进入函数体前就进行计算;可能会形成性能损失。
- 传名调用,在参数被调用时再进行计算。
编译器的传名调用的实现将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫Thunk函数。
function f(m) {
return m * 2;
}
f(x + 5);
// 等同于
var Thunk = function () {
return x + 5;
}
function f(thunk) () {
return thunk() * 2
}
复制代码
js语言是按值调用的,它的Thunk函数含义和上述的有些不一样。在js中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数做为参数的单参数函数。
(1)在js中,任何函数,只要参数有回调函数就能够写成Thunk函数的形式。
// ES5
var Thunk = function (fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
return function (callback) {
return function (callback) {
args.push(callback);
return fn.apply(this, args)
}
}
}
}
// ES6
var Thunk = function (fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback)
}
}
}
// 实例
function f (a, cb) {
cb(a)
}
const ft = Thunk(f);
ft(1)(console.log); // 1
复制代码
(2)生产环境中使用Thunkify模块
$ npm install Thunkify
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
read('package.json')(function(err, str) {
// ...
})
复制代码
前面提到了Thunk能够用于Generator函数的自动流程管理
(1)Generator能够自动执行
function *gen() {
// ...
}
var g = gen();
var res = g.next();
while (!res.done) {
console.log(res.value);
res = g.next();
}
复制代码
可是这不适合异步操做,若是必须知足上一步执行完成才能执行下一步,上面的自动执行就不可行。
(2)Thunk函数自动执行
var thunkify = require('thunkify');
var fs = require('fs');
var readFileThunk = thunkify(fs.readFile);
var gen = function* () {
var r1 = yield readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFileThunk('/etc/shell');
console.log(r2.toString());
}
var g = gen();
// 将同一个函数反复传入next方法的value属性
var r1 = g.next();
r1.value(function(err, data) {
if (err) throw err;
var r2 = g.next(data);
r2.value(function (err, data) {
if (err) throw err;
g.next(data);
})
})
// Thunk函数自动化流程管理
function run (fn) {
var gen = fn();
function next (err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next)
}
next();
}
run(g)
复制代码
上述的run函数就是以一个Generator函数自动执行器。有了这个执行器,无论内部有多少个异步操做,直接在将Generator函数传入run函数便可,可是要注意,每个异步操做都是Thunk函数,也就是说yield后面必须是Thunk函数。
co模块不须要编写Generator函数的执行器
var co = require('co');
// gen函数自动执行
co(gen);
// co函数返回一个Promise对象,所以能够用then方法添加回调
co(gen).then(function () {
console.log('Generator函数执行完毕')
})
复制代码
var fs = require('fs');
var readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function (error, data) {
if (error) return reject(error);
resolve(data);
})
})
}
var gen = function* () {
var r1 = yield readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFileThunk('/etc/shell');
console.log(r2.toString());
}
var g = gen()
// 手动执行,使用then方法层层添加回调函数
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data)
})
})
// 根据手动执行,写一个自动执行器
function run (gen) {
var g = gen();
function next(data) {
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function (data) {
next(data);
})
}
next();
}
run(gen)
复制代码
ES2017标准引入了async函数,使得异步操做变得更加方便。async函数就是Generator函数的语法糖。
async函数就是将Generator函数的*换成async,将yield换成await。
varasyncReadFile = async function () {
var r1 = await readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = await readFileThunk('/etc/shell');
console.log(r2.toString());
}
复制代码
async对于Generator的改进有三点:
// 函数式声明
async function foo() {}
// 函数表达式
const foo = async function() {}
// 箭头函数
const foo = async () => {}
// 对象方法
let obj = { async foo() {} }
obj.foo().then(...)
// class方法
class Storage {
constructor () { ... }
async getName() {}
}
复制代码
(1)async函数返回一个Promise对象
async function f() {
return 'hello'
}
f().then(v => console.log(v)) // hello
复制代码
async function f() {
throw new Error('出错了');
}
f().then(
v => console.log(v)
e => console.log(e)
)
// Error: 出错了
复制代码
(2)await命令
正常状况下await命令后面是一个Promise对象,若是不是会被resolve当即转成一个Promise对象
await命令后面的Promise对象若是变成reject状态,则reject的参数会被catch方法的而回调函数接收到
有时不但愿抛出错误终止后面的步骤
async function f() {
try {
await Promise.reject('出错了')
} catch(e) {
}
return await Promise.resolve('hello')
}
f().then( v => console.log(v)) // hello
async function f1() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello')
}
f1().then( v => console.log(v)) // hello
复制代码
await命令只能在async函数中使用,不然会报错
若是await命令后面的异步操做不是继发关系,最好让他们同步触发
let foo = getFoo();
let bar = getBar();
// 写法1
let [foo, bar] = await Promise.all([getFoo(), getBar()])
// 写法2
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
复制代码
参考资料:
- 偶像神三元的博客
- 阮一峰老师的ES6
- 你不知道的JavaScript(中)