前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >浏览器工作原理 - 页面循环系统

浏览器工作原理 - 页面循环系统

作者头像
Cellinlab
发布2023-05-17 14:33:12
发布2023-05-17 14:33:12
68800
代码可运行
举报
文章被收录于专栏:Cellinlab's BlogCellinlab's Blog
运行总次数:0
代码可运行

消息队列和事件循环

每个渲染进程都有一个主线程,并且主线程很忙,既要处理 DOM,又要计算样式,还要处理布局,同时还要处理 JavaScript 任务及各种输入事件。要让这么多不同类型的任务在主线程中顺利执行,需要一个系统来统筹调度这些任务 —— 消息队列和事件循环系统。

使用单线程处理安排好的任务

假设有一系列任务:

  • Task 1: 1 + 2
  • Task 2: 20 / 5
  • Task 3: 7 * 8
  • Task 4: 输出 Task 1、Task 2、Task 3 的结果

用一个线程去执行任务:

代码语言:javascript
代码运行次数:0
运行
复制
void MainThread () {
  int num1 = 1 + 2;
  int num2 = 20 / 5;
  int num3 = 7 * 8;
  printf ("Tasks result: %d,%d,%d", num1, num2, num3);
}

上述代码中,将所有任务代码按照顺序写进主线程,等线程执行时,任务按顺序在线程中一次被执行,等所有任务执行完成后,线程自动退出。

在线程运行过程中处理新任务

实际上并不是所有任务都是在执行之前统一安排好的,大部分情况下,新任务是在线程运行过程中产生的。

要想在线程运行过程中,能接受被执行新的任务,就需要采用时间循环机制。

代码语言:javascript
代码运行次数:0
运行
复制
int GetInput () {
  int input_number = 0;
  count<<"请输入一个数字:";
  cin>>input_number;
  return input_number;
}

void MainThread () {
  for (;;) {
    int first_num = GetInput();
    int second_num = GetInput();
    result_num = first_num + second_num;
    print("结果是:%d", result_num);
  }
}

相比第一版,做了两点改进:

  • 引入了循环机制,在最后加了for 循环,线程会一直循环执行
  • 引入了事件,在线程运行过程中,等待用户输入的数字,等待过程中线程处于暂停状态,一旦接收到用户输入,线程就会被激活,然后执行运算输出结果

处理其他线程发送过来的任务

渲染线程会频繁接收到来自于 IO 线程的一些任务,接收任务之后,渲染线程就要着手处理,如收到资源加载完成的消息后,渲染线程就要开始进行 DOM 解析等。

如何设计一个线程模型,能够接收其他线程发送的消息呢?一个通用的模式是消息队列

消息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点,即要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。

使用消息队列,对线程模型进行优化:

  1. 添加一个消息队列
  2. IO 线程中生产的新任务添加进消息队列的尾部
  3. 渲染主线程会循环地从消息队列中取出任务,然后执行

构造消息队列:

代码语言:javascript
代码运行次数:0
运行
复制
class TaskQueue {
  public:
  Task takeTask(); // 从队列中取出任务
  void pushTask(Task task); // 添加任务到队列的尾部
}

改造主线程,让主线程从队列中读取任务:

代码语言:javascript
代码运行次数:0
运行
复制
TaskQueue task_queue;

void ProcessTask();

void MainThread () {
  for (;;) {
    Task task = task_queue.takeTask();
    ProcessTask(task);
  }
}

其他线程想要发送任务让主线程去执行,只要将任务添加到消息队列中就可以:

代码语言:javascript
代码运行次数:0
运行
复制
Task clickTask;

task_queue.pushTask(clickTask);

由于多个线程操作同一个消息队列,所以在添加任务和取出任务时还会加同步锁。

处理其他进程发送过来的任务

可以使用消息队列,实现线程之间的消息通信。在 Chrome 中,跨进程之间的任务也是频繁发生的:

渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,收到消息后,会将这些消息组装成任务发送给渲染主线程处理。

消息队列中的任务类型

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

