Skip to content

JS的异步操作,浏览器的多线程间的协作

统计信息:字数 11148 阅读23分钟

https://www.cnblogs.com/pengshengguang/p/8734769.html

遇到的问题,引发了思考

今天看了一个例子,强烈引发了我对于浏览器多线程之间的操作机制、同步与异步、回调函数的兴致,代码如下:

<html>
  <head>
    <title>title</title>
  </head>
  <body>  
    <input type="text" value="" name="input" onkeydown="console.log(this.value)">  
    <input type="text" value="" name="input" onkeydown="var me = this;setTimeout(function(){console.log(me.value)});">
  </body>
</html>

如果有兴趣,你可以直接运行这段代码,打开网页,用F12进行调试,发现,代码大同小异,但是控制台输出却不一样!!

通过上面的现象,你会问,这是为什么???百思不得其解,给点耐心,往下看,涉及的知识点丰富着呢,哈哈。

这个例子可以说非常罕见,本质原因是浏览器的事件监听线程与UI渲染线程发生冲突了,到底谁先于谁执行的问题。

如果没有耐心,下面的第七点直接揭露真相。

一、前言

经过查阅了很多的资料,基本了解了JS的同步与异步的操作,浏览器内核的多线程并发操作,其中涉及到的知识点如下:

  • 什么是同步和异步?
  • JS的是基于事件驱动的单线程语言,为啥会有异步操作这种多线程的操作???
  • 浏览器线程,浏览器内核线程间的合作?
  • JS的异步操作都有哪些?它是如何工作的?
  • AJAX请求的时候,浏览器内核线程机制是怎样的工作的?
  • 当js主线程空闲时,此时同事发生事件触发线程和UI渲染线程,并发生冲突时,哪个会先执行问题(例如表单中文本框的输入与事件触发)

二、js单线程

单线程的含义是js只能在一个线程上运行,也就是说,同一时间只能做一件事情,其他的任务则会放在任务队列里面排队等等js线程处理。

但是值得注意的是,虽然js是单线程语言,但是并不代表浏览器内核中的js引擎线程只有一个。js引擎有多个线程,一个主线程,其他的线程配合主线程工作

三、为什么js选择单线程

与它的用途有关。作为浏览器脚本语言,Javascript主要用途是与用户互动,以及操作dom。这决定了它只能是单线程,否则会带来复杂的同步问题。比如,假设javascript同事有两个进程,一个线程在某个DOM阶段添加内容,另一个线程删除了这个节点,这是浏览器应该以哪一个线程为准?想要实现这个问题,肯定要加入线程锁这个概念,那就复杂多了。

单线程与异步确实不能同时成为一个语言的特性,JS选择成为单线程语言,所以它本身是不可能异步的,但是js的宿主环境(比如浏览器,Node)是多线程的,宿主通过某种方式,使得js具备异步的属性,如果你理解了,你会发现js的机制是多么的简单高效!

四、同步和异步

  • 同步:一次只能完成一件任务,如果有多个任务,必须排队
  • 异步:每一个任务有一个或者多个回调函数(callback),前一个任务执行后,把里面回调函数抛出来,给浏览器的另外一个线程处理。而后一个任务是不等待前一个任务结束就执行。
  • 一个例子来说明异步和同步:
<html><head>  <title>异步与同步</title></head><body><script>   setTimeout(function () {     console.log("异步任务")   })   console.log("同步任务")</script></body></html> //输出://同步任务//异步任务 //执行顺序://首先执行setTimeout,发现是个异步操作,就把这个异步操作丢给浏览器内核中的//计时器线程,然后js主线程继续执行console.log("同步任务"),之后js主线程空闲,//从任务队列里面获取异步操作任务,放到js主线程栈中执行,输出“异步任务”

五、异步的好处

1.异步的好处

用一个同步与一个异步图进行说明,假设有四个任务(1、2、3、4),它们各自的执行时间都是10ms,其中,任务2是任务3的前置任务,任务2需要20ms的响应时间。

img

2.适合的场景

可以看出,当我们的程序需要大量I/O操作和用户请求时,js这个具备单线程,异步,事件驱动多种气质的语言是多么应景!相比于多线程语言,它不必耗费过多的系统开销,同时也不必把精力用于处理多线程管理,相比于同步执行的语言,宿主环境的异步和事件驱动机制又让它实现了非阻塞I/O,所以你应该知道它适合什么样的场景了吧!

六、JS的异步操作有哪些

我们已经知道,js一直是单线程执行的,浏览器为了几个明显的耗时任务(例如ajax),单独开辟线程解决耗时问题。但是JS除了这几个明显耗时问题外,可能我们自己写的程序里面也会有耗时函数,怎么解决?我们肯定不能直接开辟单独的线程,但是我们可以利用浏览器给我们开发的窗口(定时器线程和事件触发线程),总的来说,有一下4种异步操作:

  • 定时器函数setTimeout(交给定时器线程处理)
  • 事件绑定(onclick、onkeydown....,交给事件触发线程处理)
  • AJAX(交给HTTP线程处理)
  • 回调函数

