JS异步编程
ExMaterial 人气:0异步编程
目前主流的JavaScript执行环境都是以单线程执行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理解成“承诺”或者“期约”(js高程作者的翻译,想了解的话,可以去看看红宝书第四版),你已经声明了这个东西,它在未来的时间一定会执行,你可以相信它。
首先,你要了解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方法第一个参数传入undefine,一个语法糖而已。还有一个有意思的现象是,当中间的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就能实现异步编程了。
加载全部内容