以上事件都是在主线程中执行的,所以在编写 Web 应用时,需要衡量这些事件所占用的时长,并想办法解决单个任务占用主线程过久的问题。

如何安全退出

当页面主线程执行完成后,确定要退出页面时,页面主线程会设置一个退出标志的变量,在每次执行完一个任务时,判断是否有设置退出标志。如有设置,就直接终端当前的所有任务,退出线程。

代码语言:javascript
代码运行次数:0
运行
复制
TaskQueue task_queue;
void ProcessTask();
bool keep_running = true;

void MainThread () {
  for (;;) {
    Task task = task_queue.takeTask();
    ProcessTask(task);
    if (!keep_running) {
      break;
    }  
  }
}

页面使用单线程的缺点

  1. 如何处理高优先级的任务
    • 典型场景是监控 DOM 节点的变化情况(如节点的插入、修改、删除等动态变化),然后根据变化处理对应业务
      • 通的设计是使用观察者模式,利用 JavaScript 设计一套监听接口,发生变化时,渲染引擎同步调用这些接口
      • 如果 DOM 变化很频繁,每次变化都会调用接口,那当前任务执行时间会被拉长,从而导致执行效率下降
      • 如果将 DOM 变化做成异步的消息时间,添加到消息队列的尾部,那又会影响监控的实时性
    • 为了权衡效率实时性,引入了微任务
      • 把消息队列中的任务称为宏任务,每个宏任务都包含一个微任务队列
      • 在执行宏任务的过程中,如果 DOM 有变化,那就将该变化添加到微任务列表,这样就不会影响宏任务的执行,解决了执行效率问题
      • 等宏任务中的主要功能直接完成后,渲染引擎不直接去执行下一个宏任务,而是检查当前宏任务中的微任务,如果有微任务,就执行微任务,否则就执行下一个宏任务,解决了实时性问题
  2. 如何解决单个任务执行时长过久的问题
    • 所有的任务都是在单线程中执行的,所以每次只能执行一个任务,而其他任务就都处于等待状态
    • 如果一个任务执行时间过久,那么下一个任务就要等待很长的时间
    • 针对这种情况,JavaScript 任务通过回调来规避这种问题,即让要执行的 JavaScript 任务滞后执行

浏览器页面是如何运行的

可以在 “开发者工具-Performance” 中点击 “start profiling and load page” 观察页面加载过程中的事件执行情况。

Web API:SetTimeout

代码语言:javascript
代码运行次数:0
运行
复制
setTimeout(() => {
  console.log('setTimeout');
}, 200);

浏览器怎么实现 setTimeout

渲染进程中所有运行在主线程上的任务都需要先添加到消息队列中,然后事件循环系统再按照顺序执行消息队列中的任务。典型的事件:

  • 当接收到 HTML 文档数据,渲染引擎就会将 “解析 DOM” 事件添加到消息队列中
  • 当用户改变了 Web 页面窗口大小,渲染引擎会将 “重新布局” 事件添加到消息队列中
  • 当触发了 JavaScript 引擎垃圾回收机制,渲染引擎会将 “垃圾回收” 任务添加到消息队列中
  • 如果要执行一段异步 JavaScript 代码,也需要将执行任务添加到消息队列中

在 Chrome 中除了正常使用的消息队列外,还有一个消息队列,用于维护需要延迟执行的任务列表,包括 定时器 和 Chromium 内部一些需要延迟执行的任务。

当通过 JavaScript 创建一个定时器时,渲染进程会将该定时器的回调任务添加到延迟队列中。

在处理完消息队列中的一个任务后,会执行 ProcessDelayTask 函数,这个函数会根据发起时间和延迟时间计算出到期的任务,然后依次执行到期的任务。等到期任务都执行完成后,再继续下一个循环过程。

设置一个定时器,JavaScript 引擎会返回一个定时器的 ID。通常,当一个定时器任务还没有被执行的时候,也可以取消的,通过 clearTimeout 函数。其原理是,直接根据 ID 去延迟队列中找到对应的任务,并移除。

代码语言:javascript
代码运行次数:0
运行
复制
clearTimeout(timerId);

