单线程
JavaScript是一门单线程的语言,被广泛应用于浏览器和页面DOM元素交互,自从Node.js出现后,JavaScript领域由浏览器扩展到服务器,逐渐变成一种通用的计算机程序设计语言。
由于JavaScript的执行环境是单线程的,也就是说JS引擎中负责解析和执行JS代码的线程只有一个,而且一次只能完成一项任务,一个任务执行完毕后才能执行下一个。换句话说,多个任务执行时,当前任务会阻塞其它任务,当前任务也就是主线程。但实际上还有其它线程,如事件触发线程、AJAX请求线程等,这也就引发了同步和异步的问题。
同步异步
JS中的任务可以分为两种同步和异步,同步任务是在主线程上排队执行的任务,只有前一个任务执行完毕后才能执行后一个任务。异步任务是不进入主线程而进入任务队列中的任务,只有任务队列通知主线程某个异步任务才能执行,此时这个任务才会进入到主线程执行。只有执行栈中所有同步任务都执行完毕后系统才会读取任务队列,看看里面的异步任务哪些可以执行...
由于单线程模式中一次只能执行一个任务,函数调用后必须等待执行结束返回结果才能进行下一个任务。若当前任务执行时间较长,就会导致线程阻塞。也就是说,如果请求的时间较长,而阻塞了后面代码的执行,对于耗时类操作并不适合。
// 同步模式
var x = true;
while(x);//while死循环会堵塞进程
console.log("don't carry out");//这里永远不会执行了
JS虽然是单线程的,但为了避免IO操作阻塞主线程,必须采用回调函数callback
的形式将耗时的IO操作委托给其他IO线程进行处理,所以说JS并不是纯粹的单线程,只是有一个主线程在做主循环而已。
异步模式与同步模式相反,可以一起同时执行多个任务,函数调用后不会立即而返回执行的结果。若任务A需要等待,可先执行任务B,等到任务A结果返回后再继续回调。最常见的异步模式就是定时器。
// 定时器是异步的
setTimeout(function(){
console.log("task A, asynchronous");
}, 0);
console.log("task B, synchronize");
// 定时器延时为0,为什么taskA还是晚于taskB呢?
// 因为定时器是异步的,异步任务会在当前脚本中所有同步任务执行完毕后才会执行。
task B, synchronize
task A, asynchronize
回调函数
在JavaScript的世界中,所有的代码都是单线程执行的,由于这个天生的缺陷,导致JavaScript中所有的网络操作、浏览器事件等都必须是异步执行。那么,JavaScript是如何实现异步编程的呢?
function callback()
{
console.log("callback");
}
console.log("before");
setTimeout(callback, 1000);//1秒后调用callback函数
console.log("after");
// 脚本执行后控制台输出
before
after
// 1秒后调用callback函数
callback // 异步操作会在未来的某个时间点上触发某个函数调用
回调地狱
JavaScript的异步是采用Callback
回调函数来实现的,典型的是AJAX操作。
// 典型的异步操作 AJAX
request.onreadystatechange = function()
{
if(request.readyState === 4)
{
if(request.status === 200)
{
return success(request.responseText);
}
else
{
return fail(request.status);
}
}
}
// 回调函数success和fail在AJAX操作中很正常,但不利用代码复用,有没有更好的写法,比如这样呢?
ajaxGet(url).ifSuccess(success).ifFail(fail);
// 这种链式写法先统一执行AJAX逻辑,并且不关心如何处理结果。然后,根据结果是成功还是失败,在未来某个时间调用success或fail函数。
Callback
回调函数可以接收外部程序传入的参数,但是却没有办法先外部传值,只能通过下一层的Callback
回调函数来使用。当逻辑复杂的时候,Callback
回调函数嵌套会变得很深,在这种情况下,参数互相影响导致bug增加,这种Callback
回调嵌套被称为Callback
回调地狱。如何解决回调嵌套太深而引发的回调地狱的问题呢?
在JavaScript中所有代码都是单线程执行的,由于这个缺陷导致JavaScript中所有网络操作、浏览器事件都必须是异步执行。
Promise
Promise是异步编程的一种解决方案,比传统的回调函数和事件更加合理和强大。
Promise的概念是由CommonJS小组成员在Promises/A规范中提出的。根据Promises/A规范,promise
是一个代理对象,promise
代理的是一个值,这个值可称之为promise
对象的状态值。promise
的状态值分为三种分别是pending
、resolved
、rejected
。
Promise由社区最早提出并实现,ES6将其写入语言标准并统一了用法,原生提供了Promise对象。
Promise简单来说是一个容器,里面保存着某个将来才会结束的事件(异步操作),从语法上说Promise是一个对象,它可以获取异步操作的消息,Promise提供了统一的API,各种异步操作都可以使用相同的方法进行处理。
Promise对象有两个特点
- 对象的状态不受外界影响
Promise对象代表了一个异步操作,具有三种状态:进行中pending
、已成功fulfilled
、已失败rejected
。只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是Promise名字的由来“承诺”,表示其他手段无法改变。
- 一旦状态改变将不会再变
Promise对象的状态改变只有两种情况:从pending
变为fulfilled
、从pending
变为rejected
。只要这两种情况发生状态就凝固了不会再变了,而且会一直保持这个结果,此时就称为resolved
以定型。如果改变已经发生了,再对Promise对象添加回调函数也会立即得到这个结果,这与事件完全不同,事件的特点是如果你错过了它再去监听是得不到结果的。
Promise的构造方法
let promise = new Promise((resolve, reject) => {
resolve();//异步处理
});
promise
的构造函数中会传入一个处理器函数executor
,函数具有两个参数分别是resolve
和reject
,当都遭函数执行时,executor
函数会立即异步执行。
const setDelay = (millisecond) => {
return new Promise( (resolve, reject) => {
if(typeof millisecond !=="number" ){
reject(new Error("参数必须是数字类型"));
}
setTimeout( () => {
resolve(`延迟${millisecond}毫秒`);
}, milliscond);
})
}
executor
函数体内可以编写业务逻辑代码,一般业务了逻辑代码包含正常执行逻辑和异常出错处理,在代理的正常执行逻辑中会调用resolve
方法将promise
的状态值修改为resolved(retval)
,在异常出错逻辑中会调用reject(error)
将promise
的状态值修改为rejected
。也就是说executor
函数执行成功还是失败是可以从promise
的状态值中判断的出。
这里需要注意两点
-
promise
从pending
状态变为resolved
或rejected
状态只会有一次,一旦变成resolved
或rejected
状态之后,这个promise
的状态就再也不会改变了。 - 通过
resolve(retval)
传入的retval
返回值可以是任意值,通过reject(error)
传入的error
一般会是一个new Error("error")
的对象。
promise
的状态变化有什么用呢?它的状态可以影响后续then
的行为。
promise.then(
function onFulfilled(value){}
).catch(
function onRejected(error){}
);
当promise
的状态是resolved
的时候,会调用then
方法中的onFulfilled
函数,其中参数value
值是resolve(retval)
传入的。如果是rejected
状态会调用catch
函数中的onRejected
函数,其参数error
则是通过rejected(error)
传入的。
等价形式
promise.then(
function onFulfilled(value){},
function onRejected(error){}
);
then
方法带有三个参数成功回调、失败回调、前进回调。一个全新的promise
对象从每个then
方法的调用中返回。
Promise是抽象的异步处理对象
Promise对象表示未来发生的事件,在创建promise
时其参数传入的函数是会被立即执行的,只是其中执行代码可以异步代码。
简单来说,then
方法就是把原来的回调写法分离出来,在异步操作执行后,用链式调用的方式执行回调函数。Promise的优势就是在于这个链式调用。
Promise的构造函数接受一个参数是函数,并传入两个参数resolve
和reject
,分别表示异步操作执行成功后的回调函数、异步操作执行失败后的回调函数。按标准来说,resolve
是将Promise的状态设置为fullfilled
,reject
是将Promise的状态设置为rejected
。
let cookie = ()=>{
return new Promise((resolve, reject)=>{
console.log("cookie begin");
//使用setTimeout模拟异步操作
setTimeout(()=>{
if(true){
console.log("cookie over");
resolve("cookie")
}else{
reject("reject")
}
},1000)
})
};
let eat = ()=>{
return new Promise((resolve, reject)=>{
console.log("eat begin");
setTimeout(()=>{
if(true){
console.log("eat over");
resolve("eat")
}else{
reject("eat")
}
},1000)
})
};
let wash = ()=>{
return new Promise((resolve, reject)=>{
console.log("wash begin");
setTimeout(()=>{
if(true){
console.log("wash over");
resolve("the end")
}else{
reject(eat)
}
},1000)
})
};
then
一个promise
必须提供一个then
方法以访问当前值、最终值、错误原因。
promise.then(onFulfilled, onRejected)
then
方法接收两个可选参数onFulfilled
和onRejected
-
onFulfilled
参数:若参数为非函数则忽略
简单来说
-
then
方法提供了一个自定义的回调函数,若传入非函数则直接忽略当前then
方法。 - 回调函数中会将上一个
then
方法中的返回值作为参数供当前then
方法调用。 -
then
方法执行完毕后需要返回一个新值给下一个then
方法调用 - 每个
then
只能使用前一个then
的返回值
使用resolve
方法将Promise对象的状态设置为完成状态,此时then
方法就能捕获到变化,并执行“成功”情况的回调。reject
方法则是把Promise对象的状态设置为失败,此时then
方法执行失败情况的回调。
cookie().then((data)=>{
console.log(data);
return eat(data)
}).then((data)=>{
console.log(data);
return wash(data)
}).then((data)=>{
console.log(data);
});
简写形式
cookie().then(eat).then(wash).then((data)=>{
console.log(data);
});
最终输出
cookie
eat begin
eat over
eat
wash begin
wash over
the end
catch
- 处理失败的情况可以使用
then(null, ...)
,或是使用catch
方法。 - Promises/A规范指出当Promise实例状态修改为
reject
时,同时该错误会被下一个catch
方法指定的回调函数捕获。
cookie().then((data)=>{
throw new Error("cookie error");
console.log(data);
return eat(data)
}).then((data)=>{
throw new Error("eat error");
console.log(data);
return wash(data)
}).then((data)=>{
throw new Error("wash error");
console.log(data);
}).catch((error)=>{
console.log(error)
});
-
cache
和then
的第二个参数一样,用来指定reject
失败的回调。另一方面,当执行resolve
的回调时,若抛出异常,此时是不会卡死js,而会进入catch
模块。这个错误的捕获是非常有用的,它能帮助我们在开发中识别代码错误。
cookie begin
cookie over
Error: cookie error
at cookie.then (test.js:43:11)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:169:7)
all
- Promise提供的
all
方法可并行执行异步操作,并且在所有异步操作执行完毕后才执行回调。
Promise.all([cookie(), eat(), wash()]).catch((error)=>{
console.log(error)
});
const clubs = [
{id:1, owner:"joe", status:1},
{id:2, owner:"lvy", status:0},
{id:3, owner:"ben", status:1}
];
const players = [
{id:1, username:"joe", account:124314},
{id:2, username:"charly", account:424614},
{id:3, username:"mary", account:994334},
];
const getClubById = id=>new Promise((resolve, reject)=>{
setTimeout(()=>{
const club = clubs.find(item=>item.id === id);
if(club){
resolve(club);
}else{
reject(Error("no club found"));
}
}, 1000);
});
const getPlayerById = id => new Promise((resolve, reject)=>{
setTimeout(()=>{
const player = players.find(item=>item.id === id);
if(player){
resolve(player);
}else{
reject(Error("no player found"));
}
}, 1000);
});
Promise.all([getClubById(1), getPlayerById(2)]).then(response=>{
const [club, player] = response;
console.log(club, player);
}).catch(error=>console.error(error));
race
- Promise提供的
race
方法与all
一样,只不过all
是等所有异步操作都完毕后才执行then
回调,而race
得话,只要有一个异步操作执行完毕,就立即执行then
是拿出
Promise.race([cookie(), eat(), wash()]).catch((error)=>{
console.log(error)
});
古人云:“君子一诺千金”,这种“承诺未来会执行”的对象在JavaScript中称为Promise对象。Promise有各种开源实现,在ES6中被统一规范,并由浏览器直接支持。
// 测试浏览器是否支持ES6的Promise对象
"use strict";
// Promise 对象用于表示一个异步操作的最终状态(完成或失败),以及其返回值。
var obj = new Promise(function(resolve, reject){
// 执行异步操作...
setTimeout(function(){
console.log("execute over");
resolve(1);// resolve将Promise的状态置为fullfiled
}, 2000);
});
console.log("ok");
Promise对象是一个代理对象,被代理的值在Promise对象创建时可能是未知的。Promise允许为异步成功和失败分别绑定对应的处理函数(handler),这样异步方法可以像同步方法一样使用返回值,但它并不是立即返回最终执行结果,而是返回一个能代表未来出现的结果的Promise对象。
Promise是JavaScript中解决回调地狱的一种方式,也被ECMAScript协会承认,固化为ES6语法中的一部分。
错误处理
(node:616) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): [object Object]
(node:616) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
未处理的承诺拒绝UnhandledPromiseRejectionWarning
错误原因
当 Promise
的状态变为 rejection
时,没有正确处理,让其一直冒泡propagation
,直至被进程捕获。这个 Promise
就被称为 unhandled promise rejection
。
拒绝警告:不推荐使用未经处理的承诺拒绝。
Bluebird
Bluebird是早期Promise的一种实现,它提供了丰富的接口和语法糖用于降低Promise的使用难度。
Bluebird的安装和引入
$ npm i bluebird
const Promise = require("bluebird");
Promise的创建和使用
使用new Promise
传入两个参数resolve
和reject
方法,当Promise调用成功后会执行resolve
方法,并封装返回数据。当调用失败时会执行reject
方法并封装失败原因。需要注意的时,Promise的返回值只能在链式调用中使用。
async/await
promise
调用链看起来比callback
方式清晰很多,但仍存在不足之处:
- 不够简洁,仍然需要创建
then
的调用链,需创建匿名函数将返回值一层层传递给下一个then
调用。 - 异常不会向上抛出,若某个
then
中的函数抛出异常,即使没有写catch
异常也不会向上抛出,所以在then
的调用链外写的try...catch
是没有效果的。 - 代码调试问题,若在某个
then
的方法中设置断点然后一步步向下走,是不能步进到下一个then
的方法的,只能每个then
方法中设置断点,然后resume run
到下一个断点。
所以ES7提出新的Async/Await标准,async/await
应运而生,async
是一个函数的修饰符,添加上async
关键词的函数会隐式地返回一个promise
,函数的返回值将作为promise resolve
的值。
await
后跟的一定是一个promise
,await
只能出现在async
函数内,await
的语义是必须等到await
后面的promise
有了返回值才继续执行await
的下一行代码。
function readFile(file){
return new Promise((resolve, reject) => {
fs.readFile(file, "utf8", (error, content)=>{
if(error){
return reject(error);
}else{
return resolve(content);
}
});
});
}
async function run(file){
try{
let result = await readFile(file);
}catch(error){
console.log("read fail");
}
}
相比promise
的写法,async/await
写法的好处是
- 代码简洁明了易于阅读和理解
- 抛出的异常可以被
try...catch
捕获 - 对程序员友好
await
可以步进到下一行代码
async
函数会返回一个Promise对象,当函数执行时一旦遇到await
就会先返回,等到触发的异步操作完成才会再接着执行函数体内后面的语句。async
函数中可能会有await
表达式,它会使async
函数暂停执行让出线程即跳出async
函数体然后继续执行后续脚本,等待表达式中的Promise解析完成后继续执行async
函数并返回解决结果。
async
定义异步函数
- 自动将函数转换为Promise
- 当调用异步函数时,函数返回值会被
resolve
处理。 - 异步函数内部执行在
async
函数中
await
暂停异步函数的执行
- 当在
promise
前使用时会等待promise
完成并返回promise
的结果 -
await
只能和promise
一起使用,不能和calback
一起使用。 -
await
只能使用在async
函数中
await
可以理解为是async wait
的缩写,await
必须出现在async
函数内部,不能单独使用。
async/await
并不会取代promise
,因为async/await
底层依然使用的是promise
。