专栏名称: JavaScript
面向JavaScript爱好人员提供:前端最新资讯、原创内容、JavaScript、HTML5、Ajax、jQuery、Node.js等一系列教程和经验分享。
目录
相关文章推荐
51好读  ›  专栏  ›  JavaScript

浅析JavaScript异步

JavaScript  · 公众号  · Javascript  · 2019-05-23 08:00

正文

一直以来都知道 JavaScript 是一门单线程语言,在笔试过程中不断的遇到一些输出结果的问题,考量的是对异步编程掌握情况。一般被问到异步的时候脑子里第一反应就是 Ajax, setTimseout...这些东西。在平时做项目过程中,基本大多数操作都是异步的。 JavaScript异步都是通过回调形式完成的,开发过程中一直在处理回调,可能不知不觉中自己就已经处在 回调地狱中。

浏览器线程

在开始之前简单的说一下浏览器的线程,对浏览器的作业有个基础的认识。之前说过 JavaScript是单线程作业,但是并不代表浏览器就是单线程的。

在 JavaScript引擎中负责解析和执行 JavaScript代码的线程只有一个。但是除了这个主进程以外,还有其他很多辅助线程。那么诸如 onclick回调, setTimeout, Ajax这些都是怎么实现的呢?即浏览器搞了几个其他线程去辅助 JavaScript线程的运行。

浏览器有很多线程,例如:

  1. GUI渲染线程 - GUI渲染线程处于挂起状态的,也就是冻结状态

  2. JavaScript引擎线程 - 用于解析JavaScript代码

  3. 定时器触发线程 - 浏览器定时计数器并不是 js引擎计数

  4. 浏览器事件线程 - 用于解析BOM渲染等工作

  5. http线程 - 主要负责数据请求

  6. EventLoop轮询处理线程 - 事件被触发时该线程会把事件添加到待处理队列的队尾

  7. 等等等

从上面来看可以得出,浏览器其实也做了很多事情,远远的没有想象中的那么简单,上面这些线程中 GUI渲染线程JavaScript引擎线程浏览器事件线程是浏览器的常驻线程。

当浏览器开始解析代码的时候,会根据代码去分配给不同的辅助线程去作业。

进程

进程是指在操作系统中正在运行的一个应用程序

线程

线程是指进程内独立执行某个任务的一个单元。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)。

进程中包含线程,一个进程中可以有N个进程。我们可以在电脑的任务管理器中查看到正在运行的进程,可以认为一个进程就是在运行一个程序,比如用浏览器打开一个网页,这就是开启了一个进程。但是比如打开3个浏览器,那么就开启了3个进程。

同步&异步

既然要了解同步异步当然要简单的说一下同步和异步。说到同步和异步最有发言权的真的就属 Ajax了,为了让例子更加明显没有使用 Ajax举例。(●ˇ∀ˇ●)

同步

同步会逐行执行代码,会对后续代码造成阻塞,直至代码接收到预期的结果之后,才会继续向下执行。

  1. console.log(1);

  2. alert("同步");

  3. console.log(2);


  4. // 结果:

  5. // 1

  6. // 同步

  7. // 2

异步

如果在函数返回的时候,调用者还不能够得到预期结果,而是将来通过一定的手段得到结果(例如回调函数),这就是异步。

  1. console.log(1);

  2. setTimeout(() =## {

  3. alert ("异步");

  4. },0);

  5. console.log(2);


  6. // 结果:

  7. // 1

  8. // 2

  9. // 异步

为什么JavaScript要采用异步编程

一开始就说过, JavaScript是一种单线程执行的脚本语言(这可能是由于历史原因或为了简单而采取的设计)。它的单线程表现在任何一个函数都要从头到尾执行完毕之后,才会执行另一个函数,界面的更新、鼠标事件的处理、计时器( setTimeoutsetInterval等)的执行也需要先排队,后串行执行。假如有一段 JavaScript从头到尾执行时间比较长,那么在执行期间任何 UI更新都会被阻塞,界面事件处理也会停止响应。这种情况下就需要异步编程模式,目的就是把代码的运行打散或者让 IO调用(例如 AJAX)在后台运行,让界面更新和事件处理能够及时地运行。

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