使用 setTimeout 注意事项

如果当前任务执行时间过久,会影响延迟到期定时器任务的执行

  • 通过 setTimeout 设置的回调任务被放入消息队列中并且等待下一次执行,而不是立即执行
  • 要执行消息队列中的下个任务,需要等待当前任务执行完成,如果当前任务执行时间过久,会影响下个任务的执行时间
代码语言:javascript
代码运行次数:0
运行
复制
function bar () {
  console.log('bar');
}
function foo () {
  setTimeout(bar, 0);
  for (let i = 0; i < 5000; i++) {
    let i = 5 + 8 + 8 + 8;
    console.log(i);
  }
}
foo();

如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 ms

代码语言:javascript
代码运行次数:0
运行
复制
function cb () { 
  setTimeout(cb, 0);
}
setTimeout(cb, 0);
  • 在 Chrome 中,定时器被嵌套调用 5 次以上,系统会判断该函数方法被阻塞了,如果定时器的调用时间间隔小于 4 ms,浏览器会将每次调用的时间间隔设置为 4 ms
  • 所以对于实时性有较高要求的场景中就不适合用 setTimeout

未激活的页面,setTimeout 执行最小间隔是 1000 毫秒

  • 目的是优化后台页面的加载损耗以及降低电量

延时执行时间有最大值

  • 大部分浏览器以 32 个 bit 来存储延时值的,所以最大值为 2^32 / 2 - 1 = 2147483647 ms
  • 如果设置的延迟值大于 2147483647 ms,会溢出,导致定时器被立即执行

使用 setTimeout 设置的回调函数中的 this 不符合直觉

  • 如果 setTimeout 推迟执行的回调函数是每个对象的方法,那么该方法中的 this 将指向全局环境,而不是定义时所在的那个对象
代码语言:javascript
代码运行次数:0
运行
复制
var name = 1;
var MyObj = {
  name: 2,
  showName: function () {
    console.log(this.name);
  }
};
setTimeout(MyObj.showName, 0);
// 1
  • 解决方案
代码语言:javascript
代码运行次数:0
运行
复制
// 使用匿名函数
setTimeout(() => {
  MyObj.showName();
}, 0);

setTimeout(function () {
  MyObj.showName();
}, 0);

// 使用 bind
setTimeout(MyObj.showName.bind(MyObj), 0);

Web API:XMLHttpRequest

回调函数 和 系统调用栈

代码语言:javascript
代码运行次数:0
运行
复制
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);

将函数作为参数传递给另一个函数,那作为参数的这个函数就是回调函数。上面代码中,callback 是在主函数 doWork 返回之前执行的,称同步回调

代码语言:javascript
代码运行次数:0
运行
复制
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);

callback 不在主函数 doWork 内部被调用,而是延迟 1 s,这种回调函数在主函数外部执行的过程为异步回调

从本质上看,消息队列和主线程循环机制保证了页面有条不紊地运行。当循环系统在执行一个任务时,都要为这个任务维护一个系统调用栈,类似于 JavaScript 调用栈。每个任务在执行过程中都有自己的调用栈,那么同步回调就是在当前主函数的上下文中执行回调函数,而异步回调是指在主函数之外执行,一般有两种方式:

  • 把异步函数做成一个任务,添加到消息队列的尾部;
  • 把异步函数添加到微任务队列中,这样可以在当前任务的末尾处执行微任务。

XMLHttpRequest 运作机制

代码语言:javascript
代码运行次数:0
运行
复制
function GetWebData (URL) {
  let xhr = new XMLHttpRequest();

  xhr.onreadystatechange = function () {
    switch (xhr.readyState) {
      case 0:
        console.log('请求未初始化');
        break;
      case 1:
        console.log('OPENED');
        break;
      case 2:
        console.log('HEADERS_RECEIVED');
        break;
      case 3:
        console.log('LOADING');
        break;
      case 4:
        if (this.status == 200 || this.status == 304) {
          console.log(this.responseText);
        }
        console.log('DONE');
        break;
    }
  };

  xhr.ontimeout = function () {
    console.log('请求超时');
  };
  xhr.onerror = function () {
    console.log('请求失败');
  };

  xhr.open('GET', URL, true); // true 表示异步

  xhr.timeout = 5000; // 设置超时时间
  xhr.responseType = 'text'; // 设置响应类型
  xhr.setRequestHeader('X_TK', 'cellinlab.xyz'); // 设置请求头

  xhr.send();
}

