Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

息息相关的 JS 同步,异步和事件轮询 #112

Open
husky-dot opened this issue Sep 16, 2019 · 0 comments
Open

息息相关的 JS 同步,异步和事件轮询 #112

husky-dot opened this issue Sep 16, 2019 · 0 comments

Comments

@husky-dot
Copy link
Owner

作者:Sukhjinder Arora

译者:前端小智

来源:medium

为了保证的可读性,本文采用意译而非直译。

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

JS 是一门单线程的编程语言,这就意味着一个时间里只能处理一件事,也就是说JS引擎一次只能在一个线程里处理一条语句。

虽然单线程简化了编程代码,因为这样咱们不必太担心并发引出的问题,这也意味着在阻塞主线程的情况下执行长时间的操作,如网络请求。

想象一下从API请求一些数据,根据具体的情况,服务器需要一些时间来处理请求,同时阻塞主线程,使网页长时间处于无响应的状态。这就是引入异步 JS 的原因。使用异步 (如 回调函数、promiseasync/await),可以不用阻塞主线程的情况下长时间执行网络请求。

了解异步的工作方式之前,咱们先来看看同步是怎么样工作的。

同步 JS 是如何工作的?

在深入研究异步JS之前,先来了解同步 JS 代码在 JavaScript 引擎中执行情况。例如:

    const second = () => {
      console.log('Hello there!');
    }
    
    const first = () => {
      console.log('Hi there!');
      second();
      console.log('The End');
    }
    
    first();

要理解上述代码如何在 JS 引擎中执行,咱们必须理解什么是执行上下文调用栈(也称为执行堆栈)。

函数代码在函数执行上下文中执行,全局代码在全局执行上下文中执行。每个函数都有自己的执行上下文。

调用栈

调用堆栈顾名思义是一个具有LIFO(后进先出)结构的堆栈,用于存储在代码执行期间创建的所有执行上下文。

JS 只有一个调用栈,因为它是一种单线程编程语言。调用堆栈具有 LIFO 结构,这意味着项目只能从堆栈顶部添加或删除。

回到上面的代码,尝试理解代该码是如何在JS引擎中执行。

const second = () => {
  console.log('Hello there!');
}
const first = () => {
  console.log('Hi there!');
  second();
  console.log('The End');
}
first();

这里发生了什么?

当执行此代码时,将创建一个全局执行上下文(由main()表示)并将其推到调用堆栈的顶部。当遇到对first()的调用时,它会被推送到堆栈的顶部。

接下来,console.log('Hi there!')被推送到堆栈的顶部,当它完成时,它会从堆栈中弹出。之后,我们调用second(),因此second()函数被推到堆栈的顶部。

console.log('Hello there!')被推送到堆栈顶部,并在完成时弹出堆栈。second() 函数结束,因此它从堆栈中弹出。

console.log(“the End”)被推到堆栈的顶部,并在完成时删除。之后,first()函数完成,因此从堆栈中删除它。

程序在这一点上完成了它的执行,所以全局执行上下文(main())从堆栈中弹出。

异步 JS 是如何工作的?

现在咱们已经对调用堆栈和同步JAS的工作原理有了基本的了解,回到异步JS上。

阻塞是什么?

假设咱们正在以同步的方式进行图像处理或网络请求。例如:

const processImage = (image) => {
  /**
  * doing some operations on image
  **/
  console.log('Image processed');
}
const networkRequest = (url) => {
  /**
  * requesting network resource
  **/
  return someData;
}
const greeting = () => {
  console.log('Hello World');
}
processImage(logo.jpg);
networkRequest('www.somerandomurl.com');
greeting();

做图像处理和网络请求需要时间,当processImage()函数被调用时,它会根据图像的大小花费一些时间。

processImage() 函数完成后,将从堆栈中删除它。然后调用 networkRequest() 函数并将其推入堆栈。同样,它也需要一些时间来完成执行。

最后,当networkRequest()函数完成时,调用greeting()函数。

因此,咱们必须等待函数如processImage()networkRequest()完成。这意味着这些函数阻塞了调用堆栈或主线程。因此,在执行上述代码时,咱们不能执行任何其他操作,这是不理想的。

解决办法是什么?

最简单的解决方案是异步回调,各位使用异步回调使代码非阻塞。例如:

const networkRequest = () => {
  setTimeout(() => {
    console.log('Async Code');
  }, 2000);
};
console.log('Hello World');
networkRequest();

这里使用了setTimeout方法来模拟网络请求。请记住setTimeout不是JS引擎的一部分,它是Web Api的一部分。