异步运行机制:

  1. 所有同步任务都在主线程上执行,形成一个执行栈。

  2. 主线程之外,还存在一个 任务队列。只要异步任务有了运行结果,就在 任务队列之中放置一个事件。

  3. 一旦 执行栈中的所有同步任务执行完毕,系统就会读取 任务队列,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

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

举个例子:

  1. id="output">

  2. function updateSync() {

  3. for (var i = 0; i < 1000000; i++) {

  4. document.getElementById('output').innerHTML = i;

  5. }

  6. }

  7. function updateAsync() {

  8. var i = 0;

  9. function updateLater() {

  10. document.getElementById('output').innerHTML = (i++);

  11. if (i < 1000000) {

  12. setTimeout(updateLater, 0);

  13. }

  14. }

  15. updateLater();

  16. }

点击 同步按钮会调用 updateSync的同步函数,逻辑非常简单,循环体内每次更新 output结点的内容为 i。如果在其他多线程模型下的语言,你可能会看到界面上以非常快的速度显示从 0到 999999后停止。但是在 JavaScript中,你会感觉按钮按下去的时候卡了一下,然后看到一个最终结果 999999,而没有中间过程,这就是因为在 updateSync函数运行过程中 UI更新被阻塞,只有当它结束退出后才会更新 UI。反之,当点击 异步的时候,会明显的看到 Dom在逐步更新的过程。

从上面的例子中可以明显的看出,异步编程对于 JavaScript来说是多么多么的重要。

异步编程有什么好处

从编程方式来讲当然是同步编程的方式更为简单,但是同步有其局限性一是假如是单线程那么一旦遇到阻塞调用,会造成整个线程阻塞,导致 cpu无法得到有效利用,而浏览器的 JavaScript执行和浏览器渲染是运行在单线程中,一旦遇到阻塞调用不仅意味 JavaScript的执行被阻塞更意味整个浏览器渲染也被阻塞这就导致界面的卡死,若是多线程则不可避免的要考虑互斥和同步问题,而互斥和同步带来复杂度也很大,实际上浏览器下因为同时只能执行一段 JavaScript代码这意味着不存在互斥问题,但是同步问题仍然不可避免,以往回调风格中异步的流程控制(其实就是同步问题)也比较复杂。浏览器端的编程方式也即是 GUI编程,其本质就是事件驱动的(鼠标点击, Http请求结束等)异步编程更为自然。

突然有个疑问,既然如此为什么 JavaScript没有使用多线程作业呢?就此就去 Google了一下 JavaScript多线程,在 HTML5推出之后是提供了多线程只是比较局限。在使用多线程的时候无法使用 window对象。若 JavaScript使用多线程,在 A 线程中正在操作 DOM,但是 B线程中已经把该 DOM已经删除了(只是简单的小栗子,可能还有很多问题,至于这些历史问题无从考究了)。会给编程作业带来很大的负担。就我而言我想这也就说明了为什么 JavaScript没有使用异步编程的原因吧。

异步与回调

回调到底属于异步么?会想起刚刚开始学习 JavaScript的时候常常吧这两个概念混合在一起。在搞清楚这个问题,首先要明白什么是回调函数。

百科:回调函数是一个函数,它作为参数传递给另一个函数,并在父函数完成后执行。回调的特殊之处在于,出现在“父类”之后的函数可以在回调执行之前执行。另一件需要知道的重要事情是如何正确地传递回调。这就是我经常忘记正确语法的地方。

通过上面的解释可以得出,回调函数本质上其实就是一种设计模式,例如我们熟悉的 JQuery也只不过是遵循了这个设计原则而已。在 JavaScript中,回调函数具体的定义为:函数 A作为参数(函数引用)传递到另一个函数 B中,并且这个函数 B执行函数 A。我们就说函数 A叫做回调函数。如果没有名称(函数表达式),就叫做匿名回调函数。

