JavaScript 是一门运行在浏览器的脚本语言,作用是实现页面的动态交互,操作 DOM决定了它是一门单线程模式的语言,否则会出现多线程的线程同步问题。
同步模式
同步模式会先在调用栈压入一个匿名函数,然后把同步代码放入这个匿名函数里,按顺序把函数的调用或者语句压入调用栈,函数和变量的声明不会压入。当执行完毕就把函数的调用或者语句取出调用栈。
缺点是如果里面有一段比较耗时的代码,会在那里发生阻塞,界面加载可能发生卡顿。
异步模式
异步模式不会等待一个任务结束再去执行另一个任务,开启了一个任务后又立即去开启另一个任务。任务完成后的后续操作一般是执行一个回调函数。异步模式使得单线程的 JavaScript 可以同时执行多个耗时长的任务,但同时也使得代码的执行顺序相比同步模式较为混乱。
对于异步代码,比如 setTimeout,运行环境会为它开启一个计时器,放入 Web APIs 中,然后先继续往下执行。Event Loop(事件循环) 有两个作用:一是监听 Web APIs, 当计时器时间到,就把任务放入队列等待执行;二是监听队列,如果调用栈里没有任务,就从队列拿出任务放入调用栈中执行。
Promise
Promise 是 ES2015 新增的一个全局类,它定义异步任务最终执行成功或者失败。
实例化 Promise- 对象需要传入一个函数作为参数,该函数会在实例化时就执行,它有两个回调函数作为参数,resolve 和 reject ,resolve 和 reject 都会返回传入的参数,resolve 是成功的回调,reject 是失败的回调,由于 Promise 的执行结果最终只能是成功或失败, 所以这两个回调也只能在最后使用一个。如果不调用 resolve 和 reject,Promise 会处于挂起(pending)状态,由运行环境来判断是否已经执行完毕了。
then 方法中回调函数会进入到队列中排队,等待调用栈的同步代码执行完毕再执行。then 方法会返回一个全新的 Promise 对象,实现 Promise 的链式调用。后面的 then 方法就是为上一个 then 返回的 Promise 注册回调。 前面 then 方法回调函数的返回值会作为传给后面 then 方法回调的参数。
var p1 = new Promise((resolve, reject) => {
console.log('p1')
resolve(true)
// reject('error')
})
p1.then(value => {
console.log(value) // 如果调用 resolve 则走到这里
}, err => {
console.log(err) // 如果调用 reject 则走到这里
})
console.log('end');
// 输出 p1, end, true/error
异常处理
Promise 链上,任何一个异常都会被传递下去,直到被捕获。
使用 catch 捕获异常,浏览器和 Node.js 环境下也可以使用全局监听器捕获。
ajax('/api/users.json')
.then(function onFulfilled (value) {
console.log('onFulfilled', value)
return ajax('/error-url')
}) // => Promise {}
.catch(function onRejected (error) {
console.log('onRejected', error)
})
// 全局捕获 Promise 异常,类似于 window.onerror
window.addEventListener('unhandledrejection', event => {
const { reason, promise } = event
console.log(reason, promise)
// reason => Promise 失败原因,一般是一个错误对象
// promise => 出现异常的 Promise 对象
event.preventDefault()
}, false)
// Node.js 中使用以下方式
process.on('unhandledRejection', (reason, promise) => {
console.log(reason, promise)
// reason => Promise 失败原因,一般是一个错误对象
// promise => 出现异常的 Promise 对象
})
Promise 常用静态方法
resolve 和 reject
resolve 和 reject 能快速返回一个 Promise 对象。
reject 返回一个失败的 Promise 对象。
如果给 resolve 传入一个值,那么它返回一个成功的对象,如果传入一个 Promise 对象,那么它直接返回这个对象,如果传入的是带有一个跟 Promise 一样的 then 方法的对象,Promise.resolve 会将这个对象作为 Promise 执行
// 常用 Promise 静态方法
function ajax (url) {
return new Promise(function (resolve, reject) {
// foo()
// throw new Error()
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'json'
xhr.onload = function () {
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
Promise.resolve('foo')
.then(function (value) {
console.log(value) // foo
})
new Promise(function (resolve, reject) {
resolve('foo')
})
// 如果传入的是一个 Promise 对象,Promise.resolve 方法原样返回
var promise = ajax('/api/users.json')
var promise2 = Promise.resolve(promise)
console.log(promise === promise2) // true
// 如果传入的是带有一个跟 Promise 一样的 then 方法的对象,Promise.resolve 会将这个对象作为 Promise 执行
Promise.resolve({
then: function (onFulfilled, onRejected) {
onFulfilled('foo')
}
})
.then(function (value) {
console.log(value)
})
// Promise.reject 传入任何值,都会作为这个 Promise 失败的理由
Promise.reject(new Error('rejected'))
.catch(function (error) {
console.log(error)
})
Promise.reject('anything')
.catch(function (error) {
console.log(error)
})
并行执行
Promise.all()
如果说同时执行多个 Promise,它们之间没有依赖,或者说需要它们的所有执行结果才能往下执行其它代码,那么可以用 Promise.all() 方法,把多个 Promise 放入一个数组里作为参数传入。Promise.all() 也是返回一个新的 Promise 对象,并且回调函数能接收到 Promise 数组执行的结果数组。
如果有一个失败的,会立即抛出错误,并且 reject 的是第一个抛出的错误信息。
Promise.race()
Promise.race() 和 Promise.all() 一样可以并行执行多个 Promise,区别是一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。
执行时序
一般把回调队列的任务称为宏任务,宏任务执行过程中有可能加上一些额外的需求,这种需求将作为宏任务进入队列排队执行,或者作为微任务立即执行。Promise 的回调是微任务,不用重新到队列排队。其它异步调用是微任务的还有一个 MutaionObserver 和 Node.js 下的 process.nextTick
// 微任务
console.log('global start')
// setTimeout 的回调是 宏任务,进入回调队列排队
setTimeout(() => {
console.log('setTimeout')
}, 0)
// Promise 的回调是 微任务,本轮调用末尾直接执行
Promise.resolve()
.then(() => {
console.log('promise')
})
.then(() => {
console.log('promise 2')
})
.then(() => {
console.log('promise 3')
})
console.log('global end') // 输出顺序global start, global end, promise, promise 2, promise 3, setTimeout
缺点
Promise 的缺点是仍然可能出现多个回调,虽然避免了嵌套地狱,但是代码可读性仍然不能和同步代码相比较。
Generator
Generator 是 ES2015 提供的生成器函数。使用 Generator 可以实现另一套异步编程方式。
// 生成器函数回顾
function * foo () {
console.log('start')
try {
const res = yield 'foo'
console.log(res)
const res1 = yield 'zce'
} catch (e) {
console.log(e)
}
}
const generator = foo()
const result = generator.next()
console.log(result)
const res1 = generator.next('bar')
console.log(res1)
console.log(generator.next())
// generator.throw(new Error('Generator error')) // 可以抛出 generator 的错误
// 输出顺序:
// start
// { value: 'foo', done: false }
// bar // 这里输出 bar 是因为 generator 的第2个 next 传入了一个值给了 foo 函数里的 res 变量,如果没有传入值这里就是输出 undefined
// { value: 'zce', done: false }
// { value: undefined, done: true }
使用 Generator 配合 Promise,用递归实现依次执行,直到 generator 返回的对象的 done 属性为 true,就停止递归,整个 Generator 函数执行完毕。
// Generator 配合 Promise 的异步方案
function ajax (url) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'json'
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(new Error(xhr.statusText))
}
}
xhr.send()
})
}
function * main () {
try {
const users = yield ajax('/api/users.json')
console.log(users)
const posts = yield ajax('/api/posts.json')
console.log(posts)
const urls = yield ajax('/api/urls11.json')
console.log(urls)
} catch (e) {
console.log(e)
}
}
// 执行器函数
function co (generator) {
const g = generator()
function handleResult (result) {
if (result.done) return // 生成器函数结束
result.value.then(data => {
handleResult(g.next(data))
}, error => {
g.throw(error)
})
}
handleResult(g.next())
}
co(main)
Async 函数
ES2017 中加入了 Async / Await 语法糖,提供了语言层面的异步编程标准。
// Async / Await 语法糖
function ajax (url) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'json'
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(new Error(xhr.statusText))
}
}
xhr.send()
})
}
async function main () {
try {
const users = await ajax('/api/users.json')
console.log(users)
const posts = await ajax('/api/posts.json')
console.log(posts)
const urls = await ajax('/api/urls.json')
console.log(urls)
} catch (e) {
console.log(e)
}
}
const promise = main() // async 函数返回一个 Promise 对象
promise.then(() => {
console.log('all completed')
})