利用 XMLHttpRequest 请求数据的执行过程:

  1. 创建 XMLHttpRequest 对象
  2. 为 xhr 对象注册回调函数
    • 因为网络请求比较耗时,所以注册回调函数,等任务执行完成后调用回调通知结果
    • XMLHttpRequest 的主要回调函数
      • ontimeout
      • onerror
      • onreadystatechange,监控后台请求过程中的状态,如 HTTP 头加载完成的消息、HTTP 响应体以及数据加载完成的消息
  3. 配置基础的请求信息
    • 请求地址
    • 请求方法
    • 请求方式,同步还是异步
    • 设置响应格式,用于将服务器返回的数据自动转换为自己想要的格式
  4. 发起请求

XMLHttpRequest 使用过程中的注意事项

  1. 跨域问题
    • 默认情况下,跨域请求是不被允许的
  2. HTTPS 混合内容的问题
    • HTTPS 混合内容是 HTTPS 页面中包含了不符合 HTTPS 安全要求的内容,如 HTTP 资源
    • 通常,如果 HTTPS 请求页面中使用混合内容,浏览器会针对 HTTPS 混合内容显示警告,虽然警告但大部分可以加载,可用
    • 但是,如果使用 XMLHttpRequest 请求时,浏览器认为这种请求可能是攻击者发起会阻止此类危险请求

宏任务和微任务

微任务可以在实时性和效率之间做一个有效的权衡。

宏任务

页面中大部分任务都是在主线程上执行的,包括:

  • 渲染事件(如解析 DOM、计算布局、绘制)
  • 用户交互事件(如鼠标点击、滚动页面、放大缩小等)
  • JavaScript 脚本执行事件
  • 网络请求完成、文件读写完成事件

为了协调这些任务有条不紊在主线程上执行,页面进程引入了消息队列和事件循环,渲染进程内部会维护多个消息队列,如延迟执行队列和普通消息队列。然后,主线程采用 for 循环,不断从队列中取出任务并执行任务,将这些消息队列中的任务称为宏任务

WHATWG (opens new window)中定义的事件循环机制:

  • 先从多个消息队列中选出一个最老的任务 oldTask
  • 然后循环系统记录任务开始执行的时间,并将 oldTask 设置为当前正在执行的任务
  • 当任务执行完成后,删除当前正在执行的任务,并从对应的消息队列中删除掉这个 oldTask
  • 最后统计执行完成的时长等信息

宏任务可以满足大部分日常需求,不过对于时间精度要求较高的需求,宏任务就难以胜任了。因为页面的渲染事件、各种 IO 的完成事件等都随时可能被添加到消息队列,而且添加事件是由系统操作的,JavaScript 代码不能准确掌握任务要添加到队列的位置,控制不了任务在消息队列中的位置,所以很难控制开始执行任务的时间。

微任务

异步回调主要有两种:

  • 把异步回调函数封装成一个宏任务,添加到消息队列尾部,当循环系统执行到该任务的时候执行回调函数,像 setTimeoutXMLHttpRequest 都是通过这种方式实现的
  • 执行时机是在主函数执行结束之后、当前宏任务结束之前执行回调函数,这种通常是以微任务形式体现

微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。

