【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(课前准备)
【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(一、函数式编程范式)
【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(二、JavaScript 异步编程)
【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(三、手写Promise源码)
【Part1作业】https://gitee.com/zgp-qz/part01-task
【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(一、ECMAScript 新特性)
【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(二、TypeScript 语言)
【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(三、JavaScript 性能优化1)
【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(四、JavaScript 性能优化2)
【Part2作业】https://gitee.com/zgp-qz/part02-homework
JavaScript 异步编程
-
-
-
- 异步编程
-
-
异步编程
单线程 javascript 异步方案
目前主流的 javascript 环境,都是以 单线程 模式去执行的 javascript 代码,javascript 采用单线程模式工作的原因与它最早的设计初衷有关。
最早 javascript 这门语言就是运行在浏览器端的脚本语言,它的目的是为了用来去实现页面上的动态交互, 而实现页面交互的核心就是 DOM 操作,这也就决定了它必须使用单线程模型,否则就会出现很复杂的线程同步问题。
为了避免这种线程同步问题,javascript 从一开始就被设计成了这种单线程的工作模式。这也就成为了这门语言最为核心的特性之一。
这里所说的单线程指的是:js 执行环境中,负责执行代码的线程只有一个。
如果出现了一个特别耗时的任务,就会导致程序会被拖延,出现假死的情况。
为了解决这种耗时任务阻塞程序执行的这种问题,javascript 语言将任务的执行模式,分成了两种,分别是:同步模式(Synchronous) 和 异步模式(Asynchronous)
这里主要讲解的是:
同步模式与异步模式
- 同步模式和异步模式表象上的差异以及存在的意义事件循环与消息队列
- javascript 单线程如何实现的异步模式异步编程的几种方式
Promise 异步方案、宏任务 / 微任务队列
- ES2015 当中提供的 Promise 异步方案
- 牵扯到的 宏任务 / 微任务 相关概念Generator 异步方案、Async / Await 语法糖
- ES2015 提供的 Generator 异步方案
- ES2017 提供的 Async / Await 语法糖
-
同步模式(Synchronous)
代码当中的任务依次执行,后一个任务必须要等待前一个任务结束,才能够开始执行。
程序的执行顺序和代码的编写顺序完全一致。
注意:这里的同步不是同时执行,而是排队执行。
-
异步模式(Asynchronous)
异步模式是不回去等待这个任务的结束才开始执行下一个任务,对于耗时操作是开启过后就立即往后执行下一个任务。耗时任务的后续逻辑一般会通过回调函数的方式定义。
在内部这个耗时任务完成之后,就会自动执行我们这里传入的回调函数。
如果没有这种异步模式的话,javascript 就无法同时处理大量的耗时任务。
对于开发者而言,难点在于 代码执行顺序混乱 ,更多的时候是需要理解和习惯。最好的办法就是多看多练多思考。
这里的不管同步也好,异步也好,肯定不是说的我们写代码的方式,而是说我们运行环境所提供的 API 到底是以同步或异步模式的方式去工作 -
回调函数 – 所有异步编程方案的根基
回调函数可以理解为一件你想要做的事情,明确的知道这件事情该怎么做,但是你并不知道这件事情所依赖的任务什么时候完成,最好的办法就是把你这件事情的步骤写到一个函数当中,交给异步任务的执行者,这个异步任务的执行者知道这个任务什么时候结束,它就可以在任务结束过后,帮你执行你想要做的事情。那么这件想要做的事情我们就可以理解成 回调函数 。
这种由调用者定义,交给执行者执行的函数,就被称之为回调函数。
-
Promise – 一种更优的异步编程统一方案
直接使用传统的回调方式去完成复杂的异步流程,就无法避免大量的回调函数嵌套,这也就会导致我们常说的 回调地狱
为了避免 回调地狱的问题,CommonJS 社区 率先提出了 Promise 的规范。目的就是为异步编程去提供一种更合理,更强大的统一解决方案。
在 ES2015 中被标准化,成为语言规范。
-
Promise – 基本用法
// Promise 基本示例const promise = new Promise(function (resolve, reject) { // 这里用于“兑现”承诺// resolve(100) // 承诺达成reject(new Error('promise rejected')) // 承诺失败 })promise.then(function (value) { // 即便没有异步操作,then 方法中传入的回调仍然会被放入队列,等待下一轮执行console.log('resolved', value) }, function (error) { console.log('rejected', error) })console.log('end')
-
Promise – 使用案例
// Promise 方式的 AJAX function ajax (url) { return new Promise(function (resolve, reject) { 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()}) }ajax('/api/foo.json').then(function (res) { console.log(res) }, function (error) { console.log(error) })
-
Promise – 常见误区
// Promise 常见误区function ajax (url) { return new Promise(function (resolve, reject) { 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 是最常见的误区 // 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) { // }) // }) // }) // }) // })
-
Promise – 链式调用
相比于传统回调的方式,Promise 最大的优势就是可以链式调用。这样就可以最大程度的避免回调嵌套。
· Promise 对象的 then 方法会返回一个全新的 Promise 对象。
· 后面的 then 方法就是在为上一个 then 返回的 Promise 注册回调。
· 前面 then 方法中回调函数的返回值会作为后面 then 方法回调的参数。
· 如果回调中返回的是 Promise ,那后面 then 方法的回调会等待它的结束。// Promise 链式调用function ajax (url) { return new Promise(function (resolve, reject) { 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()}) }// var promise = ajax('/api/users.json')// var promise2 = promise.then( // function onFulfilled (value) { // console.log('onFulfilled', value) // }, // function onRejected (error) { // console.log('onRejected', error) // } // )// console.log(promise2 === promise)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 方法其实就是 then 方法的别名
then 方法的第二个参数只是给当前的 then 方法指定的 失败回调
最后的 catch 方法是给前面的 then 方法指定的 catch (错误回调)
只不过是前面的 then 方法捕获到的错误会通过链条一直往后传递,传递到了最后的 catch 方法
// 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()}) }// ajax('/api/users11.json') // .then(function onFulfilled (value) { // console.log('onFulfilled', value) // }, function onRejected (error) { // console.log('onRejected', error) // })// 使用 catch 注册失败回调是更常见的// ajax('/api/users11.json') // .then(function onFulfilled (value) { // console.log('onFulfilled', value) // }) // .catch(function onRejected (error) { // console.log('onRejected', error) // })// then(onRejected) 实际上就相当于 then(undefined, onRejected)// ajax('/api/users11.json') // .then(function onFulfilled (value) { // console.log('onFulfilled', value) // }) // .then(undefined, function onRejected (error) { // console.log('onRejected', error) // })// 同时注册的 onRejected 只是给当前 Promise 对象注册的失败回调 // 它只能捕获到当前 Promise 对象的异常// ajax('/api/users.json') // .then(function onFulfilled (value) { // console.log('onFulfilled', value) // return ajax('/error-url') // }, function onRejected (error) { // console.log('onRejected', error) // })// 因为 Promise 链条上的任何一个异常都会被一直向后传递,直至被捕获 // 分开注册的 onRejected 相当于给整个 Promise 链条注册失败回调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 } = eventconsole.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 对象 // })
下方的这种全局捕获的方式不推荐使用:
更合适的办法是在代码中明确的捕获每一个可能发生的异常,而不是丢给全局统一处理
除此之外,我们还可以在全局对象上注册一个 unhandleredrejection 事件,去处理那些我们代码当中没有被手动捕获的 promise 异常
在浏览器环境中:
在 node 环境中:
注意:node 环境中事件的名称是驼峰命名的
-
Promise 静态方法
Promise.resolve()
作用:快速的把一个值转换为一个 Promise 对象,如果接收到的是另外一个 Promise 对象,那这个 Promise 对象会被原样返回
特殊情况:如果传入的是一个 对象,对象包含一个 同 Promise 一样的 then 方法,那么在这个方法当中可以接收到 onFulfilled 和 onRejected 两个回调,调用 onFulfilled 传入一个值,这样的一个对象也可以作为 Promise 对象被执行,在后面的 then 方法中也能够拿到对应的被传入的值。
带有这种 then 方法的对象,称之为 thenable 的接口。也就是说它是一个可以被 then 的对象。
Promise.reject()
作用:快速去创建一个一定是失败作用的对象。
// 常用 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) // })// 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)// 如果传入的是带有一个跟 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 并行执行
相比于传统回调的方式,Promise 提供了更扁平的异步编程体验。如果我们需要同时并行执行多个异步任务,Promise 也可以提供更为完善的体验。
Promise.all() // 同时执行多个异步任务,可以把多个 Promise 对象组合为一个全新的 promise 对象
Promise.race() // 同样是同时执行多个异步任务,可以把多个 Promise 对象组合为一个全新的 promise 对象
两者不同的是:
Promise.all() 等待所有任务结束后才会结束,执行后面的 then 方法
Promise.race() 是跟着所有的任务当中第一个完成的任务一起结束,执行后面的 then 方法,也就是说只要有任何一个任务完成了,这个所返回的新的 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()}) }// ajax('/api/users.json') // ajax('/api/posts.json')// var promise = Promise.all([ // ajax('/api/users.json'), // ajax('/api/posts.json') // ])// promise.then(function (values) { // console.log(values) // }).catch(function (error) { // console.log(error) // })// ajax('/api/urls.json') // .then(value => { // const urls = Object.values(value) // const tasks = urls.map(url => ajax(url)) // return Promise.all(tasks) // }) // .then(values => { // console.log(values) // })// Promise.race 实现超时控制const request = ajax('/api/posts.json') const timeout = new Promise((resolve, reject) => { setTimeout(() => reject(new Error('timeout')), 500) })Promise.race([request,timeout ]) .then(value => { console.log(value) }) .catch(error => { console.log(error) })
-
Promise 执行时序
即便是我们的 Promise 当中并没有任何的异步操作,它的回调函数仍然会进入到回调队列当中去排队。也就是说我们必须要等待当前所有的同步代码执行完了过后才会去执行 promise 当中的回调。
回调队列中的任务称之为 [ 宏任务 ]
宏任务执行过程当中会临时加上一些额外的需求,这时对于这些额外的临时需求可以选择作为一个新的宏任务进到队列中排队。
也可以作为当前任务的 [ 微任务 ]
直接在当前任务结束过后立即执行,而不是到整个队伍的末尾进行排队。
这就是 宏任务 和 微任务之间的一个差异。
Promise 的回调,就是作为微任务执行的。所以他会在本轮调用结束的末尾自动执行,这也就是为什么会先打印的 promise 然后再打印的 setTimeout 。
微任务:提高整体的响应能力。
目前,绝大多数异步调用都是作为宏任务执行(进入到回调队列),而 Promise 和 MutationObserver 还有 node 当中的一个 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')
-
Generator 异步方案(上)
Promise 会形成一个任务的链条,从而实现所有任务的串联执行,但是这样写仍然会有大量的回调函数,虽然它们相互之间没有嵌套,但是他们还是没有办法达到传统,同步代码的那种可读性。
如果是传统同步代码的方式,我们的代码可能会是这个样子的:
很明显,这种方式去写异步代码,它是最简洁,也是最容易阅读和理解的。ES2015 提供的 Generator (生成器函数)
// 生成器函数回顾function * foo () { console.log('start')try { const res = yield 'foo'console.log(res)} catch (e) { console.log(e)} }const generator = foo()const result = generator.next() console.log(result)// generator.next('bar')generator.throw(new Error('Generator error'))
-
Generator 异步方案(中)
体验 Generator 函数异步方案
例子见 15. Generator 异步方案(下) – 递归执行 Generator 函数
-
Generator 异步方案(下) – 递归执行 Generator 函数
2015 年之前是很流行的,但是后来随着技术的发展,出现的 Async & Await 之后,这种方案相对来讲就没有那么普及了,不过使用 Generator 它最明显的一个变化就是让我们的异步调用再次回归到扁平化。
这也是 javascript 异步编程发展过程当中很重要的一步。
// 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)// const result = g.next()// result.value.then(data => { // const result2 = g.next(data)// if (result2.done) return// result2.value.then(data => { // const result3 = g.next(data)// if (result3.done) return// result3.value.then(data => { // g.next(data) // }) // }) // })
-
Async / Await 语法糖 – 语言层面的异步编程标准
有了 Generator 过后,javascript 中的异步编程,基本上就已经与同步代码有类似的体验了,但是使用 Generator 这种异步编程方案,我们还需要自己手动编写一个类似于 co 的执行器函数,所以说会比较麻烦。
在 ES2017 当中提供了一个 Async 的函数,同样提供了这种扁平化的异步编程体验。而且它是语言层面标准的异步编程 语法。所以使用起来就会更加的方便一点。其实 Async 函数就是 Generator 函数的一种更方便的语法糖。所以语法上是非常类似的。
Async 函数会给我们返回一个 promise 对象,这样就利于我们对整体代码进行控制。
注意:await 关键词只能出现在 Async 函数内部,不能直接在外部(顶层作用域)直接使用。
不过关于在最外层直接使用 await 的功能已经在开发了。不久之后可能出现在标准当中。到时候使用 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()}) }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()) }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)} }// co(main) const promise = main()promise.then(() => { console.log('all completed') })
【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(课前准备)
【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(一、函数式编程范式)
【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(二、JavaScript 异步编程)
【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(三、手写Promise源码)
【Part1作业】https://gitee.com/zgp-qz/part01-task
【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(一、ECMAScript 新特性)
【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(二、TypeScript 语言)
【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(三、JavaScript 性能优化1)
【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(四、JavaScript 性能优化2)
【Part2作业】https://gitee.com/zgp-qz/part02-homework