为了理解这段代码是如何执行的,咱们必须理解更多的概念,比如事件轮询和回调队列(或消息队列)。

事件轮询、web api和消息队列不是JavaScript引擎的一部分,而是浏览器的JavaScript运行时环境或Nodejs JavaScript运行时环境的一部分(对于Nodejs)。在Nodejs中,web api被c/c++ api所替代。

现在让我们回到上面的代码,看看它是如何异步执行的。

const networkRequest = () => {
  setTimeout(() => {
    console.log('Async Code');
  }, 2000);
};

console.log('Hello World');

networkRequest();

console.log('The End');

当上述代码在浏览器中加载时,console.log(' Hello World ') 被推送到堆栈中,并在完成后弹出堆栈。接下来,将遇到对 networkRequest() 的调用,因此将它推到堆栈的顶部。

下一个 setTimeout() 函数被调用,因此它被推到堆栈的顶部。setTimeout()有两个参数:

    1. 回调和
    1. 以毫秒(ms)为单位的时间。

setTimeout() 方法在web api环境中启动一个2s的计时器。此时,setTimeout()已经完成,并从堆栈中弹出。cosole.log(“the end”) 被推送到堆栈中,在完成后执行并从堆栈中删除。

同时,计时器已经过期,现在回调被推送到消息队列。但是回调不会立即执行,这就是事件轮询开始的地方。

事件轮询

事件轮询的工作是监听调用堆栈,并确定调用堆栈是否为空。如果调用堆栈是空的,它将检查消息队列,看看是否有任何挂起的回调等待执行。

在这种情况下,消息队列包含一个回调,此时调用堆栈为空。因此,事件轮询将回调推到堆栈的顶部。

然后是 console.log(“Async Code”) 被推送到堆栈顶部,执行并从堆栈中弹出。此时,回调已经完成,因此从堆栈中删除它,程序最终完成。

消息队列还包含来自DOM事件(如单击事件和键盘事件)的回调。例如:

document.querySelector('.btn').addEventListener('click',(event) => {
  console.log('Button Clicked');
});

对于DOM事件,事件侦听器位于web api环境中,等待某个事件(在本例中单击event)发生,当该事件发生时,回调函数被放置在等待执行的消息队列中。

同样,事件轮询检查调用堆栈是否为空,并在调用堆栈为空并执行回调时将事件回调推送到堆栈。

延迟函数执行

咱们还可以使用setTimeout来延迟函数的执行,直到堆栈清空为止。例如

const bar = () => {
  console.log('bar');
}
const baz = () => {
  console.log('baz');
}
const foo = () => {
  console.log('foo');
  setTimeout(bar, 0);
  baz();
}
foo();

打印结果:

foo
baz
bar

当这段代码运行时,第一个函数foo()被调用,在foo内部我们调用console.log('foo'),然后setTimeout()被调用,bar()作为回调函数和时0秒计时器。

现在,如果咱们没有使用 setTimeout, bar() 函数将立即执行,但是使用 setTimeout0秒计时器,将bar的执行延迟到堆栈为空的时候。

0秒后,bar()回调被放入等待执行的消息队列中,但是它只会在堆栈完全空的时候执行,也就是在bazfoo函数完成之后。

ES6 任务队列

我们已经了解了异步回调和DOM事件是如何执行的,它们使用消息队列存储等待执行所有回调。

ES6引入了任务队列的概念,任务队列是 JS 中的 promise 所使用的。消息队列和任务队列的区别在于,任务队列的优先级高于消息队列,这意味着任务队列中的promise 作业将在消息队列中的回调之前执行,例如:

const bar = () => {
  console.log('bar');
};

const baz = () => {
  console.log('baz');
};

const foo = () => {
  console.log('foo');
  setTimeout(bar, 0);
  new Promise((resolve, reject) => {
    resolve('Promise resolved');
  }).then(res => console.log(res))
    .catch(err => console.log(err));
  baz();
};

foo();

打印结果:

foo
baz
Promised resolved
bar

咱们可以看到 promisesetTimeout 之前执行,因为 promise 响应存储在任务队列中,任务队列的优先级高于消息队列。

小结

因此,咱们了解了异步 JS 是如何工作的,以及调用堆栈、事件循环、消息队列和任务队列等概念,这些概念共同构成了 JS 运行时环境。虽然成为一名出色的JS开发人员并不需要学习所有这些概念,但是了解这些概念是有帮助的。

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://blog.bitsrc.io/understanding-asynchronous-javascript-the-event-loop-74cd408419ff

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq449245884/xiaozhi

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant