进程与线程

进程与线程:

  • 进程是 CPU 资源分配的最小单位;线程是 CPU 调度的最小单位
  • 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
  • 一个进程的内存空间是共享的,每个线程都可用这些共享内存

多进程与多线程:

  • 多进程:在同一个时间内,计算机允许多个进程处于运行状态。如:听歌的时候聊 QQ,互不影响
  • 多线程:程序中包含多个执行流,即:在一个程序中可以同时运行多个不同的线程来执行不同的任务,允许单个程序创建多个并行执行的线程来完成各自的任务

并行与并发:

  • 并行:宏观上来看,多个程序可以同时运行,但实际是 CPU 执行权限快速切换,其实同一时间只有一个程序在运行,其他程序在阻塞(等待 CPU 的执行权限),但由于切换太快,无法感知
  • 并发:真正的多个程序同时运行,得益于多核 CPU(硬件的支持)

事件循环

浏览器 JavaScript 引擎的两大特点:单线程非阻塞(通过 Event Loop 事件循环机制实现)。

为什么 JavaScript 是单线程?

作为浏览器脚本语言,JavaScript 可以操作 DOM,多线程同时操作 DOM 可能会出现线程安全问题(多个线程竞争一个资源——DOM)

Web Worker 说明

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。

非阻塞

当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如 I/O 事件)时,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调,浏览器 JavaScript 引擎中这个规则就是事件循环机制。

常见的阻塞:alertconfirmprompt

两种任务队列

宏任务(macrotask)队列:

  • script(整体代码)
  • setTimeoutsetInterval
  • requestAnimationFrame
  • I/O 操作
  • UI 渲染、交互事件
  • setImmediate(Node.js 环境)

