异步是 JavaScript
的一大特点, 使用 JavaScript
就会不可避免的需要处理异步, 监听用户的操作事件, 处理 Ajax
请求, 定时器等等都是异步场景。 所以很有必要好好地了解以及学习如何处理异步。
回调函数
ES6 之前, 只能通过回调函数的方式处理异步, 这是最原始的处理方式, 几乎所有原生 API 都是通过这种方式处理异步的, 如 setTimeout()
, addEventListener()
等:
1 2 3
| setTimeout(fucntion () { console.log('1s later') }, 1000)
|
上面的示例代码就是典型的回调函数方式, 当回调函数中又嵌套回调函数, 并且嵌套多层时, 就出现了回调地狱(callback hell):
1 2 3 4 5 6 7 8 9 10 11 12
| setTimeout(function () { console.log('1s later') setTimeout(function () { console.log('2s later') setTimeout(function () { console.log('3s later') setTimeout(function () { console.log('4s later') }, 1000) }, 1000) }, 1000) }, 1000)
|
回调地狱会导致代码可读性非常差, 也会造成难以维护的问题。 上面的代码可读性很差, 语义不明确。
Promise
Promise
是 ES6 中添加的新特性, 专门用于处理异有效, 具体的语法可以参考 [ECMAScript 6 但是只适用于回调函数时最后一个参数, 并且回调函数的参数只有(http://es6.ruanyifeng.com/#docs/promise), 通过 Promise
可以将上面的代码优化成这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| new Promise(function (resolve, reject) { setTimeout(function () { console.log('1s later') resolve() }, 1000) }).then(function () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('2s later') resolve() }, 1000) }) }).then(function () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('3s later') resolve() }, 1000) }) }).then(function () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('4s later') resolve() }, 1000) }) })
|
这么写整体而言比回调方式好的多了, 整个流程比较清晰, 但是缺点就是太啰嗦, 可以通过 Promise Chain
的方式进行优化。
Promise
Chain
Promise
Chain 可以将 Promise 按照顺序形成链式调用, 并且只有在前一个 Promise
的状态变为 fulfilled 之后, 后一个 Promise
才会执行。
Promise
Chain 的实现一般有两种方式:
- 通过数组实现
Promise
Chain - 通过递归实现
Promise
Chain
数组实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class PromiseChain { constructor () { this.promise = Promise.resolve() this.chain = [] }
use (node) { this.chain.push(node) return this }
exec (...params) { this.chain.forEach(node => { this.promise = this.promise.then(() => { return node() }) }) } }
|
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| const promiseChain = new PromiseChain()
promiseChain .use(after1Sec) .use(after2Sec) .use(after3Sec) .use(after4Sec) .exec()
function after1Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('1s later') resolve() }, 1000) }) }
function after2Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('2s later') resolve() }, 1000) }) }
function after3Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('3s later') resolve() }, 1000) }) }
function after4Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('4s later') resolve() }, 1000) }) }
|
递归实现
1 2 3 4 5 6 7 8 9
| function asyncQueue (array) { return (function exec (i) { if (i === array.length) return Promise.resolve()
return array[i]().then(function () { return exec(i + 1) }) })(0) }
|
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| const arrayP = [after1Sec, after2Sec, after3Sec, after4Sec]
asyncQueue(arrayP)
function after1Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('1s later') resolve() }, 1000) }) }
function after2Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('2s later') resolve() }, 1000) }) }
function after3Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('3s later') resolve() }, 1000) }) }
function after4Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('4s later') resolve() }, 1000) }) }
|
通过 Promise
实现 Observer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function waitForClick (element) { let res = null const handler = () => res && res()
element.addEventListener('click', handler)
return new Promise(function (resolve, reject) { res = resolve }).then(() => { element.removeElementListener('click', handler) }) }
waitForClick(btn).then(() => { console.log('after click') })
Promise.all([waitForClick(btn1), waitForClick(btn2), waitForClick(btn3)])
|
上面示例中的 waitForClick()
就有点类似于 Observer, 并且将 click 事件的监听与事件处理的逻辑解耦了, 整体可读性更好。
Promisify
既然所有的回调都可以用 Promise
重写, 那么就可以写一个公共的 promisify()
函数, 将回调风格变为 Promise
风格:
1 2 3 4 5 6 7 8 9 10
| function promisify (cb, context) { return function () { const args = arguments return new Promise(function (resolve, reject) { cb.call(context, ...args, function (err, value) { !!err ? reject(err) : resolve(value) }) }) } }
|
接着测试一下上面的 promisify()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function testCb (err, value, cb) { setTimeout(function () { cb(err, value) }, 1000) }
testCb(1, 2, function (err, value) { console.log(err, value) })
const testP = promisify(testCb) testP(1, 2).then(res => { console.log(res) }).catch(err => { console.log(err) })
|
上面的 promisify()
函数相对而言比较通用, 但是只适用于回调函数是最后一个参数, 并且回调函数的参数满足以下条件时才有效:
- 第一个值是
Error
对象, 表示是否出错, 未出错时值为 null
; - 第二个值是正确的值
这是 NodeJS
中回调函数的参数的风格, 即 Error First, 所以该方法在 NodeJS
中会非常通用。
但是在浏览器端就不一定了。 比如 setTimeout()
, setInterval()
的回调函数是第一个参数, 但是对 addEventListener()
等又是最后一个参数。 所以我们在自己设计回调接口时, 都应该按照 Error First 的风格进行设计, 以使得整体风格的一致。
Generator
Generator
同样是在 ES6 新增的新特性, 是异步编程的一种解决方案, 具体语法参考 ECMAScript 6 入门, 通过 Generator
可以将之前回调的写法优化成这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| function after1Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('1s later') resolve() }, 1000) }) }
function after2Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('2s later') resolve() }, 1000) }) }
function after3Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('3s later') resolve() }, 1000) }) }
function after4Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('4s later') resolve() }, 1000) }) }
function * cb2Generator () { yield after1Sec yield after2Sec yield after3Sec yield after4Sec }
const g = cb2Generator() g.next().value() .then(() => { return g.next().value() }).then(() => { return g.next().value() }).then(() => { return g.next().value() })
|
上面的示例代码中, 很明显的可以看出使用 Generator
需要手动调用以启动执行, 这就显得比较麻烦。
Generator
+ Promise
实现任务执行器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| function run (gen) { const g = gen() let result = g.next()
;(function step () { if (!result.done) { let promise = Promise.resolve(result.value) promise.then(value => { result = g.next(value) step() }).catch(err => { result = g.throw(err) step() }) } })() }
run(function * () { yield after1Sec() yield after2Sec() yield after3Sec() yield after4Sec() })
function after1Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('1s later') resolve() }, 1000) }) }
function after2Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('2s later') resolve() }, 1000) }) }
function after3Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('3s later') resolve() }, 1000) }) }
function after4Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('4s later') resolve() }, 1000) }) }
|
通过任务执行器 run
, 不再需要手动调用 Generator
的启动器了。
async
+ await
ES8 标准引入了 async
函数, 使得异步操作更加方便了。 async
函数是 Generator
函数的语法糖, 具体语法参考 ECMAScript 6 入门。
相较于 Promise
和 Generator
, async
函数具有更好的语义并且使用起来也更简单。 通过 async
函数可以将上述代码进行优化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| function after1Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('1s later') resolve() }, 1000) }) }
function after2Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('2s later') resolve() }, 1000) }) }
function after3Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('3s later') resolve() }, 1000) }) }
function after4Sec () { return new Promise(function (resolve, reject) { setTimeout(function () { console.log('4s later') resolve() }, 1000) }) }
async function exec () { await after1Sec() await after2Sec() await after3Sec() await after4Sec() }
exec()
|
参考