Javascript语言的执行环境是单线程。即一次只能完成一个任务。如有多个任务则需排队逐个执行——前一个任务完成,再执行后一个任务。javascript
这种执行模式实现简单,执行环境相对单纯。但随着前端业务日渐复杂,事务和请求等日渐增多,这种单线程执行方式在复杂的业务下势必效率低下,只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),每每就是由于某一段Javascript代码长时间运行(好比死循环),致使整个页面卡在这个地方,其余任务没法执行。css
为避免和解决这种问题,JS语言将任务执行模式分为异步和同步。同步模式”就是上一段的模式,后一个任务等待前一个任务结束,而后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;”异步模式”则彻底不一样,每个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,因此程序的执行顺序与任务的排列顺序是不一致的、异步的。前端
“异步模式”很是重要。在浏览器端,耗时很长的操做都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操做。在服务器端,”异步模式”甚至是惟一的模式,由于执行环境是单线程的,若是容许同步执行全部http请求,服务器性能会急剧降低,很快就会失去响应。java
异步编程最基本方法。git
首先须要声明,回调函数只是一种实现,并非异步模式特有的实现。回调函数一样能够运用到同步(阻塞)的场景下以及其余一些场景。github
回调函数的英文定义:A callback is a function that is passed as an argument to another function and is executed after its parent function has completed。ajax
字面上的理解,回调函数就是一个参数,将这个函数做为参数传到另外一个函数里面,当那个函数执行完以后,再执行传进去的这个函数。这个过程就叫作回调。编程
在JavaScript中,回调函数具体的定义为: 函数A做为参数(函数引用)传递到另外一个函数B中,而且这个函数B执行函数A。咱们就说函数A叫作回调函数。若是没有名称(函数表达式),就叫作匿名回调函数。数组
用一个通俗的生活例子比喻一下就是:约会结束后你送你女友回家,离别时,你确定会说:“到家了给我发条信息,我很担忧你。” 而后你女友回家之后还真给你发了条信息。其实这就是一个回调的过程。你留了个参数函数(要求女友给你发条信息)给你女友,而后你女友回家,回家的动做是主函数。她必须先回到家之后,主函数执行完了,再执行传进去的函数,而后你就收到一条信息了。promise
假定有两个函数f1和f2,后者等待前者的执行结果。
f1();
f2();
复制代码
若f1是一个很耗时的任务,能够考虑改写f1,把f2写成f1的回调函数。
function f1(callback){setTimeout(function () {// f1的任务代码callback();}, 1000);}复制代码
执行代码就变成下面这样:
f1(f2);
复制代码
采用这种方式,咱们把同步操做变成了异步操做,f1不会堵塞程序运行,至关于先执行程序的主要逻辑,将耗时的操做推迟执行。
另外一个例子:
//定义主函数,回调函数做为参数
function A(callback) {
callback();
console.log('我是主函数');
}
//定义回调函数
function B(){
setTimeout("console.log('我是回调函数')", 3000);//模仿耗时操做
}
//调用主函数,将函数B传进去
A(B);
//输出结果
我是主函数
我是回调函数
复制代码
上面的代码中,咱们先定义了主函数和回调函数,而后再去调用主函数,将回调函数传进去。
定义主函数的时候,咱们让代码先去执行callback()回调函数,但输出结果倒是后输出回调函数的内容。这就说明了主函数不用等待回调函数执行完,能够接着执行本身的代码。因此通常回调函数都用在耗时操做上面。好比ajax请求,好比处理文件等。
再来一个更俗的例子:
<strong>问:你有事去隔壁寝室找同窗,发现人不在,你怎么办呢?</strong><strong>方法1</strong>,每隔几分钟再去趟隔壁寝室,看人在不<strong>方法2</strong>,拜托与他同寝室的人,看到他回来时叫一下你 前者是轮询,后者是回调。 那你说,我直接在隔壁寝室等到同窗回来能够吗? 能够啊,只不过这样本来你能够省下时间作其余事,如今必须浪费在等待上了。把原来的非阻塞的异步调用变成了阻塞的同步调用。 JavaScript的回调是在异步调用场景下使用的,使用回调性能好于轮询。复制代码
对于回调函数,通常在同步情境下是最后执行的,而在异步情境下有可能不执行,由于事件没有被触发或者条件不知足,因此请忽略上上个例子中的小问题,并非必定回调函数就要执行。
同时补充回调函数应用场合和优缺点:
回调函数这种方式的优势是比较容易理解,能够绑定多个事件,每一个事件能够指定多个回调函数,并且能够”去耦合“,有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。
随着ES6标准的发布,处理异步数据流的解决方案又有了新的变化。promise就是这其中的一个。咱们都知道,在传统的ajax请求中,当异步请求之间的数据存在依赖关系的时候,就可能产生很难看的多层回调,这样会使代码逻辑很容易形成混乱不便于阅读和后期维护,俗称”回调地狱”(callback hell)。另外一方面,每每错误处理的代码和正常的业务代码耦合在一块儿,形成代码会极其难看。为了让编程更美好,咱们就须要引入promise
来下降异步编程的复杂性。
因此某种程度上说,promise是对上面说到的回调函数处理异步编程的一个进阶方案。首先Promise是CommandJS提出的一种规范,其目的是为异步编程提供统一接口。
简单说,Promise的思想是,每个异步任务返回一个Promise对象,该对象有一个then方法,容许指定回调函数。形如这种形式:
f1().then(f2);
复制代码
对于函数f1,使用Jquery实现如下改写:
function f1(){var dfd = $.Deferred();setTimeout(function () {// f1的任务代码dfd.resolve();}, 500);return dfd.promise;}复制代码
这样写的优势在于,回调函数变成了链式写法,程序的流程能够看得很清楚,并且有一整套的配套方法,能够实现许多强大的功能。这也就是Promise处理异步编程的其中的一个方便之处。
再举一个制定多个回调函数的例子,其形式为:
f1().then(f2).then(f3);
复制代码
当指定发生错误时的回调函数,其形式为:
f1().then(f2).fail(f3);
复制代码
在此补充一点,promise中,若是一个任务已经完成,再添加回调函数,该回调函数会当即执行。因此,你不用担忧是否错过了某个事件或信号。这种方法的缺点就是编写和理解,都相对比较难。
展开谈论一下Promise:Promise实际上就是一个特殊的Javascript对象,反映了”异步操做的最终值”。”Promise”直译过来有预期的意思,所以,它也表明了某种承诺,即不管你异步操做成功与否,这个对象最终都会返回一个值给你。
代码示例
const promise = new Promise((resolve, reject) => {
$.ajax('https://github.com/users', (value) => {
resolve(value);
}).fail((err) => {
reject(err);
});
});
promise.then((value) => {
console.log(value);
},(err) => {
console.log(err);
});
//也能够采起下面这种写法
promise.then(value => console.log(value)).catch(err => console.log(err));
复制代码
上面的例子,会在Ajax请求成功后调用resolve
回调函数来处理结果,若是请求失败则调用reject
回调函数来处理错误。Promise对象内部包含三种状态,分别为pending,fulfilled和rejected。这三种状态能够类比于咱们日常在ajax数据请求过程的pending,success,error。一开始请求发出后,状态是Pending,表示正在等待处理完毕,这个状态是中间状态并且是单向不可逆的。成功得到值后状态就变为fulfilled,而后将成功获取到的值存储起来,后续能够经过调用then
方法传入的回调函数来进一步处理。而若是失败了的话,状态变为rejected,错误能够选择抛出(throw)或者调用reject
方法来处理。
Promise基本语法以下
Promise实例必须实现then这个方法
then()必须能够接收两个函数做为参数
then()返回的必须是一个Promise实例
eg
<script src="https://cdn.bootcss.com/bluebird/3.5.1/bluebird.min.js"></script>//若是低版本浏览器不支持Promise,经过cdn这种方式
<script type="text/javascript">
function loadImg(src) {
var promise = new Promise(function (resolve, reject) {
var img = document.createElement('img')
img.onload = function () {
resolve(img)
}
img.onerror = function () {
reject('图片加载失败')
}
img.src = src
})
return promise
}
var src = 'https://www.imooc.com/static/img/index/logo_new.png'
var result = loadImg(src)
result.then(function (img) {
console.log(1, img.width)
return img
}, function () {
console.log('error 1')
}).then(function (img) {
console.log(2, img.height)
})
</script>
做者:浪里行舟
连接:https://juejin.im/post/5b1962616fb9a01e7c2783a8
来源:掘金
著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。
复制代码
Promise还能够作更多的事情,好比,有若干个异步任务,须要先作任务1,若是成功后再作任务2,任何任务失败则再也不继续并执行错误处理函数。要串行执行这样的异步任务,不用Promise须要写一层一层的嵌套代码。
有了Promise,咱们只须要简单地写job1.then(job2).then(job3).catch(handleError);
其中job一、job2和job3都是Promise对象。
好比咱们想实现第一个图片加载完成后,再加载第二个图片,若是其中有一个执行失败,就执行错误函数:
var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
var result1 = loadImg(src1) //result1是Promise对象
var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
var result2 = loadImg(src2) //result2是Promise对象
result1.then(function (img1) {
console.log('第一个图片加载完成', img1.width)
return result2 // 链式操做
}).then(function (img2) {
console.log('第二个图片加载完成', img2.width)
}).catch(function (ex) {
console.log(ex)
})复制代码
Promise的经常使用方法
除了串行执行若干异步任务外,Promise还能够并行执行异步任务。试想一个页面聊天系统,咱们须要从两个不一样的URL分别得到用户的我的信息和好友列表,这两个任务是能够并行执行的,用Promise.all()实现以下:
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
// 同时执行p1和p2,并在它们都完成后执行then:
Promise.all([p1, p2]).then(function (results) {
console.log(results); // 得到一个Array: ['P1', 'P2']
});复制代码
有些时候,多个异步任务是为了容错。好比,同时向两个URL读取用户的我的信息,只须要得到先返回的结果便可。这种状况下,用Promise.race()实现:
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
console.log(result); // 'P1'
});复制代码
因为p1执行较快,Promise的then()将得到结果'P1'。p2仍在继续执行,但执行结果将被丢弃。
总结:Promise.all接受一个promise对象的数组,待所有完成以后,统一执行success;
Promise.race接受一个包含多个promise对象的数组,只要有一个完成,就执行success。
对上面的例子作下修改,加深对这二者的理解:
var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
var result1 = loadImg(src1)
var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
var result2 = loadImg(src2)
Promise.all([result1, result2]).then(function (datas) {
console.log('all', datas[0])//<img src="https://www.imooc.com/static/img/index/logo_new.png">
console.log('all', datas[1])//<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg">
})
Promise.race([result1, result2]).then(function (data) {
console.log('race', data)//<img src="https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg">
})复制代码
若是咱们组合使用Promise,就能够把不少异步任务以并行和串行的方式组合起来执行。
Promise.reject(reason): 返回一个新的promise
对象,用reason值直接将状态变为rejected
。
const promise2 = new Promise((resolve, reject) => {
reject('Failed');
});
const promise2 = Promise.reject('Failed');
复制代码
上面两种写法是等价的。
Promise.resolve(value): 返回一个新的promise对象,这个promise对象是被resolved的。与reject相似,下面这两种写法也是等价的。
const promise2 = new Promise((resolve, reject) => {
resolve('Success');
});
const promise2 = Promise.resolve('Success');
复制代码
then 利用这个方法访问值或者错误缘由。其回调函数就是用来处理异步处理返回值的。
catch 利用这个方法捕获错误,并处理。
简介
语法
用promise示例和asyn/await示例两段代码演示:
promise
const makeRequest = () =>
getJSON()
.then(data => {
console.log(data)
return "done"
})
makeRequest()
复制代码
async/await
const makeRequest = async () => {
console.log(await getJSON())
return "done"
}
makeRequest()
复制代码
它们有一些细微不一样:
函数前面多了一个aync关键字。await关键字只能用在aync定义的函数内。async函数会隐式地返回一个promise,该promise的reosolve值就是函数return的值。(示例中reosolve值就是字符串”done”)
第1点暗示咱们不能在最外层代码中使用await,由于不在async函数内。
// 不能在最外层代码中使用await
await makeRequest()
// 这是会出事情的
makeRequest().then((result) => {
// 代码
})
复制代码
await getJSON()表示console.log会等到getJSON的promise成功reosolve以后再执行。
相对于promise,async/await的优点有哪些
1.简洁
由示例可知,使用Async/Await明显节约了很多代码。咱们不须要写.then,不须要写匿名函数处理Promise的resolve值,也不须要定义多余的data变量,还避免了嵌套代码。这些小的优势会迅速累计起来,这在以后的代码示例中会更加明显。
2.错误处理
Async/Await让try/catch能够同时处理同步和异步错误。在下面的promise示例中,try/catch不能处理JSON.parse的错误,由于它在Promise中。咱们须要使用.catch,这样错误处理代码很是冗余。而且,在咱们的实际生产代码会更加复杂。
const makeRequest = () => {
try {
getJSON()
.then(result => {
// JSON.parse可能会出错
const data = JSON.parse(result)
console.log(data)
})
// 取消注释,处理异步代码的错误
// .catch((err) => {
// console.log(err)
// })
} catch (err) {
console.log(err)
}
}
复制代码
使用aync/await的话,catch能处理JSON.parse错误
const makeRequest = async () => {
try {
// this parse may fail
const data = JSON.parse(await getJSON())
console.log(data)
} catch (err) {
console.log(err)
}
}复制代码
下面示例中,须要获取数据,而后根据返回数据决定是直接返回,仍是继续获取更多的数据。
const makeRequest = () => {
return getJSON()
.then(data => {
if (data.needsAnotherRequest) {
return makeAnotherRequest(data)
.then(moreData => {
console.log(moreData)
return moreData
})
} else {
console.log(data)
return data
}
})
}
复制代码 |
这些代码看着就头痛。嵌套(6层),括号,return语句很容易让人感到迷茫,而它们只是须要将最终结果传递到最外层的Promise。
上面的代码使用async/await编写能够大大地提升可读性:
const makeRequest = async () => {
const data = await getJSON()
if (data.needsAnotherRequest) {
const moreData = await makeAnotherRequest(data);
console.log(moreData)
return moreData
} else {
console.log(data)
return data
}
}
复制代码 |
你极可能遇到过这样的场景,调用promise1,使用promise1返回的结果去调用promise2,而后使用二者的结果去调用promise3。你的代码极可能是这样的:
const makeRequest = () => {
return promise1()
.then(value1 => {
return promise2(value1)
.then(value2 => {
return promise3(value1, value2)
})
})
}
复制代码 |
若是promise3不须要value1,能够很简单地将promise嵌套铺平。若是你忍受不了嵌套,你能够将value 1 & 2 放进Promise.all来避免深层嵌套:
const makeRequest = () => {
return promise1()
.then(value1 => {
return Promise.all([value1, promise2(value1)])
})
.then(([value1, value2]) => {
return promise3(value1, value2)
})
}
复制代码 |
这种方法为了可读性牺牲了语义。除了避免嵌套,并无其余理由将value1和value2放在一个数组中。
使用async/await的话,代码会变得异常简单和直观。
const makeRequest = async () => {
const value1 = await promise1()
const value2 = await promise2(value1)
return promise3(value1, value2)
}
复制代码 |
下面示例中调用了多个Promise,假设Promise链中某个地方抛出了一个错误:
const makeRequest = () => {
return callAPromise()
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => {
throw new Error("oops");
})
}
makeRequest()
.catch(err => {
console.log(err);
// output
// Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
})
复制代码 |
Promise链中返回的错误栈没有给出错误发生位置的线索。更糟糕的是,它会误导咱们;错误栈中惟一的函数名为callAPromise,然而它和错误没有关系。(文件名和行号仍是有用的)。
然而,async/await中的错误栈会指向错误所在的函数:
const makeRequest = async () => {
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
throw new Error("oops");
}
makeRequest()
.catch(err => {
console.log(err);
// output
// Error: oops at makeRequest (index.js:7:9)
})
复制代码 |
在开发环境中,这一点优点并不大。可是,当你分析生产环境的错误日志时,它将很是有用。这时,知道错误发生在makeRequest比知道错误发生在then链中要好。
最后一点,也是很是重要的一点在于,async/await可以使得代码调试更简单。2个理由使得调试Promise变得很是痛苦:
const markRequest = () => {
return callAPromise ()
.then (() => callAPromise())
.then (() => callAPromise())
.then (() => callAPromise())
.then (() => callAPromise())
}
复制代码
const markRequest = async () => {
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
}复制代码
对于经常使用的不一样异步编程处理方案,我的观点是针对不一样的业务场景可根据状况选择合适高效的方案,各有优点劣势,不必顶一个踩一个,虽然技术不断发展优化,但有些技术不至于淘汰如此之快,存在即合理。