View on GitHub

Cycle263 Blog

Stay hungry, stay foolish.

浏览器线程

浏览器内核是多线程的,它们在内核控制下相互配合以保持同步,一个浏览器通常由以下常驻线程组成:GUI 渲染线程,javascript 引擎线程,浏览器事件触发线程,定时触发器线程,异步 http 请求线程。

JavaScript主线程

webworker

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但完全受控于主线程。其实就是在Javascript单线程执行的基础上,开启一个子线程,进行任务处理,而不影响主线程的执行,子线程并不支持操作页面的DOM;当子线程执行完毕之后再回到主线程上,在这个过程中并不影响主线程的执行过程。

Web Worker的基本原理就是在当前的主线程中加载一个只读文件来创建一个新的线程,两个线程同时存在,且互不阻塞,并且在子线程与主线程之间提供了数据交换的接口postMessage和onmessage,来进行发送数据和接收数据,其数据格式可以为结构化数据(JSON等)。

webworker

Web Workers规范中有三种实现类型:Dedicated Workers、Shared Workers、Service workers。

WebWorker只属于某个页面,不会和其他页面的Render进程(浏览器内核进程)共享,所以Chrome会创建一个新的线程来运行Worker中的JavaScript;SharedWorker是浏览器所有页面共享的,所以Chrome浏览器为SharedWorker单独创建一个进程来运行JavaScript。

本质上就是进程和线程的区别,SharedWorker由独立的进程管理,WebWorker只是属于render进程下的一个线程。

定时器

当使用setTimeout或setInterval时,需要定时器线程计时,计时完成后就会将特定的事件推入事件队列中。那么为什么要单独的定时器线程?因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确,因此很有必要单独开一个线程用来计时。

执行任务(执行栈)

js

所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

  // 2 3 5 4 1
  setTimeout(function timecb() {   // macrotask
    console.log(1);
  }, 0);
  new Promise(function exec(resolve) {
    console.log(2);
    for( var i=0; i<10000 ; i++ ) {
      i == 9999 && resolve();
    }
    console.log(3);
  }).then(function thencb() {
    console.log(4);   // microtask
  });
  console.log(5);
  |               [code]             |    [call stack]     |     [task queue]    |    [webAPI]    |
  |----------------------------------|---------------------|---------------------|----------------|
  | setTimeout(function timecb() {   |   console.log(2)    |          |          |   setTimeout   |
  |   console.log(1);                |   console.log(3)    |          |          |  promise/then  |
  | }, 0);                           |        exec         |          |          |                |
  | new Promise(function exec(rv){   |   console.log(5)    |          |          |                |
  |   console.log(2);                |   console.log(4)    |          |          |                |
  |   for( var i=0; i<10000 ; i++ ){ |        thencb       |          |          |                |
  |     i == 9999 && rv();           |   console.log(1)    |          |          |                |
  |   }                              |        timecb       |          |          |                |
  |   console.log(3);                |    main/anonymous   |          |          |                |
  | }).then(function thencb() {      |                     |          |          |                |
  |   console.log(4);                |                     |          |          |                |
  | });                              |                     |          |          |                |
  | console.log(5);                  |                     |          |          |                |

栈(stack)中主要存放一些基本类型(Undefined、Null、Boolean、Number 和 String)的值、变量、参数,返回值,函数引用和对象的引用等(基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中),向地址减小的方向增长,可读可写可执行,其优势是存取速度比堆要快,并且栈内的数据可以共享,但缺点是存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

栈中不存在对堆中某个对象的引用,那么就认为该对象已经不再需要,在垃圾回收时就会清除该对象占用的内存空间。因此,在不需要时应该将对对象的引用释放掉(解除引用),以利于垃圾回收,这样就可以提高程序的性能。释放对对象的引用最常用的方法就是为其赋值为null,这种做法适用于大多数全局变量和全局对象的属性。局部变量会在他们离开执行环境时自动被解除引用。解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。

栈有几种含义,不同的语境代表不同的含义,分别包括:

  // JS实现栈代码
  function Stack() {
    // 用数组来模拟栈
    var items = [];

    // 将元素送入栈,放置于数组的最后一位
    this.push = function(element) {
      items.push(element);
    };

    // 弹出栈顶元素
    this.pop = function() {
      return items.pop();
    };

    // 查看栈顶元素
    this.peek = function() {
      return items[items.length - 1];
    }

    // 确定栈是否为空 @return {Boolean} 若栈为空则返回true,不为空则返回false
    this.isAmpty = function() {
      return items.length === 0
    };

    // 清空栈中所有内容
    this.clear = function() {
      items = [];
    };

    // 返回栈的长度 @return {Number} 栈的长度
    this.size = function() {
      return items.length;
    };

    // 以字符串显示栈中所有内容
    this.print = function() {
      console.log(items.toString());
    };
  }

任务队列

浏览器的任务队列都是已经完成的异步操作,任务队列不止一个,还分为 microtasks 和 macrotasks(task队列)、requestAnimationFrame队列、requestIdleCallback队列, 整个的js代码macrotask先执行,同步代码执行完后有microtask执行microtask,没有microtask执行下一个macrotask,如此往复循环至结束。

在ECMAScript中,microtask称为jobs,macrotask可称为task。

microtasks vs macrotasks

运行机制:

  // 解释代码
  if (isEmpty(stack)) {	// 执行栈空了(只剩下全局的),同步任务执行完了
  	for (macroTask of macroTaskQueue) {
    	handleMacroTask();	// 处理宏任务
	
    	for (microTask of microTaskQueue) {
        	handleMicroTask(microTask);		// 处理微任务
    	}								 
  	}
  }

备注:官方的promise和polyfill版的promise两者有很大区别,前者为microtask形式,后者通过setTimeout模拟的macrotask形式。

  async function async1(){ 
    console.log('async1 start');
    /* 遇到await暂时执行,如果await语句后面是异步函数,返回Promise,需要等待Promise状态改变后放入
    /* microtasks queue; 如果await语句后面是同步函数,则直接放入microtasks队列,等待执行栈内的同步代码
    /* 执行完毕,开始按顺序执行microtasks队列的处理函数。
     */
    await async2();
    console.log('async1 end'); 
  } 
  async function async2(){ 
    console.log('async2'); 
  } 
  console.log('script start');
  setTimeout(function(){ 
    console.log('setTimeout');
  }, 0);
  async1(); 
  new Promise(function(resolve){ 
    console.log('promise1');
    resolve(); 
  }).then(function(){ 
    console.log('promise2');
  }); 
  console.log('script end');



协程

协程(coroutine)是一种程序运行的方式,可以理解成“协作的线程”或“协作的函数”。协程既可以用单线程实现,也可以用多线程实现。前者是一种特殊的子例程,后者是一种特殊的线程。

资料参见

JS运行机制全面梳理

JS运行机制详解

js事件循环机制

js中的堆和栈

js调用堆栈概述

stack三种含义