在 V8 引擎层面分析微任务是怎么运转的:

  • JavaScript 执行一段脚本的时候,V8 会为其创建一个全局执行上下文,同时在内部创建一个微任务队列
    • 微任务队列用于存放微任务,因为在当前宏任务的执行过程中,可能会产生很多微任务
    • 微任务队列是给 V8 引擎内部使用的,无法通过 JavaScript 直接访问
  • 微任务产生的时机
    • 使用 MutationObserver 监控 DOM 节点,然后通过 JavaScript 修改节点或者为节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务
    • 使用 Promise,当调用 Promise.resolve()Promise.reject() 时,也会产生微任务
  • 执行微任务队列的时机
    • 通常,在当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局上下文的微任务队列,并按顺序执行队列中的微任务
    • WHATWG 将执行微任务的时间点称为检查点
    • 除了在退出全局执行上下文这个检查点外,还有其他检查点
  • 如果在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务队列中,V8 引擎一致循环执行微任务队列中的微任务,直到队列为空
    • 即微任务执行过程中产生的新的微任务不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行
  • 关于微任务的一些结论
    • 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列
    • 微任务的执行时长会影响当前宏任务的时长,要注意控制微任务时长
    • 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行

监听 DOM 变化方法的演变

许多 Web 应用都利用 HTML 与 JavaScript 构建其自定义控件,与一些内置控件不同,为了与内置控件一起良好工作,这些控件必须能后适应内容更改、响应事件和用户交互。因此,Web 应用需要监视 DOM 变化并及时地做出响应

早期页面并没有提供监听的支持,方案是使用轮询监测。直到 2000 年,有了 Mutation Event,它采用了观察者的设计模式,当 DOM 有变动时,会立刻触发相应事件,这种方式属于同步回调

采用 Mutation Event 解决了实时性的问题,却因此带来了严重的性能问题,因为一旦 DOM 发生变化,就会调用 JavaScript,产生巨大的性能开销。后来,慢慢被移除。

为了解决 Mutation Event 由于同步调用导致的页面性能问题,从 DOM 4 开始,推荐使用 MutationObserver 代替 Mutation Event。MutationObserver API 可以用来监视 DOM 的变化,包括属性的变化、节点的增减、内容的变化等。

MutationObserver 做了以下改进:

  • 将响应函数改成异步调用,可以不用在每次 DOM 变化都触发异步调用,而是等多次 DOM 变化后,一次触发异步调用
    • 并且会使用一个数据结构来记录这期间所有 DOM 变化
    • 这样即使频繁地操纵 DOM,也不会对性能造成太大的影响
  • 在每次 DOM 节点发生变化的时候,渲染引擎将变化记录封装成微任务,并将微任务添加到当前的微任务队列中,实现消息通知的实时性

即,MutationObserver 采用了 “异步 + 微任务” 的策略:

  • 通过异步解决了同步操作的性能问题
  • 通过微任务解决了实时性问题

Promise

异步编程的问题

页面中任务都是执行在主线程之上的,相对于页面来说,主线程就是它的整个的世界,所以在执行一项耗时的任务时,如下载网络文件任务、获取摄像头等设备信息任务,这些任务都会放到页面主线程之外的进程或者线程中去执行,这样就避免了耗时任务“霸占”页面主线程的问题。

Web 页面的单线程架构决定了异步回调,而异步回调又影响编码方式。

代码语言:javascript
代码运行次数:0
运行
复制
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); };

let URL = `https://cellinlab.xyz`;
xhr.open('GET', URL, true);

xhr.timeout = 5000;
xhr.responseType = 'text';
xhr.setRequestHeader('X_TK', '123');

xhr.send();

过多的回调会导致代码的逻辑不连贯、不线性,不符合人的直觉,这就是异步回调影响我们的编码方式。

封装异步代码,让处理流程变得线性

由于重点关注的是输入内容(请求信息)输出内容(响应信息),至于中间的异步请求过程,不想在代码中体现太多,会干扰代码逻辑,可以将请求过程封装起来。

代码语言:javascript
代码运行次数:0
运行
复制
function makeRequest (request_url) {
  let request = {
    method: 'GET',
    url: request_url,
    headers: '',
    body: '',
    credentials: false,
    sync: true,
    responseType: 'text',
    referrer: '',
    timeout: 5000,
  };
  return request;
}
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.readyState === 4) {
      if (xhr.status === 200) {
        resolve(xhr.response);
      } else {
        reject(xhr.status);
      }
    }
  };
  xhr.open(request.method, request.url, request.sync);
  xhr.timeout = request.timeout;
  xhr.responseType = request.responseType;

  xhr.send();
}

