JavaScript 语言是由当时任职于网景公司(Netscape)的 Brendan Eich 用 10 天时间开发出来的网页脚本语言。网景公司计划在浏览器中增加新的功能,以便于在网页中嵌入脚本运行,从而加强动态网页的可操作性,要求嵌入的脚本代码类似 java 的语法,但是比 java 简单易用。根据这些特点,JavaScript 被 Brendan Eich 设计编写出来。因为 JavaScript 的历史原因,JavaScript 至今仍然是语法最为简单,开发最为方便,执行最为快速的脚本语言之一,但是 JavaScript 也有其缺点,诸如单线程执行机制,局限于浏览器执行。直到 Nodejs 运行时被开发出来后, JavaScript 才能够脱离网页浏览器进行服务器和桌面应用开发,但 JavaScript 至今 仍然保留着其基本特性。
JavaScript 语言单线程执行,因为 JavaScript 设计的最初目的是更好地操作网页,采用单线程执行避免多线程互斥争夺资源,单线程执行不需要考虑多线程并行处理,让代码更为简单,程序开发速度更快。单线程执行指的是 JS 主要程序在主线程执行,不能像 java 和 c#语言一样自由增加新线程来并行任务,而事实上 JS 还有其他工作线程协助主线程工作,比如 AJAX 请求线程、处理 DOM 事件线程、定时器线程、读写文件线程(Nodejs 中的 IO 线程)等多个工作线程,JS 的主线程也叫事件循环线程(event loop)。JavaScript 因为采用单线程执行,相比多线程并行无法发挥计算机性能,为了提高程序执行效率,JavaScript 引擎提供同步执行和异步执行的方式,充分利用单线程执行任务。
此图是 Firefox 火狐浏览器开发者工具调试 JavaScript 程序,执行机制中涉及到的执行上下文和调用栈比较抽象,可以参考上图调试工具中的调用堆栈和范围。
JavaScript 程序由上往下边编译边执行,执行到的代码 JavaScript 引擎会先编译代码后生成执行上下文。JavaScript 程序执行会编译代码创建全局执行上下文(global),比如全局变量、函数等等都包含在全局执行上下文中,全局执行上下文中执行时调用其他函数,则 JavaScript 引擎再读取函数代码编译创建新的执行上下文。
console.log('start');
function a (ia) {
console.log('astring');
let c = b(ia);
return c;
}
function b (ib) {
console.log('bstring');
return ib + 1;
}
let num = a(1);
以上代码 num 变量赋值时调用了 a 函数,a 函数中又调用了 b 函数。执行时将会创建 3 个执行上下文,其中包含全局上下文以及 a 函数、b 函数内部代码块的执行上下文。具体执行如下:
1.执行此段代码,创建全局执行上下文,包含 3 个变量 num,a(ia),b(ib)(后两个为函数),输出 start,调用 a(ia)函数;
2.执行 a(ia)函数,创建函数执行上下文,包含 2 个变量 ia,c,输出 astring,调用 b(ib)函数;
3.执行 b(ib)函数,创建函数执行上下文,包含 1 个变量 ib,输出 bstring,返回值。
按照以上的执行方式,JavaScript 的运行会创建非常多的执行上下文,JavaScript 引擎如何管理这么多执行上下文呢,那就是使用调用栈(Call Stack)来管理,即所有执行到的代码段都创建执行上下文后放入调用栈中,执行完或者返回值后执行上下文将出栈销毁。
这里涉及到的概念是栈也叫堆栈,是一种线性表,数据按先进后出(FILO)或者叫后进先出(LIFO)的顺序进出栈。通俗地讲堆栈就像一个桶,物体一个个放进去,再一个个拿出来,最后进去的最先拿出来。
以上面的例子,介绍调用栈如何工作。
1.执行此段代码,创建 Global 全局执行上下文,包含 3 个变量 num,a(ia),b(ib)(后两个为函数),放入调用栈,执行输出 start,调用 a(ia)函数;
2.执行 a(ia)函数,创建 a 函数执行上下文,包含 2 个变量 ia,c,放入调用栈,输出 astring,调用 b(ib);
3.执行 b(ib)函数,创建 b 函数执行上下文,包含 1 个变量 ib,放入调用栈,输出 bstring,返回值。
4.b(ib)函数执行上下文出栈销毁;
5.a(ia)函数执行上下文出栈销毁;
只剩下全局执行上下文,至此整个操作执行结束。
JavaScript 程序我们可以使用 Chrome 和 Firefox浏览器的开发者工具进行调试,直观得查看JavaScript引擎调用栈工作情况和JavaScript程序的运行流程以及变量赋值函数调用等情况。
以 Firefox 浏览器为例,html 中加载 js 程序,F12 进入开发者工具调试器,找到加载的 js 文件,打上断点后刷新网页即可进入调试模式,此时点击“步进”按钮或者 F11 执行代码,可以看到调用栈使用情况,各上下文变量情况,以及程序执行的输出结果。
JavaScript 引擎采用单线程,程序由上往下边编译边执行,单线程意味着自始至终 JavaScript 引擎只能执行一个操作,如果 JavaScript 程序有很多操作,同步执行就是一个个操作排队执行,前一个操作没有执行结束是不会进行下一个操作的。举个例子,排队到银行柜台办业务,客户需要排号等待,前一个人没有办完业务后面的人只能等着不管多久。
console.log("a");
function b() {
console.log("b");
}
b();
console.log("c");
以上代码,执行输出:
a;
b;
c;
同步执行例子中的代码,如果 b 函数是一个非常耗时的操作,而此时需要等待 b 函数执行完毕后才会输出 c,期间无法执行其他操作,如此低效的单线程同步执行如何提高执行效率呢,JavaScript 引入了异步执行方式来解决。相对于同步执行需要排队等待,那么异步执行就是耗时的操作开始后不需要等待完成,而是先去执行下一个操作,等待耗时操作完成后再接着执行。
上面的例子,排队到银行柜台办业务,客户需要排号等待,前一位客户办理过程中缺少相关证件,柜台无法继续办理业务。同步操作是等待该客户回家取证件,期间柜台一直等待该客户不做任务工作,直到取来证件继续办理完成后才办理下一位客户的业务;异步操作是该客户回家取证件,期间柜台先办理下一位客户业务,等到该客户取来证件后继续为他办理直到完成。这样的工作方式大大提高了效率,充分利用了单线程的空余等待时间。
例子很简单但是在程序中如何实现异步任务呢,JavaScript 使用事件队列(Event Queue)来告诉主线程即将要执行的任务。事件队列(Event Queue)类似于上面提到的堆栈,它也是一种线性数据表,不同之处在于堆栈是先进后出的方式,队列是先进先出(FIFO)的方式,形象地讲堆栈是桶只有一个口出入,队列是管道两头通一边进一边出。
实际工作中,异步操作是主线程调用栈中执行的上下文,通过异步调用浏览器 API ajax 请求服务器数据,异步调用后可以挂起该任务,继续下一个操作,等待 ajax 执行完毕后返回数据,这时候会在事件队列(Event Queue)中添加事件,告诉主线程 ajax 执行完毕下一个操作可以执行我的回调函数了。
console.log("a");
b();
function b() {
setTimeout(() => {
console.log("b");
}, 3000);
}
console.log("c");
以上代码输出:
a;
c;
b;
b 最后输出是因为 setTimeout 定时器延时 3 秒后才执行,此时主线程不会等待 setTimeout 定时器而是先执行输出 c,setTimeout 定时器延时结束后将会在事件队列队列中添加事件,主线程再继续执行输出 b。
注意:异步执行不代表不阻塞主线程,是否会阻塞主线程取决于异步执行时的操作。
以上只是一个简单的例子模拟耗时异步操作,实际上 setTimeout()和 setInterval()定时器异步操作,里面的上下文还是要在主线程执行,如果此时在里面添加一个长循环会发现主线程仍然阻塞,ajax 请求服务器没有阻塞主线程是因为 ajax 是由浏览器提供的单独线程,所以 JavaScript 中真正异步非阻塞的操作只有 ajax 以及其他基于浏览器线程执行的异步 API 调用。(nodejs中可以开发c++扩展调用,因此可以随意使用异步操作完成任何任务而不阻塞主线程)
JavaScript 异步操作代码主要有提供 callback 回调函数的方式,但是容易出现多层嵌套回调形成回调地狱,ES6 使用 promise 来创建异步操作,基于 promise 还有更加优雅的 async/await 编写方式。
回调是一个函数被作为一个参数传递到另一个函数里,在那个函数执行完后再执行。( 也即:B 函数被作为参数传递到 A 函数里,在 A 函数执行完后再执行 B )
function a(callback) {
setTimeout(() => {
console.log("a");
callback("hello b");
}, 1000);
}
// 方式一:调用a函数,传递b函数回调
function b(bstring) {
console.log(bstring);
}
a(b);
// 方式二:调用a函数,传递箭头函数回调
a((bstring) => {
console.log(bstring);
});
注意:回调不代表是异步执行。
以上代码是 a 函数中的 setTimeout()定时器异步操作,执行完之后回调 callback 函数执行输出结果。要注意的是回调不一定是异步操作,如果 a 函数中不使用 setTimeout()定时器,而是直接 callback()也是回调,但不是异步操作。回调函数与异步操作没有任何关系。callback 回调简单直观易于理解,但是各个部分之间高度耦合。多次调用 a 函数传递回调函数需要层层嵌套,就是俗称的回调地狱,代码非常不美观也不易于阅读维护。
JavaScript 事件本质上是一种消息机制,也是一种异步执行的方式,JavaScript 程序在执行时不会立即执行操作,而是监听到事件触发后才执行指定的函数操作。任务的执行不在于代码先后顺序,而在于事件是否被触发。比如在 HTML 中的 a 按钮绑定一个点击事件到 func 函数,JavaScript 执行时不会立即执行 func,当 a 按钮被鼠标点击后才会执行 func 函数。这就是事件驱动的异步执行。事件机制包含事件绑定、事件监听、事件委托(事件代理),方式不同最终都是事件的发生触发执行。
事件绑定是将某个操作绑定到某个事件上,比如将 show 函数绑定到 DOM 元素的 onclick 事件上,当该元素被点击后执行 show 函数,每个元素的对应事件只能绑定一个函数。
<button id="btn3">click</button>;
//绑定onclick事件,仅执行hello
document.getElementById("btn3").onclick = show;
document.getElementById("btn3").onclick = hello;
function show() {
console.log("show");
}
function hello() {
console.log("hello");
}
当点击 click 按钮时,仅执行最后绑定的 hello 函数,输出 hello 字符串。
事件监听是使用 addEventListener 监听元素的事件响应,当监听到点击事件时,触发执行相关函数。事件监听可以执行多个函数。
<button id="btn3">click</button>;
//监听click事件,show和hello两个方法都可以触发
document.getElementById("btn3").addEventListener("click", show);
document.getElementById("btn3").addEventListener("click", hello);
function show() {
console.log("show");
}
function hello() {
console.log("hello");
}
当点击 click 按钮时,将会执行 show、hello 两个函数,输出 show、hello 字符串。
在 JavaScript 程序中 HTML 元素提供了非常多的事件,比如 onchange、onclick、onmouseover、onmouseout、onkeydown、onload,响应 html 元素事件只需要直接绑定或者监听某个元素的事件即可,在 JavaScript 实际编程中,我们想要使用事件异步执行操作不仅仅是绑定或者监听 html 的元素事件,特别是 nodejs 中不再涉及到 html 的 DOM 操作,JavaScript 承担了更多的密集型操作,那么我们如何自定义事件监听来执行异步操作呢?JavaScript 使用 Event()构造器创建事件,如果要传递参数就需要使用 CustomEvent()构造器创建事件。
// 自定义事件监听
var myEvent = new Event('event_name');
window.addEventListener('event_name', function(){
console.log('hello world');
});
setTimeout(() => {
//触发
window.dispatchEvent(myEvent); //IE8及以下使用window.fireEvent(myEvent);
}, 1000);
js 执行经过 1 秒后输出 hello world。
// 自定义事件监听
var myEvent = new CustomEvent("event_name", {
detail: { name: "Kevin" },
});
window.addEventListener("event_name", function (event) {
console.log("my name is ", event.detail.name);
});
setTimeout(() => {
//触发
window.dispatchEvent(myEvent); //IE8及以下使用window.fireEvent(myEvent);
}, 1000);
js 执行经过 1 秒后输出 my name is Kevin。
Promise 是异步编程的一种解决方案,比传统的回调函数和事件更合理且更强大。它最早由社区提出并实现,ES6 将其写进了语言标准,统一了用法,并原生提供了 Promise 对象。promise 相比 callback 回调避免了回调地狱的嵌套,而且语法更加规范代码易于维护,另外还能进行链式调用,也就是使用多个 then 链式执行。
特点
对象的状态不受外界影响 (3 种状态)
Pending 状态(进行中)
Fulfilled 状态(已成功)
Rejected 状态(已失败)
一旦状态改变就不会再变 (两种状态改变:成功或失败)
Pending -> Fulfilled
Pending -> Rejected
var promise = new Promise(function(resolve, reject){
// ... some code
if (/* 异步操作成功 */) {
resolve(value);
} else {
reject(error);
}
})
Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。resolve 作用是将 Promise 对象状态由“未完成”变为“成功”,也就是 Pending -> Fulfilled,在异步操作成功时调用,并将异步操作的结果作为参数传递出去;而 reject 函数则是将 Promise 对象状态由“未完成”变为“失败”,也就是 Pending -> Rejected,在异步操作失败时调用,并将异步操作的结果作为参数传递出去。
Promise 实例生成后,可用 then 方法分别指定两种状态回调参数。then 方法可以接受两个回调函数作为参数:
Promise 对象状态改为 Resolved 时调用 (必选)
Promise 对象状态改为 Rejected 时调用 (可选)
示例 1:创建立即执行
let promise = new Promise(function (resolve, reject) {
console.log("a");
resolve();
});
promise.then(() => console.log("b"));
console.log("c");
输出
a;
c;
b;
以上代码 promise 对象 new 创建之后会立即执行,输出 a,执行 resolve()改变 promise 对象状态为 fulfilled 成功,promise 是异步操作,因此 console.log(“c”)执行输出 c,接着执行 promise.then()里面的函数。
示例 2:调用函数创建 promise 对象结合 setTimeout()定时器操作
function sleep(ms) {
return new Promise(function (resolve, reject) {
setTimeout(() => {
console.log("b");
resolve();
}, ms);
});
}
sleep(1000).then(() => console.log("a"));
console.log("c");
输出
c;
b;
a;
该示例比较直观能看出执行顺序是调用 sleep 创建 promise 对象,然后使用 setTimeout()定时器定时 1 秒执行输出 b,因为定时器延时所以异步操作先执行输出 c,等待定时器执行输出 b 后改变 promise 的状态为成功,再执行输出 a。
async/await 其实是 promise 的优化,以一种更加简单的语法实现 promise 的异步功能,但实际上还是 promise 实现的异步。使用 async/await 语法就是异步代码更像是同步代码,代码简洁美观易维护。使用 async 创建一个函数,返回值就是一个 promise 的对象和值。
async function testAsync() {
return "hello async";
}
const result = testAsync();
console.log(result);
输出
Promise { <state>: "fulfilled", <value>: "hello async" }
执行上面的代码可以看出 async 返回的就是一个 Promise 的对象,对象包含状态和值。async 函数的 return 相当于自动执行了 resolve()并且把执行结果返回。async 的函数是执行后立即返回对象,当 testAsync 函数执行长耗时任务时,没有及时 return 对象,那么 async 自动执行 Promise.resolve(undefined)执行后面的任务。async 返回的值时 promise 对象因此我们处理返回值必须要使用 then 链来处理。
async function testAsync() {
return "hello async";
}
testAsync().then((v) => {
console.log(v);
});
既然 async 返回的是 Promise 对象,那么能不能直接异步 testAsync 执行呢,我们在 testAsync()函数添加定时器模拟长耗时任务。
错误示例:
async function testAsync() {
setTimeout(() => {
return "hello async";
}, 1000);
}
testAsync().then((v) => {
console.log(v);
console.log("a");
});
输出
undefined;
a;
以上代码执行,async 的 testAsync 函数首先执行,因为 return 在定时器中延时执行返回对象,因此 async 的 testAsync 立刻执行了 Promise.resolve(undefined),以执行 then 后面的代码。testAsync()函数未执行输出结果,promise 就直接改变状态为成功执行后面的代码,显然不是我们要的结果。
一般来说 await 等待的是 async 异步操作,但是 JavaScript 中的 await 没有限制,他可以是一个普通函数返回值,也可以是一个 promise 对象。如果等待得到的是普通函数返回值那么这就是他要的结果,而如果等到的是 Promise 对象,那么将会阻塞下面的操作等待 promise 执行完毕再执行下面的操作。
function takeLongTime() {
return new Promise((resolve) => {
setTimeout(() => resolve("a"), 1000);
});
}
async function test() {
const v = await takeLongTime();
console.log(v);
}
test();
输出结果 a
Promise 对象是 ES6 为解决 callback 回调地狱引进的解决方案,让层层嵌套的 callback 回调变成 then 链式回调, async/await 的出现就是优化让 Promise 异步 then 链式执行,让异步代码更加简洁美观易维护。
下面对比使用 then 链和 async/await 来实现多次回调的代码写法。
Promise 对象函数
function takeLongTime(n) {
return new Promise((resolve) => {
setTimeout(() => resolve(n + 100), 1000);
});
}
then 链执行
takeLongTime(100)
.then((v) => {
console.log(v);
return takeLongTime(v);
})
.then((v) => {
console.log(v);
return takeLongTime(v);
})
.then((v) => {
console.log(v);
return takeLongTime(v);
})
.then((v) => {
console.log(v);
return takeLongTime(v);
});
输出
200;
300;
400;
500;
async/await 写法
async function test() {
let v1 = await takeLongTime(100);
console.log(v1);
let v2 = await takeLongTime(v1);
console.log(v2);
let v3 = await takeLongTime(v2);
console.log(v3);
let v4 = await takeLongTime(v3);
console.log(v4);
}
test();
输出
200;
300;
400;
500;
async/await 写法优化
async function test() {
let v = 100;
for (let index = 0; index < 5; index++) {
v = await takeLongTime(v);
console.log(v);
}
}
test();
输出
200;
300;
400;
500;
600;
可以看出虽然 async/await 方式能做的事情,then 方式编写也能做到,但是 async/await 相比 then 的写法代码更加简洁明了。
观察者模式是观察与被观察的关系,观察者观察被观察者状态改变后更新观察者。比如道路交通红绿灯是被观察者,而路上的行人车辆是观察者,当交通灯变为绿灯时,行人开始通过路口。
// Subject为被观察者,Subject中的状态(state)改变,就通知 Observer更新
class Subject {
constructor() {
this.observes = [];
this.state = false;
}
// this.observes存储观察者
attach(observe) {
this.observes.push(observe);
}
// 状态改变,通知 Observer 触发更新
setState(newState) {
this.state = newState;
this.observes.forEach((observer) => observer.update(newState));
}
}
// Observer为观察者,观察Subject的状态是否改变
class Observer {
constructor(name) {
this.name = name;
}
// 更新
update(state) {
console.log(this.name + ",接收到了通知,被观察者的属性变为 " + state);
}
}
var sub = new Subject();
var obs1 = new Observer("观察者1");
var obs2 = new Observer("观察者2");
sub.attach(obs1);
sub.attach(obs2);
// 被观察者的状态改变,触发观察者更新
sub.setState(true);
以上代码中两个类一个是观察者一个是被观察者,当观察者改变状态是调用观察者的方法改变观察者的状态。其实跟上面的回调函数类似,当被被观察者 Subject 执行 setState 方法,setState 方法里面再调用 Observer 观察者的 update 方法更新。
发布订阅模式与观察者模式相似,但是发布订阅模式存在一个中间层,发布者更新中间层,订阅者根据中间层改变做出相应的操作。举个例子,学校的学生广播播音员的关系。上课时老师通过广播通知学生上课,学生乖乖回到教室;下课时老师通过广播通知学生下课,学生出教室休息。这里面老师是发布者,广播室中间层,学生是订阅者。
// 发布订阅
class Events {
constructor() {
this.sub = {}; // 容器
}
// 根绝不同 name,订阅对应函数
$on(name, fn) {
const wrap = this.sub[name] || (this.sub[name] = []);
wrap.push(fn);
}
// 遍历所有相同name的订阅函数,并发布
$emit(name, ...args) {
const fns = this.sub[name] || [];
fns.forEach((fn) => {
fn.apply(this, args);
});
}
// 销毁,避免内存泄漏
$of(name) {
this.sub[name] = null;
}
}
// event 相当于中转器
const event = new Events();
// 订阅
event.$on("eventname", function (a, b) {
console.log(a, b);
});
event.$on("eventname", function (a, b) {
console.log(a, b);
});
// 发布
event.$emit("eventname", "a", "b");
主线程从事件队列中读取任务执行,这个过程是循环不断的,这种循环执行机制叫做 event loop 事件循环。JavaScript 执行时使用调用栈和事件队列来执行 JavaScript 程序,执行的代码段编译创建上下文放入调用栈中执行,调用栈执行完任务后,将读取任务队列的任务执行,编译执行代码创建上下文放入调用栈执行出栈销毁,这整个过程是循环往复的这就是 eventloop 基本工作流程。
流程图
流程图中当 call stack 调用栈空闲时读取 event queue 事件队列中的任务执行,执行时调用 webapis 异步,异步执行完毕后再添加任务到事件队列中等待执行。调用栈不停从事件队列中读取事件执行就是 event loop 事件循环机制。
示例:
<button id="btn1">click</button>;
//绑定onclick事件
document.getElementById("btn1").onclick = show;
function show() {
console.log("show");
}
//click按钮点击时输出show
这个过程就是流程图中 DOM document 事件绑定和触发
序列图
从上图比较直观看出 event loop 是在调用栈执行完任务空闲时从事件队列中读取任务执行,event loop 只关心 event queue 中有没有任务,跟 webapis 没有关系,因为异步执行不一定是调用 webapi ,也可能是跟创建的工作线程通信等其他异步操作,在nodejs中可能是C++插件自定义的异步操作,这里的 webapis 只是一个异步操作的代表。
示例:
<button id="btn1">click</button>;
//绑定onclick事件
document.getElementById("btn1").onclick = show;
function show() {
//调用ajax请求
var ajax = new XMLHttpRequest();
ajax.open("get", "getStar.php?starName=myname");
ajax.send();
//状态改变触发(执行结束触发)
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
console.log(ajax.responseText); //输入相应的内容
}
};
}
该操作就是序列图的操作,先给 btn1 按钮绑定 onclick 事件,点击触发后执行 show()函数,然后在调用 ajax 的 webapi 异步请求服务器,ajax 执行完毕后再执行回调函数输出内容。
在网页中运行的 JavaScript 大多是操作 DOM 还有请求服务器,浏览器中执行不涉及读写文件,而且大多计算操作也都在服务器端执行,因此 JavaScript 单线程基本够用。自从 Nodejs 运行时 发布后,JavaScript 已经不再局限于浏览器运行,在服务器应用和桌面应用开发,涉及到 IO 密集型和 CPU 密集型操作,那么此时单线程就显得不够用了,虽然 JavaScript 单线程异步可以提高主线程执行效率,但是如果想要不阻塞主线程,耗时操作还是需要异步后用多线程执行。
H5 标准后网页可以通过 web worker 创建多线程,而 Nodejs 中通过 worker_threads 创建多线程,多线程实现,Nodejs 中还可以通过创建子进程执行密集型运算,而 worker_threads 工作线程与 child_process 或 cluster 创建子进程不同,worker_threads 工作线程可以共享内存。 它们通过传输 ArrayBuffer 实例或共享 SharedArrayBuffer 实例来实现。工作线程对于执行 CPU 密集型的 JavaScript 操作很有用,它们对 I/O 密集型的工作帮助不大。 Node.js 内置的异步 I/O 操作比工作线程更高效。
const { Worker } = require("worker_threads");
const worker = new Worker(__filename);
需要注意的是 new Worker()创建 worker 以文件为单位,Nodejs 可以 new Worker(__filename)使用本身创建 worker,通过 worker.isMainThread 对象区分主线程工作线程,但如果是 Electron 应用开发,使用主进程(nodejs 运行时)的 js 文件创建工作线程将会报错,可以将 worker 独立 js 文件创建工作线程。
isMainThread 对象用于判断当前是否为主线程,是则为 true
const { Worker, isMainThread } = require("worker_threads");
if (isMainThread) {
// 这会在工作线程实例中重新加载当前文件。
new Worker(__filename);
} else {
console.log("Inside Worker!");
console.log(isMainThread); // 打印 'false'。
}
worker.postMessage 方法主要是向 worker 发送消息。
const { Worker, isMainThread, parentPort } = require("worker_threads");
if (isMainThread) {
const worker = new Worker(__filename);
worker.once("message", (message) => {
console.log(message); // 打印 'Hello, world!'。
});
worker.postMessage("Hello, world!");
} else {
// 当收到来自父线程的消息时,则将其发回:
parentPort.once("message", (message) => {
parentPort.postMessage(message);
});
}
在 worker 工作线程中 parentPort 表示主线程,可以 on 监听 message 事件接收主线程消息或者通过 postMessage 发送消息。
如果此线程是 Worker,则这是允许与父线程通信的 MessagePort。 使用 parentPort.postMessage() 发送的消息在使用 worker.on(‘message’) 的父线程中可用,使用 worker.postMessage() 从父线程发送的消息在使用 parentPort.on(‘message’) 的该线程中可用。
当工作线程调用 require(‘worker_threads’).parentPort.postMessage() 时,则会触发 ‘message’ 事件。
从工作线程发送的所有消息都在 Worker 对象上触发 ‘exit’ 事件之前触发。接收消息通过监听 message 时间触发后执行,上面的例子 worker.on 监听 worker 发送消息,parentPort.on 监听主线程发送消息。
main.js
const {
isMainThread,
parentPort,
workerData,
threadId,
MessageChannel,
MessagePort,
Worker,
} = require("worker_threads");
const worker1 = new Worker(`${__dirname}/worker.js`);
worker1.on("message", (value) => console.log(value));
worker1.postMessage("hello worker1");
worker.js
const {
isMainThread,
parentPort,
workerData,
threadId,
MessageChannel,
MessagePort,
Worker,
} = require("worker_threads");
parentPort.on("message", (value) => {
// 模拟耗时
// var startTime = new Date().getTime() + parseInt(5000, 10);
// while(new Date().getTime() < startTime) {}
console.log(value);
parentPort.postMessage(`${value} post to main thread`);
});
经过测试,worker 即使长耗时阻塞线程,也不会影响主线程,而且当主线程 postMessage 发送大量消息到 worker,即使 worker 阻塞也可以接收消息。worker_threads 是 CPU 密集型操作的解决方法,但如果是读写文件 IO 密集型操作,worker_threads 并不会有所改善,因为 nodejs 原生的 io 异步操作已经足够好用。
worker_threads 还有很多方法,例如创建多个 worker 工作线程并且互相发送消息等,具体请查阅 Nodejs 官方文档。
【参考资料】