likes
comments
collection
share

浏览器原理与实现-学习4-消息循环/setTimeout/微任务/Promise/async-await

作者站长头像
站长
· 阅读数 3

1.消息队列与实践循环

单线程处理任务的3个渐进方案

第一版

浏览器原理与实现-学习4-消息循环/setTimeout/微任务/Promise/async-await 逐行执行的缺点

  • 必须提前设定好,无法中途新增
var a = 1;
var b = 1;
console.log(a)
console.log(b)
console.log(a+b)

第二版

浏览器原理与实现-学习4-消息循环/setTimeout/微任务/Promise/async-await

  • 加入while循环+ 获取输入和计算,可以一直获取输入。
  • 引入了循环
  • 引入了事件

缺点 : 不支持外部的事件

while(true) {
    let a = prompt("请输入a:")
    let b = prompt("请输入b:")
    console.log(a+b)
}


第三版

浏览器原理与实现-学习4-消息循环/setTimeout/微任务/Promise/async-await

  • 新增消息队列
  • 循环队列依次去读队列内容,弹出一个读一个


//定义队列结构
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中的 事件循环

浏览器原理与实现-学习4-消息循环/setTimeout/微任务/Promise/async-await

上面第三种方案就是 事件循环机制。

  1. 添加一个消息队列
    • 队列
    • 先进先出
  2. 跨线程处理任务,通过引入io线程统一转发给 渲染主线程
    • 资源加载完回调:来自网络进程 通过IPC传输
    • 鼠标点击回调: 来自浏览器进程 通过IPC传输
  3. 渲染主线程会循环读取消息队列头任务,并执行任务
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) {
 //...
}
  1. 退出策略 添加一个标识,每次循环检查是否可以退出

消息队列类型

  • 内部消息队列
    • 如输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript 定时器
  • 页面相关事件
    • JavaScript 执行、解析 DOM、样式计算、布局计算、CSS 动画

单线程执行的缺点

  • 实时同步执行会严重拖慢后面排队的任务
  • 优先级无法默认排在消息队列尾部,执行时机太晚
  • 监听dom实时变化的函数 Mutation Event
    • 同步太频繁
    • 异步执行时机太晚

解决方案

微任务

  • 每个宏任务都包含一个微任务队列,

  • 每次宏任务执行完之前都会清空微任务,即任何时候,当前的微任务都要先于宏任务执行。

浏览器原理与实现-学习4-消息循环/setTimeout/微任务/Promise/async-await 其实就是在执行完宏任务4时候,把微队列里的所有任务都先执行完,再走下一个宏任务

通过浏览器performance分析

通过圆点录制当前js的整个调用栈情况,分析每一个task,定位出现长时间执行的代码

浏览器原理与实现-学习4-消息循环/setTimeout/微任务/Promise/async-await

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 对比

  1. setTimeout是由前面宏任务决定下一次执行时间,不能保证在指定时间内执行。在动画方面会出现卡顿
  2. requestAnimationFrame是根据浏览器 16.7毫秒的间隔 准时执行,不收宏任务影响。可以很好的实现动画平滑执行。
  3. 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 也是一种异步回调。

包含两种回调

  • 宏任务回调
  • 微任务回调

调试调用栈

  1. 可以通过谷歌浏览器开发者工具,的performance 查看
  2. 通过谷歌浏览器 输入 chrome://tracing/,通过 record记录调用栈信息,并查看

xmlhttprequest

定义

  1. 实现局部获取数据,解决传统页面整页刷新才能获取后端数据(SSR)
  2. 整页不刷用户体验好

流程

  1. 渲染进程 通过IPC 通知网络进程下载
  2. 网络进程下载完 通过IPC通知io线程加入消息队列
  3. 渲染线程挨个取出宏任务执行

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();
}

浏览器为了安全考虑做了很多请求的限制

  1. 跨域问题:浏览器必须是同源同域才能正常访问。

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.
  1. 本地是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 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。

浏览器原理与实现-学习4-消息循环/setTimeout/微任务/Promise/async-await

  1. 当全局函数执行完后
  2. 开始检查并执行微任务队列(执行过程中可能又新创建新的微任务)
  3. 全部执行后,退出全局执行上下文

监听DOM变化的函数

  • 1. Mutation Event
    • 同步实时回调,原理是宏任务回调,观察者模式,会阻塞后面的宏任务执行
    1. 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解决了什么

  1. 消灭嵌套调用
    • 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)
  1. 把成功回调函数包装成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并作出响应

  1. 通过settimeout 和 setIinterval主动查询dom是否变化
  2. mutation event
    • 通过观察者模式实时,同步执行代码
    • 容易导致其他动画卡顿
  3. 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)

可以控制一个函数断点执行,不一次性执行完所有代码。

  1. 遇到关键字 yield ,会返回yield后面变量和控制权给外部函数,把当前函数挂起。
  2. 外部函数使用 .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 协程的父协程。
  • 一个线程也可以拥有多个协程
  • 协程不受操作系统影响,用户态自己控制,不会出现切换导致的性能问题

执行逻辑

  1. 通过调用生成器函数 genDemo 来创建一个协程 gen,创建之后,gen 协程并没有立即执行。
  2. 要让 gen 协程执行,需要通过调用 gen.next。
  3. 当协程正在执行的时候,可以通过 yield 关键字来暂停 gen 协程的执行,并返回主要信息给父协程。
  4. 如果协程在执行期间,遇到了 return 关键字,那么 JavaScript 引擎会结束当前协程,并将 return 后面的内容返回给父协程。

特点

  1. gen 协程和父协程是在主线程上交互执行的,并不是并发执行的,它们之前的切换是通过 yield 和 gen.next 来配合完成的。
  2. 当在 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)
})
  1. 首先执行的是let gen = foo(),创建了 gen 协程。
  2. 然后在父协程中通过执行 gen.next 把主线程的控制权交给 gen 协程。
  3. gen 协程获取到主线程的控制权后,就调用 fetch 函数创建了一个 Promise 对象 response1,然后通过 yield 暂停 gen 协程的执行,并将 response1 返回给父协程。
  4. 父协程恢复执行后,调用 response1.then 方法等待请求结果。
  5. 等通过 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

参考

time.geekbang.org/column/intr…