XFetch(makeRequest('https://cellinlab.xyz'), function (response) {
  console.log(response);
}, function (error) {
  console.log(error);
});

回调地狱

使用上面的方法,可以将代码思路更加线性化,可以应用在一些简单场景中。但是对于一些复杂项目,如果嵌套太多的回调容易陷入回调地狱。会导致:

  • 嵌套调用,下面的任务依赖上个任务的请求结果,并在上个任务的回调函数内部执行新的业务逻辑,导致可读性变差
  • 任务的不确定性,执行每个任务都可能失败或成功,需要在每个任务进行两种预判,对每个任务进行一次额外的错误处理,增加了代码的混乱程度

从问题出发,可以从下面入手解决:

  • 消灭嵌套调用
  • 合并多个任务的错误处理

Promise:消灭嵌套调用和多次错误处理

使用 Promise 重构 XFetch

代码语言:javascript
代码运行次数:0
运行
复制
function XFetch (request) {
  function executor (resolve, reject) {
    let xhr = new XMLHttpRequest();
    xhr.ontimeout = function (e) { reject(e); };
    xhr.onerror = function (e) { reject(e); };
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          resolve(xhr.responseText, xhr);
        } else {
          reject(xhr.status, xhr);
        }
      }
    };
    xhr.open(request.method, request.url, request.sync);
    xhr.timeout = request.timeout;
    xhr.responseType = request.responseType;

    xhr.send();
  }
  return new Promise(executor);
}

var x = XFetch(makeRequest('https://cellinlab.xyz/x'));
var y = x.then(value => {
  console.log(value);
  return XFetch(makeRequest('https://cellinlab.xyz/y'));
});
var z = y.then(value => {
  console.log(value);
  return XFetch(makeRequest('https://cellinlab.xyz/z'));
});
z.catch(error => {
  console.log(error);
});

Promise 主要通过下面两步来解决嵌套回调问题;

Promise 实现了回调函数的延时绑定

  • 先创建 Promise 对象 x1 ,通过 Promise 的构造函数 executor 来执行业务逻辑
  • 创建好 x1 之后,再使用 x1.then 来设置回调函数
代码语言:javascript
代码运行次数:0
运行
复制
// 创建 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);

需要将回调函数 onResolve 的返回值穿透到最外层

  • 会根据 onResolve 函数的传入值来决定创建什么类型的 Promise 任务,创建好的 Promise 对象需要返回到最外层,这样就摆脱嵌套循环了

Promise 处理异常的方法:

  • Promise 对象的错误具有“冒泡”性质,会一直往后传递,直到被 onReject 处理或 catch 语句捕获位置

Promise 与 微任务

代码语言:javascript
代码运行次数:0
运行
复制
function executor (resolve, reject) {
  resolve(100);
}
let demo = new Promise(executor);

function onResolve (value) {
  console.log(value);
}
demo.then(onResolve);

上面代码的执行细节;

  1. new Promise(executor) 时,Promise 的构造函数会被执行,不过由于 Promise 是 V8 引擎提供,暂时看不到 Promise 构造函数细节
  2. 构造函数调用 Promise 的参数 executor 函数,然后在 executor 中执行了 resolve
    • resolve 函数也是 V8 内部实现的,执行 resolve 函数会触发 demo.then 设置的回调函数 onResolve
    • 注意,由于 Promise 采用了回调函数延迟绑定技术,所以在执行 resolve 函数的时候,回调函数还没有绑定,所以只能推迟回调函数的执行
代码语言:javascript
代码运行次数:0
运行
复制
function MyPromise (executor) {
  var _onResolve = null;
  var _onReject = null;

  this.then = function (onResolve, onReject) {
    _onResolve = onResolve;
    _onReject = onReject;
  };

  function resolve (value) {
    setTimeout(function () {
      _onResolve(value);
    }, 0);
  }

  // ...

  executor(resolve);
}

