异步回调嵌套
JavaScript中存在异步操作,对其有依赖的动作需在回调函数中处理。如下示例,要执行待加载脚本中的函数,需要等待脚本加载完成后在回调函数中进行。如果多个异步操作逐一依赖,会形成层层嵌套的回调函数,即“回调地狱”。
function loadScript(src, callback) { let script = document.createElement('script') script.src = src script.onload = () => callback(null, script) script.onerror = () => callback(new Error(`Script load error: ${src}`)) document.head.append(script)}
loadScript('http://no-such-url', (error, script) => { if (error) { // 处理错误 } else { // 成功加载脚本 }})
Promise对象
ES6原生提供Promise对象,将异步操作以同步操作的流程表达出来,避免回调函数的层层嵌套。Promise对象代表一个异步操作,有三种状态:初始pending、成功fulfilled或失败rejected。只有异步操作的结果,可以决定初始状态变为成功或失败最终态。传给new Promise的控制函数会立即被自动调用,此函数接受两个参数resolve和reject,分别会在Promise对象状态变为成功或失败时被执行。
function loadScript(src) { return new Promise((resolve, reject) => { let script = document.createElement('script') script.src = src script.onload = () => resolve(script) script.onerror = () => reject(new Error(`Script load error: ${src}`)) document.head.append(script) })}
let promise = loadScript('https://cdn.bootcss.com/lodash.js/4.17.11/lodash.min.js')promise.then( script => console.log(`${script.src} is loaded`), error => console.log(`Error: ${error.message}`))promise.then(script => console.log('One more handler'))
Promise处理链
promise.then会返回一个promise,可以用它调用下一个then。当then中的控制函数返回一个值时,它会变成当前promise的result,下一个处理程序会使用这个返回值运行。
new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000)}).then(function(result) { console.log(result) // 1 return result * 2}).then(function(result) { console.log(result) // 2 return result * 2}).then(function(result) { console.log(result) // 4 return result * 2})
如果then中的控制函数返回的值是一个promise,那么直到它结束之前,下一步执行会一直被暂停。在结束之后,该promise的结果会传递给下一个then中的处理程序。
fetch('https://api.github.com/users/YopheeHsin') .then(response => response.json()) .then(githubUser => new Promise((resolve, reject) => { let img = document.createElement('img') img.src = githubUser.avatar_url document.body.append(img)
setTimeout(() => { img.remove() resolve(githubUser) }, 3000) })) // 3秒后执行 .then(githubUser => console.log(`Finished showing ${githubUser.name}`))
把代码分割成几个可复用的函数。使用catch来处理Promise链中的所有错误,如果上面任一promise被reject,那么catch中的错误处理程序便会执行。除了明确的rejected失败状态,catch也适用于throw new Error和包括代码错误在内的任何错误。
function loadJson(url) { return fetch(url).then(response => response.json())}
function showAvatar(githubUser) { return new Promise((resolve, reject) => { let img = document.createElement('img') img.src = githubUser.avatar_url document.body.append(img)
setTimeout(() => { img.remove() resolve(githubUser) }, 3000) })}
loadJson('https://api.github.com/users/YopheeHsin') .then(showAvatar) .then(githubUser => console.log(`Finished showing ${githubUser.name}`)) .catch(err => console.log(err))
Promise静态方法
Promise类中有4个静态方法。
Promise.resolve根据给定的value值返回resolved promise。
let promise = Promise.resolve(value)// 等价于let promise = new Promise(resolve => resolve(value))
用来将已有的值“封装”进promise,确保接口统一性。
function loadCached(url) { let cache = loadCached.cache || (loadCached.cache = new Map()) if (cache.has(url)) return Promise.resolve(cache.get(url)) return fetch(url) .then(response => response.text()) .then(text => { cache.set(url, text) return text })}
Promise.reject创建带有error的rejected promise。
let promise = Promise.reject(error)// 等价于let promise = new Promise((resolve, reject) => reject(error))
Promise.all并行运行多个promise,并等待所有promise准备就绪。
let names = ['remy', 'jeresig', 'iliakan']let requests = names.map(name => fetch(`https://api.github.com/users/${name}`))Promise.all(requests) .then(responses => { responses.forEach(response => console.log(`${response.url}: ${response.status}`)) return responses }) .then(responses => Promise.all(responses.map(r => r.json()))) .then(users => console.log(users.map(user => user.name)))
Promise.all使用下面这样的容错机制。catch会对异常promise产生error并返回,根据promise工作原理,只要then/catch处理器返回了值,执行流程就会“正常”进行。
let urls = [ 'https://api.github.com/users/YopheeHsin', '/', 'http://no-such-url']
Promise.all(urls.map(url => fetch(url).catch(err => err))) .then(responses => Promise.all( responses.map(r => r instanceof Error ? r : r.json().catch(err => err)) )) .then(results => { console.log(results[0].name) console.log(results[1]) console.log(results[2]) })
Promise.race与Promise.all类似,但不会等待所有promise都完成,只等待第一个promise完成后即继续执行。
async/await
ES8标准引入的async函数使得异步操作变得更加方便。
将async关键字放在函数前,意味着该函数总是会返回promise。如果代码return非promise值,会自动将其封装到resolved promise中。
async function f() { return 1}// 等价于async function f() { return Promise.resolve(1)}
f().then(r => console.log(r))
await只在async函数中工作,作用是等待promise,直到其解决并返回其结果。这样的语法比promise.then更优雅地获取结果,也更容易阅读和编写。
async function showAvatar() { let response = await fetch('https://api.github.com/users/YopheeHsin') let githubUser = await response.json() let img = document.createElement('img') img.src = githubUser.avatar_url document.body.append(img) await new Promise(resolve => setTimeout(resolve, 3000)) img.remove() return githubUser}
showAvatar() .then(githubUser => console.log(`Finished showing ${githubUser.name}`))
当await的promise失败reject时,它会抛出error,就像该行上有常规throw语句一样,可以使用try…catch来捕获这个error。
async function f() { await Promise.reject(new Error('Whoops!'))}// 等价于async function f() { throw new Error('Whoops!')}
// 出错时,控制权会进入catch块,可以多行封装async function f() { try { let response = await fetch('/no-user-here') let user = await response.json() } catch (err) { console.log(err) }}
// 也可以通过追加catch来处理async function f() { let response = await fetch('http://no-such-url')}f().catch(err => console.log(err))
参考:
http://es6.ruanyifeng.com/#docs/promise
http://es6.ruanyifeng.com/#docs/async
https://javascript.info/async