简单的举个小例子:

  1. function test (n,fn){

  2. console.log(n);

  3. fn && fn(n);

  4. }

  5. console.log(1);

  6. test(2);

  7. test(3,function(n){

  8. console.log(n+1)

  9. });

  10. console.log(5)


  11. // 结果

  12. // 1

  13. // 2

  14. // 3

  15. // 4

  16. // 5

通过上面的代码输出的结果可以得出回调函数不一定属于异步,一般同步会阻塞后面的代码,通过输出结果也就得出了这个结论。回调函数,一般在同步情境下是最后执行的,而在异步情境下有可能不执行,因为事件没有被触发或者条件不满足。

回调函数应用场景

  1. 资源加载:动态加载js文件后执行回调,加载iframe后执行回调,ajax操作回调,图片加载完成执行回调,AJAX等等。

  2. DOM事件及Node.js事件基于回调机制(Node.js回调可能会出现多层回调嵌套的问题)。

  3. setTimeout的延迟时间为0,这个hack经常被用到,settimeout调用的函数其实就是一个callback的体现

  4. 链式调用:链式调用的时候,在赋值器(setter)方法中(或者本身没有返回值的方法中)很容易实现链式调用,而取值器(getter)相对来说不好实现链式调用,因为你需要取值器返回你需要的数据而不是this指针,如果要实现链式方法,可以用回调函数来实现。

  5. setTimeout、setInterval的函数调用得到其返回值。由于两个函数都是异步的,即:调用时序和程序的主流程是相对独立的,所以没有办法在主体里面等待它们的返回值,它们被打开的时候程序也不会停下来等待,否则也就失去了setTimeout及setInterval的意义了,所以用return已经没有意义,只能使用callback。callback的意义在于将timer执行的结果通知给代理函数进行及时处理。

JavaScript中的那些异步操作

JavaScript既然有很多的辅助线程,不可能所有的工作都是通过主线程去做,既然分配给辅助线程去做事情。

XMLHttpRequest

XMLHttpRequest 对象应该不是很陌生的,主要用于浏览器的数据请求与数据交互。 XMLHttpRequest对象提供两种请求数据的方式,一种是 同步,一种是 异步。可以通过参数进行配置。默认为异步。

对于 XMLHttpRequest这里就不作太多的赘述了。

  1. var xhr = new XMLHttpRequest();

  2. xhr.open("GET", url, false); //同步方式请求

  3. xhr.open("GET", url, true); //异步

  4. xhr.send();

同步 Ajax请求:

当请求开始发送时, 浏览器事件线程通知 主线程,让 Http线程发送数据请求,主线程收到请求之后,通知 Http线程发送请求, Http线程收到 主线程通知之后就去请求数据,等待服务器响应,过了 N年之后,收到请求回来的数据,返回给 主线程数据已经请求完成, 主线程把结果返回给了 浏览器事件线程,去完成后续操作。

异步 Ajax请求:

当请求开始发送时, 浏览器事件线程通知, 浏览器事件线程通知 主线程,让 Http线程发送数据请求,主线程收到请求之后,通知 Http线程发送请求, Http线程收到 主线程通知之后就去请求数据,并通知 主线程请求已经发送, 主进程通知 浏览器事件线程已经去请求数据,则
浏览器事件线程,只需要等待结果,并不影响其他工作。

setInterval&setTimeout

setInterval与 setTimeout同属于异步方法,其异步是通过回调函数方式实现。其两者的区别则 setInterval会连续调用回调函数,则 setTimeout会延时调用回调函数只会执行一次。

  1. setInterval (() =## {

  2. alert(1)

  3. },2000)

  4. // 每隔2s弹出一次1

  5. setTimeout(() =## {

  6. alert(2)

  7. },2000)

  8. // 进入页面后2s弹出2,则不会再次弹出

