本文深入解析JavaScript异步编程机制,从单线程模型出发,对比同步与异步模式。详细讲解回调函数的局限性及回调地狱问题,系统介绍Promise的三种状态、链式调用与异常处理。进一步探讨宏任务与微任务执行顺序,并对比Generator函数与Async/Await语法,提供从基础到高级的异步解决方案。
异步编程
当前主流的JavaScript运行环境均采用单线程模型来执行脚本。
JavaScript最初仅作为浏览器端的脚本语言,主要用于DOM操作。若在添加DOM的同时进行删除,浏览器将无法确定操作顺序,因此被设计为单线程模型。随着JavaScript功能日益丰富,若始终采用同步编程,浏览器在处理耗时操作时会出现假死现象,严重影响用户体验。为此,异步编程机制应运而生。
同步模式与异步模式
同步模式(Synchronous)
在同步模式中,代码按编写顺序依次执行,后续代码必须等待前一步骤完全执行完毕后才能开始运行。
console.log('global begin')
function bar() {
console.log('bar task')
}
function foo() {
console.log('foo task')
bar()
}
foo()
console.log('global end')异步模式(Asynchronous)
异步模式不等待任务执行完成即启动后续任务,通常在任务启动后立即执行下一行代码,后续逻辑则通过回调函数来定义和处理。
console.log('global begin')
setTimeout(function timer1 () {
console.log('timer1 invoke')
}, 1800)
setTimeout(function timer2 () {
console.log('timer2 invoke')
setTimeout(function inner () {
console.log('inner invoke')
}, 1000)
}, 1000)
console.log('global end')回调函数
回调函数——所有异步编程方案的根基
回调函数本质上是对特定数据操作逻辑的封装,当预定的操作执行完毕后,系统会调用该函数以处理结果。
在面试中,回调函数常是必问话题,尤其是关于什么是回调地狱以及如何解决它。以下面代码为例:
// 回调地狱,只是示例,不能运行
$.get('/url1', function (data1) {
$.get('/url2', data1, function (data2) {
$.get('/url3', data2, function (data3) {
$.get('/url4', data3, function (data4) {
$.get('/url5', data4, function (data5) {
$.get('/url6', data5, function (data6) {
$.get('/url7', data6, function (data7) {
// 略微夸张了一点点
})
})
})
})
})
})
})冗长的回调嵌套不仅难以阅读,一旦代码出现错误,排查问题更是令人头疼。幸运的是,JavaScript语言在不断演进,在ES2015(即ES6)中推出了一种解决方案,让开发者不再为回调地狱所困扰。
Promise
Promise——一种更优的异步编程统一方案
可以将Promise理解为“承诺”或“期约”(这是《JavaScript高级程序设计》第四版中的译法,感兴趣者可查阅该书)。它表示一个值在未来某个时刻一定会执行,你可以信赖它。
首先,需要了解Promise具有三种状态:pending(等待)、onFulfilled(完成)和onRejected(失败)。一旦状态确定为完成或失败,便不可更改。
Promise基本用法
const promise = new Promise(function (resolve, reject) {
// 注意,要得到reject的结果时要先把resolved的代码注释掉,原因上面已经解释了
resolve(100) // 兑现承诺
reject(new Error('promise rejected')) // 承诺失败
})
promise.then(function(value) {
console.log('resolved', value)
}, function(error) {
console.log('rejected', error)
})
console.log('end')每个new Promise实例接收两个参数,第一个是兑现承诺的函数,其内部值会传递给Promise实例,reject同理。返回的Promise对象自带then方法,该方法也接收两个参数,分别代表成功和失败的回调函数。
Promise使用案例
在当前文件夹下新建一个文件夹,其中随意放置两个json文件,用于模拟ajax请求。
// Promise 方式的 AJAX
function ajax(url) {
return new Promise(function(resolve, reject) {
let 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()
})
}
ajax('./api/posts.json').then(function (res) {
console.log(res)
}, function(err) {
console.log(err)
})注意,上述代码应在浏览器环境中运行。
Promise常见误区
有些开发者在学习Promise后,可能会写出如下代码:
ajax('/api/urls.json').then(function (urls) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
})
})
})
})
})说实话,这样写还不如不写。正确的写法应采用链式调用,熟悉jQuery的读者应该不会感到陌生。
ajax('/api/users.json')
.then(function (value) {
console.log(1111)
return ajax('/api/urls.json')
}) // => Promise
.then(function (value) {
console.log(2222)
console.log(value)
return ajax('/api/urls.json')
}) // => Promise
.then(function (value) {
console.log(3333)
return ajax('/api/urls.json')
}) // => Promise
.then(function (value) {
console.log(4444)
return 'foo'
}) // => Promise
.then(function (value) {
console.log(5555)
console.log(value)
})Promise异常处理
通过.catch方法来捕获异常。
ajax('/api/users.json')
.then(function onFulfilled (value) {
console.log('onFulfilled', value)
return ajax('/error-url')
})
.catch(function onRejected (error) {
console.log('onRejected', error)
})其实catch方法与then方法实现原理相似,仅是将then方法的第一个参数传入undefined,属于一种语法糖。还有一个有趣的现象是,当中间的then出现错误时,异常会直接穿透至最后的catch方法。若有兴趣了解其实现原理,可查阅相关源码,相信会有所收获。
Promise静态方法
- Promise.resolve
Promise.resolve('foo')
.then(function (value) {
console.log(value)
})
该方法会直接将传入的内容当作一个onFulfilled对象返回;若传入Promise对象,则直接返回该Promise.resolve方法的结果;若传入带有then方法的函数,处理方式同理。
- Promise.reject
该方法的调用方式与上述类似,但后续通常接一个catch处理。
- Promise.all
该方法接收一个数组,会等待数组内的方法全部调用完毕后,再返回一个包含所有结果的数组对象。
- Promise.race
该方法会返回最先完成的promise实例。
宏任务与微任务
// 微任务
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')每次调用宏任务之前,必须确保微任务队列已清空,因此也能理解为何输出会按特定顺序进行。
常见的宏任务有
- setTimeout
- setInterval
- setImmediate
- I/O
- UI rendering
常见的微任务有
- promise
- nextTick
- mutationObserver
Generator 异步方案
ES6也推出了Generator异步解决方案。首先来看下生成器函数如何使用。
生成器函数回顾
function *foo() {
console.log('satrt')
try {
const res = yield 'foo'
console.log(res)
} catch (e) {
console.log(e)
}
}
const generator = foo()
const result = generator.next()
console.log(result)
generator.throw(new Error('Generato error'))生成器函数比普通函数多了一个 * 符号,其放置位置在左在右均可,我个人倾向于放在右边。
其内部包含一个next方法,返回一个对象,大致结构如下
{ value: 'foo', done: false }
value为yield返回的值,当然,如果在调用yield时传入了值,返回的值就是你传入的值。当执行完毕时,done属性变为true。
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)只要理解这一点,即必须通过调用next方法才能进行下一步,否则代码会停在yield处。不过,每次享受优美代码时都需要自己编写一个co函数,未免过于麻烦。不过不用担心,async即将登场。
async与await
async、await——可能是异步的终极解决方案
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)
}
}只需在函数定义前添加async,并在希望等待完成的操作前添加await,即可实现同步书写代码而异步调用的效果。怎么样?这是否更加优雅便捷呢?鉴于目前ECMAScript的发展趋势,未来或许无需async,直接使用await即可实现异步编程。
到此这篇关于JavaScript异步编程操作实现介绍的文章就介绍到这了,更多相关JS异步编程内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!