前言
身为前端,一直都知道异步编程怎么用,但是都没有记录过,此系列详细介绍一下我所理解的javascript中的异步编程。
链接扩展:
异步编程之–深度理解Promise
异步编程之–理解es6的Generators(生成器 )
异步编程之–理解es6中的Iterator(迭代器)
正文
异步编程是什么?为什么要使用异步编程?
我们都知道,javascript
从诞生之日起就是一门单线程的非阻塞的脚本语言。这是由其最初的用途来决定的:与浏览器交互。
javascripty有两大特点:单线程+非阻塞
-
单线程意味着,
javascript
代码在执行的任何时候,都只有一个主线程来处理所有的任务。单线程是必要的,也是javascript
这门语言的基石,原因在其最初也是最主要的执行环境——浏览器中,我们需要进行各种各样的dom操作。 -
试想一下 如果
javascript
是多线程的,那么当两个线程同时对dom
进行一项操作,例如一个向其添加事件,而另一个删除了这个dom
,此时该如何处理呢?因此,为了保证不会 发生类似于这个例子中的情景,javascript
选择只用一个主线程来执行代码,这样就保证了程序执行的一致性。 -
现如今人们也意识到,单线程在保证了执行顺序的同时也限制了
javascript
的效率,因此开发出了web worker
技术。这项技术号称让javascript
成为一门多线程语言。 -
然而,使用
web worker
技术开的多线程有着诸多限制,例如:所有新线程都受主线程的完全控制,不能独立执行。这意味着这些“线程” 实际上应属于主线程的子线程。另外,这些子线程并没有执行I/O操作的权限,只能为主线程分担一些诸如计算等任务。所以严格来讲这些线程并没有完整的功能,也因此这项技术并非改变了javascript
语言的单线程本质。 -
单线程就好像100个人在1个售票窗口买票,而多线程就好像100个人在100个窗口买票。但是js的特点决定了不能多线程,所以衍生了单线程+非阻塞。
-
而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如
I/O事件,ajax
)的时候,主线程会挂起(pending
)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。 -
这种就好像:100个人去买票,车站开放了1个窗口,但是车票的预订可以在网上或其他地方完成,到车站时候只需要排队取票。我们可以看到,开1个窗口,就相当于只有1个线程。然后把耗时的一些操作分成两部分,先把快速能做完的事情做了,这样保证它不会阻塞其他代码的运行。剩下耗时的部分再单独执行。这就是单线程阻塞式的异步实现机制。
javascript
的另一个特点是“非阻塞”,那么javascript
引擎到底是如何实现非阻塞异步呢?
具体JS如何实现异步?
方法:JS的事件循环机制(Event Loop)
单线程就意味着,所有任务需要排队。所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
异步执行的运行机制如下:
- (1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- (2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- (3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- (4)主线程不断重复上面的第三步。
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
理解任务队列(task queue)
任务队列是放置异步任务事件的队列,任务队列可以存在多个,在同一任务队列内,按队列顺序被主线程取走;不同任务队列之间,存在着优先级,优先级高的优先获取(如用户I/O)。
任务队列的类型:
- 宏任务队列 (macrotask queue)
比如: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。 - 微任务队列 (microtask queue)
例如: process.nextTick, Promises, Object.observe, MutationObserver
为了更好理解,下面来分析一段代码:
console.log('script start');
setTimeout(() => {
console.log('setTimeout 1');
},500);
setTimeout(() => {
console.log('setTimeout 2');
});
Promise.resolve()
.then(() => {
console.log('promise 1');
})
.then(() => {
console.log('promise 2');
});
console.log('script end');
上述代码的执行结果为:
> "script start"
> "script end"
> "promise 1"
> "promise 2"
> "setTimeout 2"
> "setTimeout 1"
我们来分析一下执行逻辑:
- 1.浏览器从上往下执行,执行到
console.log('script start');
输出结果; - 2.执行到两个
setTimeout
时把的回调函数按顺序放入Macro Task的队列中。 - 3.执行 Promise时 把两个 then 的回调函数放入 Micro Task 栈中。
- 4.执行
console.log('script end');
输出结果。 - 5.这里我们就可以看出来,
macrotask
和microtask
执行的特征:在宏任务和微任务同时存在的时候,首先按顺序执行Micro Task 栈中的所有任务,等Micro Task 栈中的所有任务执行结束后再按先进入栈先执行的规则来执行Macro Task的队列中任务。
异步编程都有哪些解决方案?
-
回调函数(callback)–例:ajax
详解:理解回调函数
优点:简单、容易理解和实现。
缺点:是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套造成回调地狱的情况),而且每个任务只能指定一个回调函数。此外它不能使用 try catch 捕获错误,不能直接 return。 -
事件监听-利用定时器(setTimeout)
监听一个事件的执行,等到状态改变时在内部利用定时器执行其他操作。
优点:便于理解,可以绑定多个事件,有利于实现模块化。
缺点:是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。 -
promise
详解:Promise的特点和方法详解优点: 相比传统回调函数和事件更加合理和优雅,Promise是链式编程,有效的解决了令人头痛的回调地狱问题。
缺点:不易理解,编程代码量并没有减少很多,而且Promise的结果有成功和失败两种状态,只有异步操作的结果,可以决定当前是哪一种状态,外界的任何操作都无法改变这个状态,在执行过成功,不能终止。 -
生成器Generators
详解:理解es6的Generators(生成器 )优点:相较于以上三种,既可以解决回调地狱又可以在函数中间暂停执行,使用同步的方式去编写异步代码,代码的可读性更好,很好的解决了JavaScript中异步的问题。
缺点:手动执行时复杂,需要依赖执行器函数。在解决异步问题时对封装,promise等知识都需要用到,上手并不是很容易。 -
async awiat
详解:async表示该函数要做异步处理。await表示后面的代码是一个异步操作,等待该异步操作完成后再执行后面的动作。如果异步操作有返回的数据,则在左边用一个变量来接收它。优点:很优秀,确实是一个既好用、又简单的异步处理方法!
缺点:身为es7的语法,可能考虑兼容性
借鉴:https://blog.csdn.net/li123128/article/details/80650256
如果本文对你有帮助的话,请不要忘记给我点赞打call哦~o( ̄▽ ̄)do
有其他问题留言 over~