微任务(microtask)队列:

  • Promise.then
  • Observer(如:MutationObserverIntersectionObserverPerformanceObserver
  • process.nextTick(Node.js 环境)

宏任务队列可以有多个,微任务队列只有一个

为什么要引入微任务的概念,只有宏任务可以吗?

宏任务:先进先出的执行原则

当有高优先级的任务时无法“插队”,所以引入微任务:每个宏任务结束后,都要清空所有的微任务。

浏览器

  1. 执行宏任务队列队头的任务
  2. 将微任务中的所有任务依次执行,清空微任务队列
  3. 执行渲染操作,更新界面
  4. 处理 worker 相关的任务
  5. 重复第一步,如此循环(如果没有任务则等待)

浏览器中的宏任务队列有多个,如:定时器任务队列、网络请求任务队列等,浏览器将根据自己的算法(考虑队列优先级、搁置时间等因素)决定下一个宏任务从哪个队列中取

Node.js

Node.js 采用 V8 作为 JS 的解析引擎,而 I/O 处理方面使用了自己设计的 libuv,libuv 是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的 API,事件循环机制也是其中的实现

libuv 中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
  1. timers(定时器):setTimeout()setInterval() 安排的回调
  2. pending callbacks(挂起的回调):执行推迟到下一个循环迭代的 I/O 回调
  3. idle, prepare(空闲,准备):仅系统内部使用
  4. poll 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(除了关闭、计时器、setImmediate() 之外的几乎所有回调),node 会在适当的时候阻塞在这里
  5. check(检查):setImmediate() 的回调
  6. close callbacks(关闭的回调函数):一些准备关闭的回调函数,如:socket.on('close', ...)

Node.js V10 及以前:

  1. 执行完一个阶段中的所有任务
  2. 执行完 nextTick 队列里的内容
  3. 执行完微任务队列的内容
  4. 重复第一步,如此循环(如果没有任务则结束 JS)

Node.js V11 及以后:和浏览器的行为统一了,都是每执行一个宏任务就执行完微任务队列

总结

  1. 三个空间:执行栈、宏任务队列(可能多个)、微任务队列
  2. 第一次是 script(整体代码)进入执行栈,当在执行过程中遇到异步任务时,会交给浏览器的其他模块进行处理,当异步任务完成(如:计时器结束、网络请求响应返回,Promise 完成),task(回调函数)会放入到任务队列(宏任务队列或微任务队列)之中
  3. 当执行栈代码(同步任务)执行完毕后,依次执行当前微任务队列中的所有任务,之后将宏任务队列的队头任务压入执行栈执行
  4. 重复步骤 3,直到无任务,结束 JS 或等待

实现微任务的方式

promise

在浏览器和 Node.js 环境都支持

Promise.resolve().then(callback);

queueMicrotask

在浏览器和 Node.js 环境都支持,是一个全局的方法,它接受一个回调函数作为参数,并将这个回调函数放入微任务队列中

queueMicrotask(callback)

MutationObserver

MutationObserver 是一个可以用来监视 DOM 变化的 API,仅在浏览器环境可用。当使用 MutationObserver 创建一个观察者并开始观察某个 DOM 元素时,每当这个元素或其子元素发生变化,MutationObserver 就会将提供的回调函数添加到微任务队列

const observer = new MutationObserver(callback);
const element = document.createTextNode("");
observer.observe(element, {
	characterData: true,
});
const change = () => {
	element.data = `${Date.now()}}`;
};
change();

process.nextTick

process.nextTickNode.js 特有的 API,它的回调函数会在当前操作完成后,下一个事件循环开始前立即执行,也是微任务的一种

process.nextTick(callback);

总结

const micros = [
    {
        name: 'queueMicrotask',
        test: () => {
            return typeof queueMicrotask === "function";
        },
        run: (callback: Task) => {
            queueMicrotask(callback);
        },
    },
    {
        name: 'Promise',
        test: () => {
            return typeof Promise !== "undefined";
        },
        run: (callback: Task) => {
            Promise.resolve().then(callback);
        },
    },
    {
        name: 'process.nextTick',
        test: () => {
            return (
                typeof process === "object" && typeof process.nextTick === "function"
            );
        },
        run: (callback: Task) => {
            process.nextTick(callback);
        },
    },
    {
        name: 'MutationObserver',
        test: () => {
            return (
                typeof MutationObserver !== "undefined" && typeof window !== "undefined"
            );
        },
        run: (callback: Task) => {
            const observer = new MutationObserver(callback);
            const element = document.createTextNode("");
            observer.observe(element, {
                characterData: true,
            });
            element.data = `${Date.now()}`;
        },
    },
];
 
export const runMicroTask = (callback: Task) => {
    const runner = micros.find((item) => item.test());
    if (!runner) {
        throw new Error('当前环境不支持微任务');
    }
    runner.run(callback);
};

实现宏任务的方式

MessageChannel

仅浏览器环境

提供了一种在不同文档(例如主线程和 worker 线程,或者不同的 iframes)之间,或者在同一文档的不同部分之间进行直接通信的方式

MessageChannel 有两个属性,port1 和 port2,它们都是 MessagePort 类型的对象。可以在一个地方使用 port1 发送消息,在另一个地方使用 port2 接收消息,反之亦然

const channel = new MessageChannel();
channel.port1.onmessage = callback;
const change = function () {
	channel.port2.postMessage(0);
};
change();

上面的代码中 channel.port2 将一个消息异步地发送到 prot1 端口,然后 channel.port1.onmessage 的回调函数会加入到宏任务队列等待执行

requestAnimationFrame

仅浏览器环境

由浏览器在下一次重绘之前调用传入给该方法的回调函数。由于是和渲染帧同步的,所以可以认为是天然的宏任务

requestAnimationFrame(() => requestAnimationFrame(callback));

调用两次 requestAnimationFrame原因

计时器

在浏览器和 Node.js 环境都支持

  • setTimeout 指定时间后将回调函数插入到任务队列中
  • setInterval 循环指定时间后将回调函数不断的插入到任务队列中
setTimeout(callback, 1000)
 
setInterval(callback, 1000)

setImmediate

Node.js 特有的 API,用于将一个回调函数排入事件循环队列,它将在当前事件循环(也就是当前的宏任务)完成后,下一个事件循环开始前被执行

setImmediate(callback);

总结

const macros = [
    {
	    name: 'MessageChannel',
	    test: () => {
	        return (
	            typeof MessageChannel !== "undefined" && typeof window !== "undefined"
	        );
	    },
	    run: (callback: Task) => {
	        const channel = new MessageChannel();
	        channel.port1.onmessage = callback;
	        channel.port2.postMessage(0);
	    },
	},
	{
	    name: 'setImmediate',
	    test: () => {
	        return typeof setImmediate === "function";
	    },
	    run: (callback: Task) => {
	        setImmediate(callback);
	    },
	},
	{
	    name: 'requestAnimationFrame',
	    test: () => {
	        return (
	            typeof requestAnimationFrame !== "undefined" && typeof window !== "undefined"
	        );
	    },
	    run: (callback: Task) => {
	        requestAnimationFrame(() => requestAnimationFrame(callback));
	    }
	},
	{
	    name: 'setTimeout',
	    test: () => {
	        return true;
	    },
	    run: (callback: Task) => {
	        setTimeout(callback, 0);
	    },
    }
]
 
export const runMacroTask = (callback: Task) => {
    const runner = macros.find((item) => item.test());
    if (!runner) {
        throw new Error('当前环境不支持宏任务');
    }
    runner.run(callback);
};