七、说说浏览器的线程

前面说了,js是单线程,浏览器只分配给JS一个主线程,用来执行函数,但一次只能执行一个任务,假设有多个任务,就会形成一个任务队列,意味着这些任务需要排队等待执行,但前端某些任务是非常耗时的,例如网络请求、定时器和事件监听,如果让他们和别的任务一样,老老实实排队等待执行的话,执行效率会非常低,甚至可能导致页面假死。

所以为了解决这个问题,浏览器为这些耗时任务开辟了另外的线程,主要包括http请求线程,浏览器定时触发器,浏览器事件触发线程,这些任务都是异步的。依靠宿主(浏览器、Node)的多线程,来使自己具备异步的属性。

浏览器的内核是多线程,它们在内核控制下相互配合以保持同步,一个浏览器至少实现3个常驻线程

    • Javascript引擎线程(浏览器无论什么时候都只有一个JS线程在运行)
  • UI渲染线程(与Javascript引擎线程互斥,Javascript引擎线程执行时,UI渲染线程被挂起,当Javascript引擎空闲时,UI渲染线程立即执行)
  • 事件触发线程(当一个事件被触发时该线程会把事件添加到待处理队列的对位,等待JS引擎处理。这些事件可来自Javascript引擎当前执行的代码块,如setTimeout,也可以来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等。)

下图可以说明浏览器的三个主要线程

img

从图片可以看出,JS引擎线程是单线程,它从上到下执行任务,当执行到鼠标点击事件、setTimeout这些异步任务的话,它会交给浏览器的对应的线程执行,事件触发线程就会把回调函数插入到Javascript引擎线程的队尾,等待js线程执行。这个过程中,因为Javascript线程一直繁忙,所以UI渲染线程一直被挂起。

------------------------------------------------------------------------------------------------------------------------------------------------------

说到这里,基本上可以解决本文开篇所说的问题了。

第一个文本框的js是这样写的:

<input type="text" value="" name="input" onkeydown="console.log(this.value)">

一开始按下键盘a的时候,已经触发了事件,onkeydown为异步操作,它的函数体会交给事件监听进程,此时JS线程空闲,你以为UI线程会启动?事实上并不会,UI线程依然被挂起,原因是事件监听进程比UI线程更快执行(当事件触发线程与ui渲染线程发生冲突时,例如例子中的表单的输入与触发),事件监听进程把函数体插入到任务队列尾部,JS引擎执行就通过event loop机制,把这段函数体压进栈中执行。因JS引擎线程繁忙,所以UI渲染线程方面,a字符串没法渲染到dom里,所以console.log在获得文本框的值时,为”“(空)。之后js线程空闲,UI渲染线程执行,页面渲染出a出来。

第二个文本框的js是这样写的:

<input type="text" value="" name="input" onkeydown="var me = this;setTimeout(function(){console.log(me.value)},0);">

一开始按下键盘a的时候,触发了事件,js执行里面的函数,发现setTimeout是异步操作时,虽然它的延迟设为0,几乎是即是触发的,但是把函数丢给浏览器setTimeout定时器线程处理,此时js线程空闲,UI渲染线程把a渲染到页面,之后事件触发线程把回调函数插入js线程待处理的任务队列,js线程执行回调函数,最后浏览器及时输出字符串a。

八、最后说说js主线程

js除了处理自己的主线程的任务之外,还在背地里一直做一个工作,就是从任务队列(callback queue)中提取任务,放到主线程里执行,下图深入讲解了JS主线程的工作:

img

上图的WebAPIs,可以统一理解为浏览器为异步任务单独开辟的线程

上图的callback queue,就是上面所说的任务队列,里面放的就是回调函数体

JS主线程由堆(heap)与栈(stack)共同组成。函数的执行通过进栈出栈来执行,例如foo()函数,主线程把它推进栈,在执行它的过程中,发现还需要执行上面几个函数,所以JS主线程又陆陆续续把几个函数推进栈中,等到函数执行完,把他们一一推出栈,等到stack清空的时候,说明一个任务已经执行完成了,这时就会从callback queue中寻找下一个任务,把他推进栈中(这个寻找过程,就叫event loop,因为它总是循环查找任务队列里是否还有任务)

九、说说一些应用与实践吧

AJAX发送异步请求,浏览器做了什么?

1.JS创建了一个ajax请求

2.浏览器另外打开一个ajax引擎线程,执行ajax请求

3.执行的到响应后将回调函数体放入任务队列

4.js线程执行任务队列中的回调函数

参考内容:

https://www.cnblogs.com/woodyblog/p/6061671.html (博客主要参考)

https://zhuanlan.zhihu.com/p/23659122?refer=dreawer

https://blog.csdn.net/w2765006513/article/details/53743051

https://blog.csdn.net/baidu_24024601/article/details/51861792

https://www.bilibili.com/video/av18833649


Last update: November 9, 2024