Async / Await

Promise 能很好地解决回调地狱问题,但是这种方法充满了 Promise 的 then() 方法,如果处理流程比较复杂,那么整段代码会充斥 then,语义化不明显。

基于上述原因,ES7 引入了 asyncawait 语法,它们可以让代码更加简洁,更加语义化,这是 JavaScript 异步编程的一个重大改进,提供了在不阻赛主线程的情况下使用同步代码实现异步访问资源的能力,并且能使得代码逻辑更加清晰。

生成器 和 协程

生成器函数是一个带星号的函数,而且可以暂停执行和恢复执行。

代码语言:javascript
代码运行次数:0
运行
复制
function* genDemo () {
  console.log('first yield');
  yield 'generator 2';

  console.log('second yield');
  yield 'generator 2';

  console.log('third yield');
  yield 'generator 2';

  console.log('execute done');
  return 'generator 2';
}

console.log('main 0'); // main 0
let g = genDemo(); 
console.log(g.next().value); // first yield generator 2
console.log('main 1'); // main 1
console.log(g.next().value); // second yield generator 2
console.log('main 2'); // main 2
console.log(g.next().value); // third yield generator 2
console.log('main 3'); // main 3
console.log(g.next().value); // execute done generator 2
console.log('main 4'); // main 4
console.log(g.next().value); // undefined

生成器的使用方式:

  • 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停函数的执行
  • 外部函数可以通过 next 方法恢复函数的执行

要搞清楚函数为何能暂停和恢复,需要了解协程的概念。协程是一种比线程更加轻量级的存在

  • 可以把协程看成是跑在线程上的任务
    • 一个线程上可以存在多个协程
    • 线程上同时只能执行一个协程
    • 可以从 A 协程中启动 B 协程,管 A 协程叫 B 协程的父协程
  • 协程不是被操作系统内核所管理,而完全由程序控制(即用户态执行)
    • 好处就是性能得到了很大的提升,不会像线程切换那样消耗资源

父协程和 genDemo 协程切换调用栈:

在 JavaScript 中,生成器就是协程的一种实现方法,通过使用生成器配合执行器,就能实现用同步的方式写出异步代码,大大增强代码可读性。

Async / Await

ES 7 引入了 async/await,其技术背景就是 Promise 和 生成器应用,或者更加底层的说法是微任务和协程应用

async 是一个通过异步执行隐式返回 Promise作为结果的函数。

代码语言:javascript
代码运行次数:0
运行
复制
// 关于 隐式返回 Promise
async function foo () {
  return 2;
}
console.log(foo()); // Promise {<fulfilled>: 2}

await 又是什么呢?

代码语言:javascript
代码运行次数:0
运行
复制
async function foo () {
  console.log(1);
  let a = await 100; // 此处会 默认创建一个 Promise 对象
  console.log(a);
  console.log(2);
}
console.log(0);
foo();
console.log(3);
// 0
// 1
// 3
// 100
// 2
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/5/10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 消息队列和事件循环
    • 使用单线程处理安排好的任务
    • 在线程运行过程中处理新任务
    • 处理其他线程发送过来的任务
    • 处理其他进程发送过来的任务
    • 消息队列中的任务类型
    • 如何安全退出
    • 页面使用单线程的缺点
    • 浏览器页面是如何运行的
  • Web API:SetTimeout
    • 浏览器怎么实现 setTimeout
    • 使用 setTimeout 注意事项
  • Web API:XMLHttpRequest
    • 回调函数 和 系统调用栈
    • XMLHttpRequest 运作机制
    • XMLHttpRequest 使用过程中的注意事项
  • 宏任务和微任务
    • 宏任务
    • 微任务
    • 监听 DOM 变化方法的演变
  • Promise
    • 异步编程的问题
    • 封装异步代码,让处理流程变得线性
    • 回调地狱
    • Promise:消灭嵌套调用和多次错误处理
    • Promise 与 微任务
  • Async / Await
    • 生成器 和 协程
    • Async / Await
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档