浏览器原理与实现-学习4-消息循环/setTimeout/微任务/Promise/async-await
1.消息队列与实践循环
单线程处理任务的3个渐进方案
第一版
逐行执行的缺点
- 必须提前设定好,无法中途新增
var a = 1;
var b = 1;
console.log(a)
console.log(b)
console.log(a+b)
第二版
- 加入while循环+ 获取输入和计算,可以一直获取输入。
- 引入了循环
- 引入了事件
缺点 : 不支持外部的事件
while(true) {
let a = prompt("请输入a:")
let b = prompt("请输入b:")
console.log(a+b)
}
第三版
- 新增消息队列
- 循环队列依次去读队列内容,弹出一个读一个
//定义队列结构
class TaskQueue{
function takeTask(); // 取出队列头部的一个任务
function pushTask(Task task); // 添加一个任务到队列尾部
};
//任务结构
class Task{
string name;
function do() {
}
};
//主代码
TaskQueue task_queue = new TaskQueue()
function ProcessTask(task){
task.do(); ...
... //任务逻辑
}
//程序入口
void MainThread(){
for(;;){
Task task = task_queue.takeTask();
ProcessTask(task); //一直循处理任务
}
}
//动态添加任务
Task task1 = new Task('任务1',...)
task_queue.pushTask(task1)
Task task2 = new Task('任务2',...)
task_queue.pushTask(task2)
js中的 事件循环
上面第三种方案就是 事件循环机制。
- 添加一个消息队列
- 队列
- 先进先出
- 跨线程处理任务,通过引入io线程统一转发给 渲染主线程
- 资源加载完回调:来自网络进程 通过IPC传输
- 鼠标点击回调: 来自浏览器进程 通过IPC传输
- 渲染主线程会循环读取消息队列头任务,并执行任务
function main(){
isOver = false //是否标识了退出,默认不退出
while(true) {
const taskList = taskListForIO //任务列表为io线程传入,一直动态变化
processTask(taskList.shift())//每次取出一个任务执行
if(isOver) { //判断是否退出
break
}
}
}
//实时获取最新的来自io 线程的任务
function taskListForIO() {
const taskList = [task1,task2]
///.. 这里省略 io 新增任务的逻辑
return taskList //
}
//处理任务
function processTask(task) {
//...
}
- 退出策略 添加一个标识,每次循环检查是否可以退出
消息队列类型
- 内部消息队列
- 如输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript 定时器
- 页面相关事件
- JavaScript 执行、解析 DOM、样式计算、布局计算、CSS 动画
单线程执行的缺点
- 实时同步执行会严重拖慢后面排队的任务
- 优先级无法默认排在消息队列尾部,执行时机太晚
- 监听dom实时变化的函数 Mutation Event
- 同步太频繁
- 异步执行时机太晚
解决方案
微任务
-
每个宏任务都包含一个微任务队列,
-
每次宏任务执行完之前都会清空微任务,即任何时候,当前的微任务都要先于宏任务执行。
其实就是在执行完宏任务4时候,把微队列里的所有任务都先执行完,再走下一个宏任务
通过浏览器performance分析
通过圆点录制当前js的整个调用栈情况,分析每一个task,定位出现长时间执行的代码
2.WebAPI:setTimeout是如何实现的?
常见的消息循环事件
- 浏览器接收并解析html文档,解析dom事件
- 改变windows窗口大小事件
- 垃圾回收事件
- setTimeout的事件
settimeout实现
- 在每次事件循环处理完 processTask后,都会调用一遍ProcessDelayTask,如果当前有任务已经满足时间,则出发对应的回调方法
- 缺点:当前面有个长任务再跑,processDelayTask执行时机会被延后
setTimeout常用用法
function showName(){
console.log("xxx")
}
var timerID = setTimeout(showName,200);
clearTimeout(timer_id)
模拟setTimeout代码实现
DelayedIncomingQueue DelayTaskList;
struct DelayTask{
int64 id;
CallBackFunction cbf;
int start_time;
int delay_time;
};
DelayTask timerTask;
timerTask.cbf = showName;
timerTask.start_time = getCurrentTime(); // 获取当前时间
timerTask.delay_time = 200;// 设置延迟执行时间
//在外部调用settimeout(){...} 代码是自动触发下面 push 添加任务
DelayTaskList.push(timerTask);
//执行定时任务的方法
void ProcessDelayTask(){
for (let i = DelayTaskList.length ; i >= 0; i-- ){// DelayTaskList 循环取出已经到期的定时器任务
let obj = DelayTaskListp[i]
if(obj.start_time + obj.start_time === Date.now) {
obj.cbf() //执行回调
DelayTaskListp.pop() //弹出
}
}
}
//主程序入口
function main(){
isOver = false //是否标识了退出,默认不退出
while(true) {
const taskList = taskListForIO //任务列表为io线程传入,一直动态变化
processTask(taskList.shift())//每次取出一个任务执行
ProcessDelayTask()// 执行延迟队列中的任务
if(isOver) { //判断是否退出
break
}
}
}
//实时获取最新的来自io 线程的任务
function taskListForIO() {
const taskList = [task1,task2]
///.. 这里省略 io 新增任务的逻辑
return taskList //
}
//处理正常宏任务
function processTask(task) {
//...
}
当settimeout 执行任务时间过长
导致下一个宏任务不能预期执行。
function bar() {
console.log('bar')
}
function foo() {
setTimeout(bar, 0);
for (let i = 0; i < 10000; i++) {//每次由于运算大, 都会导致下一个宏任务执行时间不能达到预期。
let i = 5+8+8+8
console.log(i)
}
}
foo()
当settimeout 循环嵌套执行超过5次,
chrome会强制把间隔时间调整为 最小间隔4秒
function cb() { setTimeout(cb, 0); } ////虽然默认是0 但是实际会因为嵌套超过5次,调整为4秒间隔执行
setTimeout(cb, 0);
Chrome源码中的定义
static const int kMaxTimerNestingLevel = 5;
static constexpr base::TimeDelta kMinimumInterval = base::TimeDelta::FromMilliseconds(4);
未激活的页面 settimeout执行最小时间间隔是1秒
就在有些tab页签再未激活显示时候,最小也只能设置1秒的定时,为了优化浏览器性能的消耗。
settimeout的最大值
- 32 个 bit 2147483647 毫秒即 24.8天
- 超出这个范围后就会溢出,变成立即执行
function showName(){
console.log("xxx")
}
var timerID = setTimeout(showName,2147483648);// 实际不会延迟,立即执行
settimeout的this 指向
默认指向window,修复方法
var name= 1;
var MyObj = {
name: 2,
showName: function(){
console.log(this.name); //this默认是指向全局
}
}
setTimeout(MyObj.showName,1000) //这里 相当于 在全局定义了一个function showName 方法,然后window.showName调用
修复方法
通过箭头函数
setTimeout(
() => {
MyObj.showName() //这里就是正常的对象调用方法,会隐式调用showName.call(MyObj)
}
,1000) //这里 相当于 在全局定义了一个function showName 方法,然后
通过 bind
var newShowName = MyObj.showName.bind(MyObj) //永久绑定 注意是用返回的对象
setTimeout(newShowName, 1000)
requestAnimationFrame 和 setTimeout 对比
- setTimeout是由前面宏任务决定下一次执行时间,不能保证在指定时间内执行。在动画方面会出现卡顿
- requestAnimationFrame是根据浏览器 16.7毫秒的间隔 准时执行,不收宏任务影响。可以很好的实现动画平滑执行。
- RAF能保证按照系统的刷新时间执行。回调函数会在下一次浏览器重绘之前被调用
3.xmlhttprequest 与 回调函数
回调函数
函数作为参数传入另外一个函数,这个参数就是回调函数
同步执行的回调
代码都是同步执行的,直接在当前主线程的调用栈中执行
let callback = function(){
console.log('i am do homework')
}
function doWork(cb) {
console.log('start do work')
cb()
console.log('end do work')
}
doWork(callback)
异步回调
通过事件循环触发的方法回调
let callback = function(){
console.log('i am do homework')
}
function doWork(cb) {
console.log('start do work')
setTimeout(cb,1000)
console.log('end do work')
}
doWork(callback)
xmlhttprequest 也是一种异步回调。
包含两种回调
- 宏任务回调
- 微任务回调
调试调用栈
- 可以通过谷歌浏览器开发者工具,的performance 查看
- 通过谷歌浏览器 输入 chrome://tracing/,通过 record记录调用栈信息,并查看
xmlhttprequest
定义
- 实现局部获取数据,解决传统页面整页刷新才能获取后端数据(SSR)
- 整页不刷用户体验好
流程
- 渲染进程 通过IPC 通知网络进程下载
- 网络进程下载完 通过IPC通知io线程加入消息队列
- 渲染线程挨个取出宏任务执行
xhr.responseType = "text" 控制不同的配置返回信息
- "text": 默认值,将响应体解析为字符串;
- "json": 将响应体解析为 JSON 对象;
- "arraybuffer": 将响应体解析为 ArrayBuffer 类型;
- "blob": 将响应体解析为 Blob 类型;
- "document": 将响应体解析为 HTML 或 XML DOM 对象。
例子
function GetWebData(URL){
//1: 新建 XMLHttpRequest 请求对象
let xhr = new XMLHttpRequest()
//2: 注册相关事件回调处理函数
xhr.onreadystatechange = function () {
switch(xhr.readyState){
case 0: // 请求未初始化
console.log(" 请求未初始化 ")
break;
case 1://OPENED
console.log("OPENED")
break;
case 2://HEADERS_RECEIVED
console.log("HEADERS_RECEIVED")
break;
case 3://LOADING
console.log("LOADING")
break;
case 4://DONE
if(this.status == 200||this.status == 304){
console.log(this.responseText);
}
console.log("DONE")
break;
}
}
xhr.ontimeout = function(e) { console.log('ontimeout') }
xhr.onerror = function(e) { console.log('onerror') }
// 3: 打开请求
xhr.open('Get', URL, true);// 创建一个 Get 请求, 采用异步
// 4: 配置参数
xhr.timeout = 3000 // 设置 xhr 请求的超时时间
xhr.responseType = "text" // 设置响应返回的数据格式
xhr.setRequestHeader("a","xxxx")
// 5: 发送请求
xhr.send();
}
坑
浏览器为了安全考虑做了很多请求的限制
- 跨域问题:浏览器必须是同源同域才能正常访问。
即 www.xxx.com:8088 不能访问 www.xxx.com
Access to XMLHttpRequest at 'https://www.xxx.com/' from origin 'https://www.xxx.com:8088' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
- 本地是https访问http 提示报错
即 www.xxx.com:8088 不能访问 www.xxx.com/a.jpg , 反过来http访问https可以,但是会提示警告
Mixed Content: The page at 'https://www.xxx.com:8088' was loaded over HTTPs, but requested an
insecure XMLHttpRequest endpoint "http://www.xxx.com/a.jpg". This request has been
blocked; the content must be served over HTTPS.
4.微任务
宏任务
宏任务的缺点
宏任务不能连续执行
function fun1(){
console.log(1)
setTimeout(fun2)
}
function fun2(){
console.log(2)
}
setTimeout(fun1,0)
//执行完fun1 后,fun2并不是紧跟着执行,中间会穿插很多别的宏任务
产生宏任务的来源
- 渲染事件(如解析 DOM、计算布局、绘制)
- 用户交互事件(如鼠标点击、滚动页面、放大缩小等)
- JavaScript 脚本执行事件
- 网络请求完成、文件读写完成事件
每个宏任务都包含一个微任务队列
微任务
产生微任务的方法
- mutationObserver 监听dom变化事件
- Promise 调用resolve() 和 reject()触发
微任务队列保存在全局执行上下文
当全局执行上下文快退出前,执行所有微任务队列
微任务执行时机
在当前主函数执行之后,当前宏任务结束之前
JavaScript 引擎退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。
- 当全局函数执行完后
- 开始检查并执行微任务队列(执行过程中可能又新创建新的微任务)
- 全部执行后,退出全局执行上下文
监听DOM变化的函数
- 1. Mutation Event
- 同步实时回调,原理是宏任务回调,观察者模式,会阻塞后面的宏任务执行
-
- MutationObserver
- 节流的效果,最后一次才触发异步回调,合并之前的所有操作在集合里,防止重复执行。
- 放入微任务队列,实现了实时调用性
- MutationObserver
思考题
function executor(resolve, reject) {
let rand = Math.random();
console.log(1)
console.log(rand)
if (rand > 0.5)
resolve()
else
reject()
}
var p0 = new Promise(executor);
var p1 = p0.then((value) => {
console.log("succeed-1")
return new Promise(executor)
})
var p3 = p1.then((value) => {
console.log("succeed-2")
return new Promise(executor)
})
var p4 = p3.then((value) => {
console.log("succeed-3")
return new Promise(executor)
})
p4.catch((error) => {
console.log("error")
})
console.log(2)
//当随机数都在0.5以上
1
0.8480186061167625
2
succeed-1
1
0.64485682360380578
succeed-2
1
0.74485682360380578
succeed-3
1
0.94485682360380578
//当随机数 有0.5以下
1
0.8480186061167625
2
succeed-1
1
0.24485682360380578
error
5.Promise
异步编程的缺点
1.代码不连续问题
// 回调方法 逻辑居然写在的下面调用的前面
function onResolve(response){console.log(response) }
function onReject(error){console.log(error) }
let xhr = new XMLHttpRequest()
xhr.ontimeout = function(e) { onReject(e)}
xhr.onerror = function(e) { onReject(e) }
xhr.onreadystatechange = function () { onResolve(xhr.response) }
// 设置请求类型,请求 URL,是否同步信息
let URL = 'https://www.xxx.com'
xhr.open('Get', URL, true);
// 设置参数
xhr.timeout = 3000 // 设置 xhr 请求的超时时间
xhr.responseType = "text" // 设置响应返回的数据格式
xhr.setRequestHeader("a","abc")
// 发出请求
xhr.send();
优化方案 封装
封装url方法 和 原有的xmlhttprequest方法,只需要传入两个对应的回调函数
//封装url请求
//makeRequest 用来构造 request 对象
function makeRequest(request_url) {
let request = {
method: 'Get',
url: request_url,
headers: '',
body: '',
credentials: false,
sync: true,
responseType: 'text',
referrer: ''
}
return request
}
//封装 xmlhttprequest 方法
//[in] request,请求信息,请求头,延时值,返回类型等
//[out] resolve, 执行成功,回调该函数
//[out] reject 执行失败,回调该函数
function XFetch(request, resolve, reject) {
let xhr = new XMLHttpRequest()
xhr.ontimeout = function (e) { reject(e) }
xhr.onerror = function (e) { reject(e) }
xhr.onreadystatechange = function () {
if (xhr.status = 200)
resolve(xhr.response)
}
xhr.open(request.method, URL, request.sync);
xhr.timeout = request.timeout;
xhr.responseType = request.responseType;
// 补充其他请求信息
//...
xhr.send();
}
//调用
XFetch(makeRequest('https://www.test.com'),
function resolve(data) {
console.log(data)
}, function reject(e) {
console.log(e)
})
缺点:代码开始出现嵌套的关系
当嵌套够深就变成地狱回调
地狱回调例子
//上面例子如果多次调用就会出现地狱回调
XFetch(makeRequest('https://www.xxx.com/a'),
function resolve(response) {
console.log(response)
XFetch(makeRequest('https://www.xxx.com/b'),
function resolve(response) {
console.log(response)
XFetch(makeRequest('https://www.xxx.com/c')
function resolve(response) {
console.log(response)
}, function reject(e) {
console.log(e)
})
}, function reject(e) {
console.log(e)
})
}, function reject(e) {
console.log(e)
}
)
地狱回调的两个问题
- 嵌套调用
- 任务的结果不确定,成功与失败组成的分支会有很多
Promise解决了什么
- 消灭嵌套调用
- Promise 实现了回调函数的延时绑定
- 用链式调用的方式解决嵌套问题
// 创建 Promise 对象 x1,并在 executor 函数中执行业务逻辑
function executor(resolve, reject){
resolve(100)
}
let x1 = new Promise(executor)
//x1 延迟绑定回调函数 onResolve
function onResolve(value){
console.log(value)
}
x1.then(onResolve)
2.合并多个任务的错误处理
- 需要将回调函数 onResolve 的返回值穿透到最外层
使用方式
包装返回一个promise对象
function XFetch(request) {
function executor(resolve, reject) {
let xhr = new XMLHttpRequest()
xhr.open('GET', request.url, true)
xhr.ontimeout = function (e) { reject(e) }
xhr.onerror = function (e) { reject(e) }
xhr.onreadystatechange = function () {
if (this.readyState === 4) {
if (this.status === 200) {
resolve(this.responseText, this)
} else {
let error = {
code: this.status,
response: this.response
}
reject(error, this)
}
}
}
xhr.send()
}
return new Promise(executor) //这里封装返回一个promise对象
}
//调用
var x1 = XFetch(makeRequest('https://www.test.com/?category'))
var x2 = x1.then(value => {
console.log(value)
return XFetch(makeRequest('https://www.test.com/column'))
})
var x3 = x2.then(value => {
console.log(value)
return XFetch(makeRequest('https://www.test.com'))
})
x3.catch(error => { //通过统一的catch合并所有错误,业务代码只关心正确的流程
console.log(error)
})
实现2个效果
1.回调函数的延迟绑定,实现代码嵌套执行变成 按序执行,并且回调方法定义也可以写在调用的后面书写。
// 创建 Promise 对象 x1,并在 executor 函数中执行业务逻辑
function executor(resolve, reject){
resolve(100)
}
let x1 = new Promise(executor)
//x1 延迟绑定回调函数 onResolve
function onResolve(value){
console.log(value)
}
x1.then(onResolve)
- 把成功回调函数包装成Promise 返回,穿透到最外层,实现异常外抛, catch只是语法糖,最终等价于
Promise.proptotype.catch = function(callback) {
return this.then(null,callback)
}
例子
这里的catch可以捕获全部catch。 因为 Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被 onReject 函数处理或 catch 语句捕获为止
function executor(resolve, reject) {
resolve()
}
function executor2(resolve, reject) {
reject()
}
var p0 = new Promise(executor);
var p1 = p0.then((value) => {
console.log("succeed-1")
return new Promise(executor)
})
var p3 = p1.then((value) => {
console.log("succeed-2")
return new Promise(executor)
})
var p4 = p3.then((value) => {
console.log("succeed-3")
return new Promise(executor2)
})
p4.catch((error) => {
console.log("error")
})
console.log(2)
promise最终代码
function MyPromise(executor) {
var onResolve_ = null
var onReject_ = null
// 模拟实现 resolve 和 then,暂不支持 rejcet
this.then = function (onResolve, onReject) {
onResolve_ = onResolve
};
function resolve(value) { //函数resolve 作为一等公民做参数 被下面executor调用
setTimeout(()=>{// 这里利用宏任务的特性实现延迟执行,注释会报错
onResolve_(value)
},0)
}
executor(resolve, null);//这里的resole 参数是 通过后面的then传入
}
function executor(resolve, reject) {
resolve(100) //按正常执行,这里会立即执行,也就是 MyPromise 内部已经定义好的resolve方法。
}
//实例化Mypromise 就会立马执行executor
let demo = new MyPromise(executor) //这里分离的嵌套的函数,实现再下面代码才定义和绑定
function onResolve(value){
console.log(value)
}
demo.then(onResolve) //这里的回调延迟到调用then才绑定
实际的js引擎, 会把Promise中的 setTimeout方法改回微任务,提高实时调用的效率。
思考题
1. Promise 中为什么要引入微任务?
- 提高实时调用性,并且通过promise可以实现更精细的执行控制
- 任务执行更加紧凑
2. Promise 中是如何实现回调函数返回值穿透的?
是通过包装一个新的promise一直返回实现,如果是普通数据类型,也会包一层promise,当没有定义reject方法,也会继续往上抛。
3. Promise 出错后,是怎么通过“冒泡”传递给最后那个捕获异常的函数?
通过把所有返回的包装成promise,错误也包装成promise,
function getData() {
return new Promise((resolve, reject) => {
// 模拟网络请求,随机时间后返回数据或者错误
setTimeout(() => {
const rand = Math.random()
if (rand > 0.5) {
resolve('data') // 数据获取成功
} else {
reject(new Error('network error')) // 数据获取失败
}
}, Math.random() * 1000)
})
}
getData()
.then(data => {
console.log(`data is ${data}`)
// 数据解析、计算等操作
})
.catch(error => {
console.log(`error is ${error}`)
// 错误处理操作
throw error // 抛出错误,继续向上传递
})
.then(() => {
console.log('Promise is resolved')
})
.catch(error => {
console.log(`catch error is ${error}`)
// 继续处理后续错误
});
4. 上面封装的promise的xhr请求,还是宏任务吗?
是的 最终触发的xhr.onreadystatechange是走宏任务触发,但是自己定义的promise会走微任务
自定义组件如何监听dom并作出响应
- 通过settimeout 和 setIinterval主动查询dom是否变化
- mutation event
- 通过观察者模式实时,同步执行代码
- 容易导致其他动画卡顿
- MutationObsever
- 通过微任务执行dom响应事件
6. async/await
使用同步的方式去写异步代码
promise的缺陷
上面的promise当比较多的时候,还不是很直观,依然存在嵌套关系。
fetch('https://www.test.com')
.then((response) => {
console.log(response)
return fetch('https://www.test.com/a')
}).then((response) => {
console.log(response)
}).catch((error) => {
console.log(error)
})
使用async/await优化后
es7新特性,可以实现线性思维的编码
async function foo(){
try{
let response1 = await fetch('https://www.test.com')
console.log('response1')
console.log(response1)
let response2 = await fetch('https://www.test.com/a')
console.log('response2')
console.log(response2)
}catch(err) {
console.error(err)
}
}
foo()
生成器(Generator)
可以控制一个函数断点执行,不一次性执行完所有代码。
- 遇到关键字 yield ,会返回yield后面变量和控制权给外部函数,把当前函数挂起。
- 外部函数使用 .next()方法进行方法继续执行
例子
function* genDemo() {
console.log(" 开始执行第一段 ")
yield 'generator 2'
console.log(" 开始执行第二段 ")
yield 'generator 2'
console.log(" 开始执行第三段 ")
yield 'generator 2'
console.log(" 执行结束 ")
return 'generator 2'
}
console.log('main 0')
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')
协程
协程概念
- 协程是一种比线程更加轻量级的存在
- 如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。
- 一个线程也可以拥有多个协程
- 协程不受操作系统影响,用户态自己控制,不会出现切换导致的性能问题
执行逻辑
- 通过调用生成器函数 genDemo 来创建一个协程 gen,创建之后,gen 协程并没有立即执行。
- 要让 gen 协程执行,需要通过调用 gen.next。
- 当协程正在执行的时候,可以通过 yield 关键字来暂停 gen 协程的执行,并返回主要信息给父协程。
- 如果协程在执行期间,遇到了 return 关键字,那么 JavaScript 引擎会结束当前协程,并将 return 后面的内容返回给父协程。
特点
- gen 协程和父协程是在主线程上交互执行的,并不是并发执行的,它们之前的切换是通过 yield 和 gen.next 来配合完成的。
- 当在 gen 协程中调用了 yield 方法时,JavaScript 引擎会保存 gen 协程当前的调用栈信息,并恢复父协程的调用栈信息。同样,当在父协程中执行 gen.next 时,JavaScript 引擎会保存父协程的调用栈信息,并恢复 gen 协程的调用栈信息。
生成器+promise
//foo 函数
function* foo() {
let response1 = yield fetch('https://www.test.com')
console.log('response1')
console.log(response1)
let response2 = yield fetch('https://www.test.com/a')
console.log('response2')
console.log(response2)
}
// 执行 foo 函数的代码
let gen = foo()
function getGenPromise(gen) {
return gen.next().value
}
getGenPromise(gen).then((response) => {
console.log('response1')
console.log(response)
return getGenPromise(gen)
}).then((response) => {
console.log('response2')
console.log(response)
})
- 首先执行的是let gen = foo(),创建了 gen 协程。
- 然后在父协程中通过执行 gen.next 把主线程的控制权交给 gen 协程。
- gen 协程获取到主线程的控制权后,就调用 fetch 函数创建了一个 Promise 对象 response1,然后通过 yield 暂停 gen 协程的执行,并将 response1 返回给父协程。
- 父协程恢复执行后,调用 response1.then 方法等待请求结果。
- 等通过 fetch 发起的请求完成之后,会调用 then 中的回调函数,then 中的回调函数拿到结果之后,通过调用 gen.next 放弃主线程的控制权,将控制权交 gen 协程继续执行下个请求。
co 框架
封装执行生成器
function* foo() {
let response1 = yield fetch('https://www.test.com')
console.log('response1')
console.log(response1)
let response2 = yield fetch('https://www.test.com/a')
console.log('response2')
console.log(response2)
}
co(foo());
async/await
async
async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。
async function foo() {
return 2
}
console.log(foo()) //foo() 等价于 Promise {<resolved>: 2}
await
async function foo() {
console.log(1)
let a = await 100
console.log(a)
console.log(2)
}
console.log(0)
foo()
console.log(3)
//打印结果
0
1
3
100
2
await 100 的含义
await 100
//等价于
let promise_ = new Promise((resolve,reject){
resolve(100)
})
执行流程
- 在这个 promise_ 对象创建的过程中,我们可以看到在 executor 函数中调用了 resolve 函数,
- JavaScript 引擎会将该任务提交给微任务队列
- 然后 JavaScript 引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将 promise_ 对象返回给父协程。
- 主线程的控制权已经交给父协程了,这时候父协程要做的一件事是调用 promise_.then 来监控 promise 状态的改变。
- 接下来继续执行父协程的流程,这里我们执行console.log(3),并打印出来 3。
- 随后父协程将执行结束,在结束之前,会进入微任务的检查点,然后执行微任务队列,
- 微任务队列中有resolve(100)的任务等待执行,执行到这里的时候,会触发 promise_.then 中的回调函数
- 该回调函数被激活以后,会将主线程的控制权交给 foo 函数的协程,并同时将 value 值传给该协程。
- foo 协程激活之后,会把刚才的 value 值赋给了变量 a,然后 foo 协程继续执行后续语句,执行完成之后,将控制权归还给父协程。
思考题
async function foo() {
console.log('foo')
}
async function bar() {
console.log('bar start')
await foo() //注意这里还是会先执行 打印里面的foo
console.log('bar end')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
bar();
new Promise(function (resolve) {
console.log('promise executor')//定义内的参数等于执行
resolve();
}).then(function () {
console.log('promise then')
})
console.log('script end')
//打印结果
//script start
//bar start
//foo
//promise executor
//script end
//bar end
//promise then
//setTimeout
参考
转载自:https://juejin.cn/post/7221422131858014263