JavaScript 执行机制和同异步执行

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 引擎提供同步执行和异步执行的方式,充分利用单线程执行任务。

二、执行机制

JavaScript 执行机制和同异步执行_第1张图片

此图是 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)函数执行上下文出栈销毁;

只剩下全局执行上下文,至此整个操作执行结束。

调用栈
执行
调用函数a
调用函数b
调用函数c
最先出栈
其次出栈
最后出栈
创建a函数执行上下文
创建全局执行上下文
创建b函数执行上下文
创建c函数执行上下文
JavaScript程序
销毁

(三)开发者工具调试程序

JavaScript 程序我们可以使用 Chrome 和 Firefox浏览器的开发者工具进行调试,直观得查看JavaScript引擎调用栈工作情况和JavaScript程序的运行流程以及变量赋值函数调用等情况。

以 Firefox 浏览器为例,html 中加载 js 程序,F12 进入开发者工具调试器,找到加载的 js 文件,打上断点后刷新网页即可进入调试模式,此时点击“步进”按钮或者 F11 执行代码,可以看到调用栈使用情况,各上下文变量情况,以及程序执行的输出结果。
JavaScript 执行机制和同异步执行_第2张图片

三、同步执行

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++扩展调用,因此可以随意使用异步操作完成任何任务而不阻塞主线程)

Event Queue
异步
Call Stack调用栈
执行
调用函数a
异步后立即调用函数b
最先出栈
最后出栈
异步执行
添加事件
调用栈执行完后再执行
出栈
事件队列
ajax请求
a函数执行上下文
全局执行上下文
b函数执行上下文
Ajax回调执行上下文
JavaScript
销毁

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 函数。这就是事件驱动的异步执行。事件机制包含事件绑定、事件监听、事件委托(事件代理),方式不同最终都是事件的发生触发执行。

1.事件绑定

事件绑定是将某个操作绑定到某个事件上,比如将 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 字符串。

2.事件监听

事件监听是使用 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 字符串。

3.自定义事件监听

在 JavaScript 程序中 HTML 元素提供了非常多的事件,比如 onchange、onclick、onmouseover、onmouseout、onkeydown、onload,响应 html 元素事件只需要直接绑定或者监听某个元素的事件即可,在 JavaScript 实际编程中,我们想要使用事件异步执行操作不仅仅是绑定或者监听 html 的元素事件,特别是 nodejs 中不再涉及到 html 的 DOM 操作,JavaScript 承担了更多的密集型操作,那么我们如何自定义事件监听来执行异步操作呢?JavaScript 使用 Event()构造器创建事件,如果要传递参数就需要使用 CustomEvent()构造器创建事件。

(1)Event()构造器
// 自定义事件监听
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。

(2)CustomEvent()构造器
// 自定义事件监听
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 对象

Promise 是异步编程的一种解决方案,比传统的回调函数和事件更合理且更强大。它最早由社区提出并实现,ES6 将其写进了语言标准,统一了用法,并原生提供了 Promise 对象。promise 相比 callback 回调避免了回调地狱的嵌套,而且语法更加规范代码易于维护,另外还能进行链式调用,也就是使用多个 then 链式执行。

特点

对象的状态不受外界影响 (3 种状态)

  • Pending 状态(进行中)

  • Fulfilled 状态(已成功)

  • Rejected 状态(已失败)

一旦状态改变就不会再变 (两种状态改变:成功或失败)

  • Pending -> Fulfilled

  • Pending -> Rejected

1.Promise 实例

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,在异步操作失败时调用,并将异步操作的结果作为参数传递出去。

2.then 方法

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。

3.async/await

async/await 其实是 promise 的优化,以一种更加简单的语法实现 promise 的异步功能,但实际上还是 promise 实现的异步。使用 async/await 语法就是异步代码更像是同步代码,代码简洁美观易维护。使用 async 创建一个函数,返回值就是一个 promise 的对象和值。

(1).async
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 就直接改变状态为成功执行后面的代码,显然不是我们要的结果。

(2).await

一般来说 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

(3).使用 async/await 目的

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");
执行
发布者
消息中心
订阅者1
订阅者2
订阅者3

五、事件循环(Event Loop)

主线程从事件队列中读取任务执行,这个过程是循环不断的,这种循环执行机制叫做 event loop 事件循环。JavaScript 执行时使用调用栈和事件队列来执行 JavaScript 程序,执行的代码段编译创建上下文放入调用栈中执行,调用栈执行完任务后,将读取任务队列的任务执行,编译执行代码创建上下文放入调用栈执行出栈销毁,这整个过程是循环往复的这就是 eventloop 基本工作流程。

流程图

JavaScript
Heap
Call Stack
Event Queue
WebAPIs
绑定onClick事件
事件触发添加任务
callstack空闲时读取任务生成context进栈执行
调用API请求服务器
请求完毕添加任务
callstack空闲时读取任务生成context进栈执行
Context1
Context2
Context3
heap
onClick
callback
onLoad
DOM document
AJAX
setTimeout
setInterval

流程图中当 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 Queue Call Stack WebAPIs 调用栈空闲时读取队列任务,创建上下文进入调用栈执行 loop [event loop] DOM document绑定onClick事件 DOM document触发onClick事件,添加任务到队列 AJAX请求 AJAX执行完毕,添加任务到队列 Event Queue Call Stack WebAPIs

从上图比较直观看出 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 执行机制和同异步执行_第3张图片

六、多线程

在网页中运行的 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 操作比工作线程更高效。

(一)worker 对象

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 文件创建工作线程。

(二)worker 类

1.worker.isMainThread

isMainThread 对象用于判断当前是否为主线程,是则为 true

const { Worker, isMainThread } = require("worker_threads");

if (isMainThread) {
  // 这会在工作线程实例中重新加载当前文件。
  new Worker(__filename);
} else {
  console.log("Inside Worker!");
  console.log(isMainThread); // 打印 'false'。
}

2.worker.postMessage(value[, transferList])

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

3.worker.parentPort

在 worker 工作线程中 parentPort 表示主线程,可以 on 监听 message 事件接收主线程消息或者通过 postMessage 发送消息。

如果此线程是 Worker,则这是允许与父线程通信的 MessagePort。 使用 parentPort.postMessage() 发送的消息在使用 worker.on(‘message’) 的父线程中可用,使用 worker.postMessage() 从父线程发送的消息在使用 parentPort.on(‘message’) 的该线程中可用。

4.message 事件

当工作线程调用 require(‘worker_threads’).parentPort.postMessage() 时,则会触发 ‘message’ 事件。

从工作线程发送的所有消息都在 Worker 对象上触发 ‘exit’ 事件之前触发。接收消息通过监听 message 时间触发后执行,上面的例子 worker.on 监听 worker 发送消息,parentPort.on 监听主线程发送消息。

(三)worker 实例

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 官方文档。

【参考资料】

你可能感兴趣的:(Node.js,JavaScript,javascript,前端,开发语言)