异步、promise、axios

329 阅读7分钟

本文目录:

  1. 同步模式、异步模式
  2. 线程、事件循环和任务队列
  3. 异步和回调的关系
  4. 判断同步异步
  5. 异步举例
  6. 异步任务有两个结果(成功或失败),怎么办
  7. promise

前置背景:

  • JS的执行环境是单线程single thread,就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。
  • 这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行
  • 为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)

1. 同步模式、异步模式

同步模式就是后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;

异步模式则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行队列上的后一个任务,而是执行回调函数;后一个任务则是不等前一个任务的回调函数的执行而执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。

异步模式非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,异步模式 甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

2. 线程、事件循环和任务队列

  • Javascript是单线程的,但却能执行异步任务,这主要是因为 JS 中存在事件循环(Event Loop)和任务队列(Task Queue)

  • 事件循环

JS 会创建一个类似于 while (true) 的循环,每执行一次循环体的过程称之为Tick。每次Tick的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。 待处理的事件会存储在一个任务队列中,也就是每次Tick会查看任务队列中是否有需要执行的任务。

  • 任务队列: 单线程只有前一个任务结束,才能执行下一个任务。如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JS语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如onclicksetTimeout,ajax 处理的方式都不同,这些异步操作是由浏览器内核的webcore来执行的,webcore包含3种 webAPI,分别是DOM Bindingnetworktimer模块。

  • DOM Binding 模块处理一些DOM绑定事件,如onclick事件触发时,回调函数会立即被webcore添加到任务队列中。

  • network 模块处理Ajax请求,在网络请求返回时,才会将对应的回调函数添加到任务队列中。

  • timer 模块会对setTimeout等计时器进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。

  • 任务分成两种,一种是同步任务,一种是异步任务

  • 同步任务:只有前一个任务执行完成后,才可执行下一个任务,在主线程中

  • 异步任务:这个队列的所有任务都是不进入主线程执行,而是被浏览提供的线程执行,当执行完毕后就会产生一个回调函数,并且通知主线程,在主线程执行完当前所执行的任务后,就会调取最早通知自己的回调函数,使其进入主线程中执行,比如ajax请求,在主线程中呈现的就是请求结果。

  • 解析: (1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
    (2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件(回调函数callback)。
    (3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

“任务队列”是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在”任务队列”中添加一个事件,表示相关的异步任务可以进入”执行栈”了。主线程读取”任务队列”,就是读取里面有哪些事件。

“任务队列”中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入”任务队列”,等待主线程读取。

所谓”回调函数”(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

“任务队列”是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,”任务队列”上第一位的事件就自动进入主线程。但是,由于存在后文提到的”定时器”功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

  • 异步不会阻塞代码执行,同步会阻塞代码执行。

如果能直接拿到结果,就是同步,拿不到结果不会离开;

如果不能直接拿到结果,就是异步,每隔一段时间问询,就是轮询,通知已到号,就是回调;

回调call back ,写了却不调用,给别人调用的函数,就是回调。

3. 异步和回调的关系?

区别:异步任务需要用到回调函数来通知结果,也可以用轮询来获取结果;回调函数也可以用在同步任务里

4. 判断同步异步

如果一个函数的返回值处于以下三个东西内部,那么这个函数就是异步函数:

  • setTimeout
  • AJAX(即XMLHttpRequest)
  • AddEventListener

5. 异步举例

以AJAX为例:

请求JSON时,request.send()之后,并不能直接得到response,JS中发网络请求,成功得到响应的大概时间段是几百毫秒到2秒钟之间,就是必须等到readyState变为4后,浏览器回头(将来)调用request.onreadystatechange函数,才能得到request.response,这跟餐厅给你发送微信提醒的过程是类似的。

6. 异步任务有两个结果(成功或失败),怎么办?

两种方法来处理:

方法一: 回调接受两个参数

fs.readFile('./1.txt',(error,data)=>{
  if(error){console.log('失败');return}
  console.log(data.toString()) // 成功
})

方法二: 搞两个回调

以上方法一、方法二,都有问题:

  1. 不规范,名称五花八门;

  2. 容易出现'回调地狱',代码变得看不懂;

  3. 很难进行错误处理;

7. 解决以上问题的promise思想解决方案,下面以AJAX为例,来解释Promise的用法:

参考文档: