同步模式与异步模式
- JS是单线程语言
JS执行环境找那个负责执行代码的线程只有一个
执行任务的模式有两种:同步模式、异步模式。
同步模式
就是简单的顺序执行,遇到了就压入调用栈,执行完之后弹出。执行的时候用到的语句也会压入调用栈,执行结束弹出。同步模式的问题主要就是浏览器环境中的ajax请求或者nodejs环境中的大文件读写。
异步模式
耗时任务开启之后就会立即执行下一个任务。,回叙逻辑一般通过回调函数定义。单线程的JS需要一部任务来处理大量的耗时的任务。但是异步的代码执行比较跳跃。包括消息队列,执行栈,Web API等等。例如setTimeout会在setTimeout压栈的时候利用WebAPI执行,也就是开启一个倒计时器,然后setTimeout就弹出执行栈,继续执行任务了。当WebAPI开启一个倒计时器结束就会把这个任务放入消息队列,然后消息队列里面的任务会被顺序压入调用栈。直到调用栈和消息队列都为空。
举一个形象的例子:调用栈:执行工作表。消息队列:待办工作表。先去做完调用栈,然后再去做消息队列。消息队列里面的任务会排队等待事件循环。
这里虽然JS是单线程(主线程)的,但是WebAPI是多线程(其他的回调线程)的!
同步模式和异步模式指的是API是以同步模式去工作还是异步模式的方式去工作。
回调函数
- 所有异步编程方案的根基
- 是指使用者自己定义一个函数,实现这个函数的程序内容,然后把这个函数(入口地址)作为参数传入别人(或系统)的函数中,由别人(或系统)的函数在运行时来调用的函数。函数是你实现的,但由别人(或系统)的函数在运行时通过参数传递的方式调用,这就是所谓的回调函数。简单来说,就是由别人的函数运行期间来回调你实现的函数。
实现异步的根本就是回调函数。实现回调函数的方式:
- 直接传递回调函数参数(不利于阅读)
- 事件(观察者?)
- 发布订阅
回调函数嵌套就是回调地狱。解决回调函数嵌套的方法主要有Promise,Generator和Async
事件循环与消息队列
为什么会有消息队列和事件循环呢?首先最关键的一点在于JS是个单线程,并且主线程非常繁忙,既要处理 DOM,又要计算样式,还要处理布局,同时还需要处理 JavaScript 任务以及各种输入事件。要让这么多不同类型的任务在主线程中有条不紊地执行,这就需要一个系统来统筹调度这些任务,这个统筹调度系统就是消息队列和事件循环系统。
消息队列
消息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点,也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。
事件循环
在线程运行过程中,接收并执行新的任务,就需要采用事件循环机制。
消息队列和事件循环的配合♻️
其实事件循环机制和消息队列的维护是由事件触发线程控制的。事件触发线程同样是浏览器渲染引擎提供的,它会维护一个消息队列。JS引擎线程遇到异步(DOM事件监听、网络请求、setTimeout计时器等),会交给相应的线程单独去维护异步任务,等待某个时机(计时器结束、网络请求成功、用户点击DOM),然后由事件触发线程将异步对应的回调函数封装成任务并加入到消息队列中对应的任务队列中,等待被执行。
同时,JS引擎线程会维护一个执行栈(调用栈),同步代码会依次加入执行栈(调用栈)然后执行,结束会退出执行栈。如果执行栈(调用栈)里的任务执行完成,即执行栈为空的时候(即JS引擎线程空闲),事件触发线程才会从消息队列取出一个任务(即异步的回调函数)放入执行栈中执行。
存在的问题
添加了微任务队列来解决,通常我们把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列,在执行宏任务的过程中,如果DOM有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,因此也就解决了执行效率的问题。等宏任务中的主要功能都直接完成之后,这时候,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,因为 DOM 变化的事件都保存在这些微任务队列中,这样也就解决了实时性问题。
JavaScript 可以通过回调功能来规避这种问题,也就是让要执行的 JavaScript 任务滞后执行。
异步编程的几种方式
-
回调函数
-
事件监听
采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。
-
Promise对象
Promise对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。 简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。
-
发布/订阅
Promise异步方案、宏任务/微任务列表
-
promise一共有三种状态:Pending、Fulfilled(onFulfilled)和Rejected(onRejected)。
-
Pending 之后 Fulfilled(onFulfilled)或者Rejected(onRejected)。明确结果之后就承诺的结果不会改变了。
Promise基本用法
const promise = new Promise(function(resolve,reject) {
// 兑现承诺
resolve(100)
// reject(new Error('promise rejected'))
})
promise.then(function(value){
console.log('resolved',value);
},function(error){
console.log('rejected',error);
})
console.log('end');
//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()
})
}
Promise链式调用
嵌套使用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) {
})
})
})
})
})
- then的链式调用可以解决回调地狱,让异步调用扁平化!
- then方法返回的是全新的promise对象
- 后面的then为上一个返回的promise注册回调
- 前面的回调函数的返回值作为后面回调的参数
- then方法会等待前面任务的结束才会执行!需要前面的返回值嘛
then接口第一个函数是onFulfilled第二个函数是onRejected,then一般不写第二个参数,这样then只执行onFulfilled
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
catch第一个参数默认写undefined,所以第二个参数是onRejected。也就可以捕捉前面的回调的错误了!
ajax('/api/users.json').then(function onFulfilled (res){ console.log(res); return ajax('/api/urls.json') }).catch(function onRejected (error){ console.log(error); return ajax('/api/urls.json') })
-
unhandledRejection
可以用来监听全局回调的异常,不推荐全局捕获的方式。效率低。
process.on('unhandledRejection',(reason,promise)=> { console.log(reason,promise); })
Promise静态方法
reslove
执行成功
- 快速把一个值转化成为promise对象
- 接受一个promise对象原样返回
- 把thenable的对象转化为promise对象
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)
})
Reject
执行失败
传入的参数会作为失败的原因
// Promise.reject 传入任何值,都会作为这个 Promise 失败的理由
Promise.reject(new Error('rejected'))
.catch(function (error) {
console.log(error)
})
Promise.reject('anything')
.catch(function (error) {
console.log(error)
})
All(Promise并行执行)
之前都是串联执行,Promise也可以提供并行执行。比如同时调用ajax,应该怎么判断所有请求都结束了呢?这里就需要Promise的静态方法all登场了!Promise.all等所有的方法完成后才会完成!也就是多个promise合成一个promise!
var promise = Promise.all([
ajax(),
ajax(),
])
promise.then(function (values) {
console.log(values);
}).catch(function(error) {
console.log(error);
})
- 组合使用串行和并行的方式
// 获取包含url地址的urls数组
ajax('/api/urls.json')
// 得到的是包含所有URL地址的对象
.then(value => {
// 获取对象的所有的属性的值(URL地址)组成的一个数组
const urls = Object.values(value)
// 利用上面封装的ajax把字符串数组转化成为包含全部请求任务的Promise对象数组
const tasks= urls.map(url => ajax(url))
// 调用Promise.all把Promise数组组合成为一个新的Promise然后return掉
return Promise.all(tasks)
})
// then可以拿到当前Promise数组的每一个异步请求得到的结果数数据
// 需要等待所有任务结束才会结束 全成功才成功
.then(values =>{
console.log(values);
})
race
Promise.race会返回第一个完成的对象!
// 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执行时序/宏任务vs微任务
promise的回调会作为微任务执行
-
提高应用的响应能力
-
微任务
- Promise
- MutationObserver
- process.nextTick
-
微任务的执行是在本轮调用的末尾,当前任务结束过后立即执行。
- 宏任务与微任务
Generator异步方案、Async/Await语法糖
Generator的基本使用
生成器函数会返回一个生成器对象,调用这个生成器对象的next方法,才会让函数体执行,一旦遇到了yield关键词,函数的执行则会暂停下来,yield后面的值作为next函数的结果返回,如果继续调用函数的next函数,则会再上一次暂停的位置继续执行,知道函数体执行完毕,next返回的对象的done就变成了true
function* foo() {
console.log('start')
try {
const res = yield 'foo'
console.log(res)
} catch (e) {
console.log(e)
}
}
const generator = foo()
// 调用next方法会执行到yield关键词
const result = generator.next()
console.log(result)
// { value: 'foo', done: false }
// 调用throw方法抛出异常
generator.throw(new Error('Generator error'))
// Error: Generator error
//使用的时候在then中调用next方法就可以了!递归实现
function handleResult(result) {
if (result.done) return
result.value.then(
(data) => {
handleResult(g.next(data))
},
(error) => {
g.throw(error)
}
)
}
handleResult(g.next())
//使用的时候整体做一个try catch
function* foo() {
console.log('start')
try {
const res1 = yield 'foo'
console.log(res)
const res2 = yield 'bar'
console.log(res)
const res3 = yield 'ohh'
console.log(res)
} catch (e) {
console.log(e)
}
}
Generator实现异步
注意:generator.next(value)中,next传入的参数会作为上一次yield的返回值。
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()
})
}
// 生成器函数
function * main () {
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/urls.json')
console.log(urls)
}
// 调用生成器函数得到一个生成器对象
const generator = main()
// 递归实现generator.next()的调用,直到done为true终止
function dfs(value) {
const result = generator.next(value)
if(result.done) return
result.value.then(data=>{
console.log(data)
dfs(data)
})
}
dfs()
// 打印结果
// Generator实现异步.js:35 {username: "yibo"}
// Generator实现异步.js:19 {username: "yibo"}
// Generator实现异步.js:35 {posts: "jiailing"}
// Generator实现异步.js:22 {posts: "jiailing"}
// Generator实现异步.js:35 {posts: "/api/posts.json", users: "/api/users.json"}
// Generator实现异步.js:25 {posts: "/api/posts.json", users: "/api/users.json"}
co
可以把上面的generator封装成为一个简易的co库
// 封装了一个生成器函数执行器
function co(generator) {
// 调用生成器函数得到一个生成器对象
const g = generator()
// 递归实现generator.next()的调用,直到done为true终止
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/Await语法糖
- await关键词只能出现在async函数中
generator需要配合co或者自己写递归在then中执行next方法,async解决了这个麻烦。
async await就是generator的语法糖。内部原理是相同的。
async function mainAsync () {
try {
// 返回具有ajax调用的promise对象
const users = await ajax('/api/users.json')
// 接收一下yield的返回值
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);
}
}