快来加入我们吧!
"小和山的菜鸟们",为前端开发者提供技术相关资讯以及系列基础文章。为更好的用户体验,请您移至我们官网小和山的菜鸟们 ( https://xhs-rookies.com/ ) 进行学习,及时获取最新文章。
"Code tailor" ,如果您对我们文章感兴趣、或是想提一些建议,微信关注 “小和山的菜鸟们” 公众号,与我们取的联系,您也可以在微信上观看我们的文章。每一个建议或是赞同都是对我们极大的鼓励!
同步与异步
前言
在开始学习之前,我们想要告诉您的是,本文章是对JavaScript
语言知识中 “异步操作” 部分的总结,如果您已掌握下面知识事项,则可跳过此环节直接进入题目练习
- 单线程
- 同步概念
- 异步概念
- 异步操作的模式
- 异步操作的流程控制
- 定时器的创建和清除
如果您对某些部分有些遗忘,👇🏻 已经为您准备好了!
汇总总结
单线程
单线程指的是,JavaScript
只在一个线程上运行。也就是说,JavaScript
同时只能执行一个任务,其他任务都必须在后面排队等待。
JavaScript
之所以采用单线程,而不是多线程,跟历史有关系。JavaScript
从诞生起就是单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。
单线程的好处:
- 实现起来比较简单
- 执行环境相对单纯
单线程的坏处:
- 坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行
如果排队是因为计算量大,CPU
忙不过来,倒也算了,但是很多时候 CPU
是闲着的,因为 IO
操作(输入输出)很慢(比如 Ajax
操作从网络读取数据),不得不等着结果出来,再往下执行。JavaScript
语言的设计者意识到,这时 CPU
完全可以不管 IO
操作,挂起处于等待中的任务,先运行排在后面的任务。等到 IO
操作返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是 JavaScript
内部采用的**“事件循环”机制**(Event Loop
)。
单线程虽然对 JavaScript
构成了很大的限制,但也因此使它具备了其他语言不具备的优势。如果用得好,JavaScript
程序是不会出现堵塞的,这就是为什么 Node
可以用很少的资源,应付大流量访问的原因。
为了利用多核 CPU
的计算能力,HTML5
提出 Web Worker
标准,允许 JavaScript
脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM
。所以,这个新标准并没有改变 JavaScript
单线程的本质。
同步
同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。这样的执行流程容易分析程序在执行到代码任意位置时的状态(比如变量的值)。
同步操作的例子可以是执行一次简单的数学计算:
let xhs = 3
xhs = xhs + 4
在程序执行的每一步,都可以推断出程序的状态。这是因为后面的指令总是在前面的指令完成后才会执行。等到最后一条指定执行完毕,存储在 xhs
的值就立即可以使用。
首先,操作系统会在栈内存上分配一个存储浮点数值的空间,然后针对这个值做一次数学计算,再把计算结果写回之前分配的内存中。所有这些指令都是在单个线程中按顺序执行的。在低级指令的层面,有充足的工具可以确定系统状态。
异步
异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。异步操作经常是必要的,因为强制进程等待一个长时间的操作通常是不可行的(同步操作则必须要等)。如果代码要访问一些高延迟的资源,比如向远程服务器发送请求并等待响应,那么就会出现长时间的等待。
异步操作的例子可以是在定时回调中执行一次简单的数学计算:
let xhs = 3
setTimeout(() => (xhs = xhs + 4), 1000)
这段程序最终与同步代码执行的任务一样,都是把两个数加在一起,但这一次执行线程不知道 xhs
值何时会改变,因为这取决于回调何时从消息队列出列并执行。
异步代码不容易推断。虽然这个例子对应的低级代码最终跟前面的例子没什么区别,但第二个指令块(加操作及赋值操作)是由系统计时器触发的,这会生成一个入队执行的中断。到底什么时候会触发这个中断,这对 JavaScript
运行时来说是一个黑盒,因此实际上无法预知(尽管可以保证这发生在当前线程的同步代码执行之后,否则回调都没有机会出列被执行)。无论如何,在排定回调以后基本没办法知道系统状态何时变化。
为了让后续代码能够使用 xhs
,异步执行的函数需要在更新 xhs
的值以后通知其他代码。如果程序不需要这个值,那么就只管继续执行,不必等待这个结果了。
任务队列和事件循环
JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue
),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)
首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。
异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。
JavaScript
引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop
)。
维基百科对事件循环的定义是:“事件循环是一个程序结构,用于等待和发送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”。
异步操作的模式
回调函数
回调函数是异步操作最基本的方法。
下面是两个函数 f1
和 f2
,编程的意图是 f2
必须等到 f1
执行完成,才能执行。
function f1() {
// ...
}
function f2() {
// ...
}
f1()
f2()
上面代码的问题在于,如果 f1
是异步操作,f2
会立即执行,不会等到 f1
结束再执行。
这时,可以考虑改写 f1
,把 f2
写成 f1
的回调函数。
function f1(callback) {
// ...
callback()
}
function f2() {
// ...
}
f1(f2)
回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(coupling
),使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。
异步操作的流程控制
如果有多个异步操作,就存在一个流程控制的问题:如何确定异步操作执行的顺序,以及如何保证遵守这种顺序。
function async(arg,