前言
你可能知道,Javascript语言的执行环境是"单线程"(single thread)。
所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。
单线程的好处是实现起来比较简单,执行环境相对单纯;
坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。
常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
为了解决这个问题,Javascript语言将任务的执行模式分成两种:
- 同步(Synchronous)
- 异步(Asynchronous)
"同步模式"就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;
"异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。
在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。
在服务器端,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。
以下是异步编程的六种方式:
1、回调函数的方式
这是异步编程最基本的方法。
所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,再调用这个函数
// 异步请求
ajax(url, () => {
// 请求返回后的处理逻辑
})
// 读取文件
fs.readFile('/etc/shells', function (err, data) {
// 文件读取完毕后的操作
console.log(data);
});
回调函数有一个致命的弱点,就是容易写出回调地狱
ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})
- 优点 是简单、容易理解和部署
- 缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱。
2、Promise对象
Promise就是为了解决回调地狱而产生的,将回调函数的嵌套,改成链式调用。
简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。
例子如下:
function A(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return A(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return A(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return A(n);
}
step1(100).then( time2 =>step2(time2))
.then( time3 => step3(time3))
.then( result => {
console.log(`result is ${result}`)
});
优点:回调函数变成了链式写法,每个异步函数执行完成后,才会去执行下一个then函数;
缺点:Promise的写法只是回调函数的改进,用then()方法免去了嵌套,更为直观。但这样写也存在了很明显的问题,代码变得冗杂了,语义化并不强
。
3、生成器函数 Generator/ yield
Generator 函数是 ES6 提供的一种异步编程解决方案:
yield表达式可以暂停函数执行,next方法用于恢复函数执行,这使得Generator函数非常适合将异步任务同步化
。- yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
- 每个yield返回的是
{value:yield返回的值,done:true/false(执行状态)}
Generator的原理
generator 的方式,它可以在函数的执行过程中,将函数的执行权
转移出去,在函数外部还可以将执行权转移回来。
执行过程中,当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。
使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
// 异步函数
function ajax(duration) {
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log(duration+'ms延时打印输出!,当前位于'+ new Date().getSeconds() +'秒')
resolve(duration);
},duration)
})
}
// 生成器函数
function* main() {
const user = yield ajax(2000);
console.log('user',user);
const posts = yield ajax(10000);
console.log(posts);
}
// 封装执行生成器函数
function co(generator) {
const g = generator();
function handleResult(result) {
if(result.done) return; //生成器函数结束
result.value.then(data => { //这里与yield的返回有关
console.log('继续执行!',data);
handleResult(g.next(data));
},error => {
g.throw(error);
}
);
}
handleResult(g.next());
}
co(main);
输出结果如下:
4、async/await 函数的实现
为了解决 Promise 的问题,async、await 在 ES7 中被提了出来,是目前为止最好的解决方案。
- async 是“异步”的意思,而 await 是等待的意思。所以应该很好理解 async 用于申明一个 异步的function(实际上是async function 对象)
- await 用于等待一个异步任务执行完成的的结果。并且 await 只能出现在 async 函数中。
async/await 异步编程的解决原理
async 函数是通过 generator 和 promise 实现的一个自动执行的语法糖
,当函数内部执行到一个await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写。
async function test(){
let newTime = await new Promise((resolve,reject)=>{ //这里等待异步返回结果,再继续向下执行
let time = 3000;
setTimeout(()=>{
resolve(time);
},time)
})
console.log(newTime+'毫秒后执行');
let content = 'test';
console.log(content); //3s后,先输出 “3000毫秒后执行”,再输出 "test"
}
test()
async函数
——返回的是一个 Promise 对象
async function testAsync() {
return "hello async";
}
const result = testAsync();
console.log(result); // async 函数返回的是一个Promise对象
result.then(v=>{
console.log(v);
})
console.log(123) // 先输出promise对象,其次123,最后hello async
5、事件监听
这种方式下,异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
下面是两个函数f1和f2,编程的意图是f2必须等到f1执行完成,才能执行。
首先,为f1绑定一个事件(jQuery写法)
f1.on('done', f2);
上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:
function f1() {
setTimeout(function () {
// ...
f1.trigger('done');
}, 1000);
}
上面代码中,f1.trigger(‘done’)表示,执行完成后,立即触发done事件,从而开始执行f2
优点:是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合",有利于实现模块化。
缺点:是整个程序都要变成事件驱动型,运行流程会变得很不清晰。
6、发布/订阅
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)
下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件
首先,f2向"信号中心"jQuery订阅"done"信号
jQuery.subscribe("done", f2);
f1进行如下改写:
function f1(){
setTimeout(function () {
// f1的任务代码
jQuery.publish("done");
}, 1000);
}
jQuery.publish(“done”)的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。
此外,f2完成执行后,也可以取消订阅(unsubscribe)
jQuery.unsubscribe("done", f2);
更多参考链接:https://blog.csdn.net/yiyueqinghui/article/details/111057634
参考链接:https://www.jianshu.com/p/d7f6077a0dd2