requestAnimationFarme

requestAnimationFrame字面意思就是去请求动画帧,在没有 API之前都是基于 setInterval,与 setInterval相比, requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。具体一点讲,如果屏幕刷新率是 60Hz,那么回调函数就每 16.7ms被执行一次,如果刷新率是 75Hz,那么这个时间间隔就变成了 1000/75=13.3ms,换句话说就是, requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。

举个小例子:

  1. var progress = 0;

  2. //回调函数

  3. function render() {

  4. progress += 1; //修改图像的位置

  5. if (progress < 100) {

  6. //在动画没有结束前,递归渲染

  7. window.requestAnimationFrame(render);

  8. }

  9. }

  10. //第一帧渲染

  11. window.requestAnimationFrame(render);

Object.observe - 观察者

Object.observe是一个提供数据监视的 API,在 chrome中已经可以使用。是 ECMAScript  7 的一个提案规范,官方建议的是 谨慎使用级别,但是个人认为这个 API非常有用,例如可以对现在流行的 MVVM框架作一些简化和优化。虽然标准还没定,但是标准往往是滞后于实现的,只要是有用的东西,肯定会有越来越多的人去使用,越来越多的引擎会支持,最终促使标准的生成。从 observe字面意思就可以知道,这玩意儿就是用来做观察者模式之类。

  1. var obj = {a: 1};

  2. Object.observe(obj, output);

  3. obj.b = 2;

  4. obj.a = 2;

  5. Object.defineProperties(obj, {a: { enumerable: false}}); //修改属性设定

  6. delete obj.b;

  7. function output(change) {

  8. console.log(1)

  9. }

Promise

Promise是对异步编程的一种抽象。它是一个代理对象,代表一个必须进行异步处理的函数返回的值或抛出的异常。也就是说 Promise对象代表了一个异步操作,可以将异步对象和回调函数脱离开来,通过 then方法在这个异步操作上面绑定回调函数。

在Promise中最直观的例子就是 Promise.all统一去请求,返回结果。

  1. var p1 = Promise.resolve(3);

  2. var p2 = 42;

  3. var p3 = new Promise(function(resolve, reject) {

  4. setTimeout(resolve, 100, 'foo');

  5. });

  6. Promise.all([p1, p2, p3]).then(function(values) {

  7. console.log(values);

  8. });

  9. // expected output: Array [3, 42, "foo"]

Generator&Async/Await

ES6的 Generator却给异步操作又提供了新的思路,马上就有人给出了如何用 Generator来更加优雅的处理异步操作。 Generator函数是协程在 ES6的实现,最大特点就是可以交出函数的执行权(即暂停执行)。整个 Generator函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。 Generator函数的执行方法如下。

  1. function * greneratorDome(){

  2. yield "Hello";

  3. yield "World";

  4. return "Ending";

  5. }

  6. let grenDome = greneratorDome();

  7. console.log(grenDome.next());

  8. // {value: "Hello", done: false}

  9. console.log(grenDome.next());

  10. // {value: "World", done: false}

  11. console.log(grenDome.next());

  12. // {value: "Ending", done: true}

  13. console.log(grenDome.next());

  14. // {value: undefined, done: true}

粗略实现 Generator

  1. function makeIterator(array) {

  2. var nextIndex = 0;

  3. return {

  4. next: function() {

  5. return nextIndex < array.length ?

  6. {value: array[nextIndex++], done: false} :

  7. {value: undefined, done: true};

  8. }

  9. };

  10. }

  11. var it = makeIterator(['a', 'b']);

  12. it.next() // { value: "a", done: false }

  13. it





    请到「今天看啥」查看全文

推荐文章
奔波儿灞与灞波儿奔  ·  亲爱的,你倒是快点穿衣服啊!
8 年前
warfalcon  ·  数千人面对高房价不同的选择
7 年前
热门视频集汇  ·  下套全过程,为了家人赶紧看看!
7 年前