异步编程方案
javascript语言的执行环境是"单线程"(single thread),就是指一次只能完成一件任务。如果有多个任务,就必须排队,等前面一个任务完成,再执行后面一个任务,以此类推。
这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行
同步
-
指在 主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务。
-
也就是调用一旦开始,必须这个调用 返回结果 才能继续往后执行。程序的执行顺序和任务排列顺序是一致的。
异步
-
异步任务是指不进入主线程,而进入 任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。
-
每一个任务有一个或多个 回调函数。前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行。
-
程序的执行顺序和任务的排列顺序是不一致的,异步的。
-
我们常用的setTimeout和setInterval函数,Ajax都是异步操作。
如何实现异步编程
1、回调函数(Callback)
回调函数是异步操作最基本的方法。以下代码就是一个回调函数的例子:
ajax(url, () => {
// 处理逻辑
})
- 弱点:就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码:
ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})
- 优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。
2. 事件监听
在这种方式下:异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
-
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合",有利于实现模块化。
-
缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
3. 发布订阅
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做 "发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。
// <!--首先,f2向信号中心jQuery订阅done信号。-->
jQuery.subscribe('done', f2);
// <!--然后,f1进行如下改写:-->
function f1() {
setTimeout(function () {
// ...
jQuery.publish('done');
}, 1000);
}
// <!--上面代码中,jQuery.publish('done')的意思是,f1执行完成后,向信号中心jQuery发布done信号,从而引发f2的执行。 f2完成执行后,可以取消订阅(unsubscribe)-->
jQuery.unsubscribe('done', f2);
4. Promise/A+
MDN对Promise定义:Promise对象用于表示一个异步操作的最终状态(完成或失败),以及其返回的值。
Promise本意为承诺,我们可以理解为程序承诺过一段时间后会给你一个结果。 Promise是一个对象,可以保存三个状态 每一时刻必须有一个状态。
1. Promise的三种状态
-
Pending(处理中) ---- Promise对象实例创建时候的初始状态
-
Fulfilled(完成) ---- 成功的操作,为表述方便,fulfilled 使用 resolved 代替
-
Rejected(失败) ---- 可以理解为失败的状态
-
pending可以转化为fulfilled或rejected并且只能转化一次,也就是说如果pending转化到fulfilled状态,那么就不能再转化到rejected。并且fulfilled和rejected状态只能由pending转化而来,两者之间不能互相转换。
2. promise的链式调用
-
每次调用返回的都是一个新的Promise实例(这就是then可用链式调用的原因)
-
then — 运行 promise 函数完成时传递给它的回调
-
catch — 运行 promise 函数错误时传递给它的回调
-
-
如果then中返回的是一个结果的话会把这个结果传递下一次then中的成功回调
-
如果then中出现异常,会走下一个then的失败回调
-
在 then中使用了return,那么 return 的值会被Promise.resolve() 包装(见例1)
-
then中可以不传递参数,如果不传递会透到下一个then中(见例2)
-
catch 会捕获到没有捕获的异常
example:
// 例1
Promise.resolve(1)
.then(res => {
console.log(res)
return 2 //包装成 Promise.resolve(2)
})
.catch(err => 3)
.then(res => console.log(res))
// result:
1
2
Promise:{<resolved>: undefined}
// 例2
let fs = require('fs')
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if (err) reject(err)
resolve(data)
})
})
}
read('./name.txt')
.then(function(data) {
throw new Error() //then中出现异常,会走下一个then的失败回调
}) //由于下一个then没有失败回调,就会继续往下找,如果都没有,就会被catch捕获到
.then(function(data) {
console.log('data')
})
.then()
.then(null, function(err) {
console.log('then', err)// then error
})
.catch(function(err) {
console.log('error')
})
5. 生成器Generators/ yield
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行。
-
语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
-
Generator 函数除了状态机,还是一个遍历器对象生成函数。
-
可暂停函数, yield可暂停,next方法可启动,每次返回的是yield后的表达式结果。
-
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
example:
function *foo(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
return (x + y + z)
}
let it = foo(5)
console.log(it.next()) // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}
// 首先 Generator 函数调用和普通函数不同,它会返回一个迭代器
// 当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
// 当执行第二次 next 时,传入的参数12就会被当作上一个yield表达式的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8
// 当执行第三次 next 时,传入的参数13就会被当作上一个yield表达式的返回值,所以 z = 13, x = 5, y = 24,相加等于 42
从上例中我们看出手动迭代Generator 函数很麻烦,实现逻辑有点绕,而实际开发一般会配合 co 库去使用。co是一个为Node.js和浏览器打造的基于生成器的流程控制工具,借助于Promise,你可以使用更加优雅的方式编写非阻塞代码。
// npm install co
function* r() {
let r1 = yield read('./1.txt')
let r2 = yield read(r1)
let r3 = yield read(r2)
console.log(r1)
console.log(r2)
console.log(r3)
}
let co = require('co')
co(r()).then(function(data) {
console.log(data)
})
// 2.txt=>3.txt=>结束=>undefined
6. async/await
Async/Await简介:
-
async/await是基于Promise实现的,它不能用于普通的回调函数。
-
async/await与Promise一样,是非阻塞的。
-
async/await使得异步代码看起来像同步代码,这正是它的魔力所在。
Async 函数是通过在函数声明之前加上单词 async 来创建的: 一个函数如果加上 async ,那么该函数就会返回一个 Promise
- async必须在函数声明前
async function async1() {
return "1"
}
console.log(async1()) // -> Promise {<resolved>: "1"}
Async/Await并发请求: Async 函数可以使用 await 暂停,该关键字只能在 Async 函数中使用。 Await 返回 async 函数完成时返回的任何内容。
- await 接一个 promise,那么后面的代码就会等待,等promise resolve了才会执行。
let fs = require('fs')
function read(file) {
return new Promise(function(resolve, reject) {
fs.readFile(file, 'utf8', function(err, data) {
if (err) reject(err)
resolve(data)
})
})
}
function readAll() {
read1()
read2()//这个函数同步执行
}
async function read1() {
let r = await read('1.txt','utf8')
console.log(r)
}
async function read2() {
let r = await read('2.txt','utf8')
console.log(r)
}
readAll() // 1.txt 2.txt
这是 promise 和 Async/Await 之间的区别
// Async / Await
const asyncGreeting = async () => 'Greetings';
// Promise
const promiseGreeting = () => new Promise(((resolve) => {
resolve('Greetings');
}));
asyncGreeting().then(result => console.log(result));
promiseGreeting().then(result => console.log(result));
本文由博客群发一文多发等运营工具平台 OpenWrite 发布