一直以来都知道 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举例。(●ˇ∀ˇ●)

同步

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

console.log(1);
alert("同步");
console.log(2);
// 结果:
// 1
// 同步
// 2

异步

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

console.log(1);
setTimeout(() =## {
alert("异步");
},0);
console.log(2);
// 结果:
// 1
// 2
// 异步

为什么JavaScript要采用异步编程

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

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

异步运行机制:

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

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

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

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

举个例子:

<button οnclick="updateSync()">同步</button>
<button οnclick="updateAsync()">异步</button>
<div id="output"></div>
<script>
function updateSync() {
for (var i = 0; i < 1000000; i++) {
document.getElementById('output').innerHTML = i;
}
}
function updateAsync() {
var i = 0;
function updateLater() {
document.getElementById('output').innerHTML = (i++);
if (i < 1000000) {
setTimeout(updateLater, 0);
}
}
updateLater();
}
</script>

点击 同步按钮会调用 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叫做回调函数。如果没有名称(函数表达式),就叫做匿名回调函数。

简单的举个小例子:

function test (n,fn){
console.log(n);
fn && fn(n);
}
console.log(1);
test(2);
test(3,function(n){
console.log(n+1)
});
console.log(5)
// 结果
// 1
// 2
// 3
// 4
// 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这里就不作太多的赘述了。

var xhr = new XMLHttpRequest();
xhr.open("GET", url, false); //同步方式请求
xhr.open("GET", url, true); //异步
xhr.send();

同步 Ajax请求:

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

异步 Ajax请求:

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

setInterval&setTimeout

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

setInterval(() =## {
alert(1)
},2000)
// 每隔2s弹出一次1
setTimeout(() =## {
alert(2)
},2000)
// 进入页面后2s弹出2,则不会再次弹出

requestAnimationFarme

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

举个小例子:

var progress = 0;
//回调函数
function render() {
progress += 1; //修改图像的位置
if (progress < 100) {
//在动画没有结束前,递归渲染
window.requestAnimationFrame(render);
}
}
//第一帧渲染
window.requestAnimationFrame(render);

Object.observe - 观察者

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

var obj = {a: 1};
Object.observe(obj, output);
obj.b = 2;
obj.a = 2;
Object.defineProperties(obj, {a: { enumerable: false}}); //修改属性设定
delete obj.b;
function output(change) {
console.log(1)
}

Promise

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

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

var p1 = Promise.resolve(3);
var p2 = 42;
var p3 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, 'foo');
});
Promise.all([p1, p2, p3]).then(function(values) {
console.log(values);
});
// expected output: Array [3, 42, "foo"]

Generator&Async/Await

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

function * greneratorDome(){
yield "Hello";
yield "World";
return "Ending";
}
let grenDome = greneratorDome();
console.log(grenDome.next());
// {value: "Hello", done: false}
console.log(grenDome.next());
// {value: "World", done: false}
console.log(grenDome.next());
// {value: "Ending", done: true}
console.log(grenDome.next());
// {value: undefined, done: true}

粗略实现 Generator

function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

Async/Await与 Generator类似, Async/await是 Javascript编写异步程序的新方法。以往的异步方法无外乎回调函数和 Promise。但是 Async/await建立于Promise之上,个人理解是使用了 Generator函数做了语法糖。 async函数就是隧道尽头的亮光,很多人认为它是异步操作的终极解决方案。

function a(){
return new Promise((resolve,reject) =## {
console.log("a函数")
resolve("a函数")
})
}
function b (){
return new Promise((resolve,reject) =## {
console.log("b函数")
resolve("b函数")
})
}
async function dome (){
let A = await a();
let B = await b();
return Promise.resolve([A,B]);
}
dome().then((res) =## {
console.log(res);
});

Node.js异步I/O

当我们发起 IO请求时,调用的是各个不同平台的操作系统内部实现的线程池内的线程。这里的 IO请求可不仅仅是读写磁盘文件,在 *nix中,将计算机抽象了一层,磁盘文件、硬件、套接字等几乎所有计算机资源都被抽象为文件,常说的 IO请求就是抽象后的文件。完成 Node整个异步 IO环节的有事件循环、观察者、请求对象。

事件循环机制

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。于是就有一个概念,任务队列。如果排队是因为计算量大, CPU忙不过来,倒也算了,但是很多时候 CPU是闲着的,因为 IO设备(输入输出设备)很慢(比如 Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

事件循环是 Node的自身执行模型,正是事件循环使得回调函数得以在 Node中大量的使用。在进程启动时 Node会创建一个 while(true)死循环,这个和 Netty也是一样的,每次执行循环体,都会完成一次 Tick。每个 Tick的过程就是查看是否有事件等待被处理。如果有,就取出事件及相关的回调函数,并执行关联的回调函数。如果不再有事件处理就退出进程。

线程只会做一件事情,就是从事件队列里面取事件、执行事件,再取事件、再事件。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。

while(true) {
var message = queue.get();
execute(message);
}

我们可以把整个事件循环想象成一个事件队列,在进入事件队列时开始对事件进行弹出操作,直至事件为 0为止。

process.nextTick

process.nextTick()方法可以在当前"执行栈"的尾部-->下一次 EventLoop(主线程读取"任务队列")之前-->触发 process指定的回调函数。也就是说,它指定的任务总是发生在所有异步任务之前,当前主线程的末尾。( nextTick虽然也会异步执行,但是不会给其他 io事件执行的任何机会);

process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){console.log(2);});
});
setTimeout(function C() {
console.log(3');
}, 0);
// 1
// 2
// 3

异步过程的构成要素

异步函数实际上很快就调用完成了,但是后面还有工作线程执行异步任务,通知主线程,主线程调用回调函数等很多步骤。我们把整个过程叫做异步过程,异步函数的调用在整个异步过程中只是一小部分。

一个异步过程的整个过程:主线程发一起一个异步请求,相应的工作线程接收请求并告知主线程已收到通知(异步函数返回);主线程可以继续执行后面的代码,同时工作线程执行异步任务;工作线程完成工作后,通知主线程;主线程收到通知后,执行一定的动作(调用回调函数)。

它可以叫做异步过程的发起函数,或者叫做异步任务注册函数。 args是这个函数需要的参数, callbackFn(回调函数)也是这个函数的参数,但是它比较特殊所以单独列出来。所以,从主线程的角度看,一个异步过程包括下面两个要素:

  1. 发起函数;

  2. 回调函数callbackFn

它们都是主线程上调用的,其中注册函数用来发起异步过程,回调函数用来处理结果。

举个具体的栗子:

setTimeout(function,1000);

其中 setTimeout就是异步过程的发起函数, function是回调函数。

注:前面说得形式 A(args...,callbackFn)只是一种抽象的表示,并不代表回调函数一定要作为发起函数的参数,例如:

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx;
xhr.open('GET', url);
xhr.send();  

总结

JavaScript的异步编程模式不仅是一种趋势,而且是一种必要,因此作为 HTML5开发者是非常有必要掌握的。采用第三方的异步编程库和异步同步化的方法,会让代码结构相对简洁,便于维护,推荐开发人员掌握一二,提高团队开发效率。

作者:Aaron

https://segmentfault.com/a/1190000019253700

原创系列推荐



4. 
5. 
6. 
7. 

回复“加群”与大佬们一起交流学习~

点这,与大家一起分享本文吧~

【JS】368- 浅析JavaScript异步的更多相关文章

  1. 浅析JavaScript异步

    一直以来都知道JavaScript是一门单线程语言,在笔试过程中不断的遇到一些输出结果的问题,考量的是对异步编程掌握情况.一般被问到异步的时候脑子里第一反应就是Ajax,setTimseout...这 ...

  2. javascript异步延时载入及推断是否已载入js/css文件

    <html> <head> <script type="text/javascript"> /**======================= ...

  3. javascript异步延时加载及判断是否已加载js/css文件

    <html> <head> <script type="text/javascript"> /**======================= ...

  4. Func-Chain.js 另一种思路的javascript异步编程解决方案

    本文转载自:https://www.ctolib.com/panruiplay-func-chain.html Func-Chain.js 另一种思路的javascript异步编程,用于解决老式的回调 ...

  5. JavaScript异步编程的主要解决方案—对不起,我和你不在同一个频率上

    众所周知(这也忒夸张了吧?),Javascript通过事件驱动机制,在单线程模型下,以异步的形式来实现非阻塞的IO操作.这种模式使得JavaScript在处理事务时非常高效,但这带来了很多问题,比如异 ...

  6. JavaScript异步编程原理

    众所周知,JavaScript 的执行环境是单线程的,所谓的单线程就是一次只能完成一个任务,其任务的调度方式就是排队,这就和火车站洗手间门口的等待一样,前面的那个人没有搞定,你就只能站在后面排队等着. ...

  7. JavaScript异步编程(2)- 先驱者:jsDeferred

    JavaScript当前有众多实现异步编程的方式,最为耀眼的就是ECMAScript 6规范中的Promise对象,它来自于CommonJS小组的努力:Promise/A+规范. 研究javascri ...

  8. 【转】JavaScript 异步进化史

    前言 JS 中最基础的异步调用方式是 callback,它将回调函数 callback 传给异步 API,由浏览器或 Node 在异步完成后,通知 JS 引擎调用 callback.对于简单的异步操作 ...

  9. Javascript 异步加载详解(转)

    本文总结一下浏览器在 javascript 的加载方式. 关键词:异步加载(async loading),延迟加载(lazy loading),延迟执行(lazy execution),async 属 ...

随机推荐

  1. [FPGA]Verilog实现寄存器LS374

    目录 想说的话... 正文 IC介绍 电路连接图 功能表 逻辑图 实验原理 单元实现_D触发器 整体实现(完整代码) 想说的话... 不久前正式开通了博客,以后有空了会尽量把自己学习过程中的心得或者感 ...

  2. 详解 PHP 中的三大经典模式

    单例模式 单例模式的含义: 作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统全局地提供这个实例.它不会创建实例副本,而是会向单例类内部存储的实例返回一个引用. 单例模式 ...

  3. 最省钱的爬虫解决方案,比IP代理更划算

    现状: 1.网上提供代理IP池的解决方案非常多,价格也有高有低,包天/月/年的都有,品质都要靠自己去尝试. 2.试过之后,发现成本相对高,每月要花200~300元, 所以希望研究一下是否有更性价比高的 ...

  4. node 后台使用增删改查(4)

    无论node还是java增删改查都是一样的原理,变得是配合框架使用时候有简便方法而已. 这里我接着上一篇开始讲,使用同一个数据库(数据库创建)这里必须创建了数据库 优化:为了维护方便这里我们把sql语 ...

  5. 和SharpDX坑爹的Variant刚正面

    和SharpDX坑爹的Variant刚正面 几个月前我写了和篇文章<.NET中生成动态验证码>文章,其实里面藏着一个大坑.运行里面的代码,会发现运行的gif图片并没有循环播放: 细心的网友 ...

  6. linux磁盘分区三步走

    为了便于理解硬盘的物理结构 ,可将硬盘看作一个圆,它是坚硬金属材料制成的涂以磁性介质的盘片,不同容量硬盘的盘片数不等.每个盘有两面,都可记录信息.要了解硬盘的物理结构,需要弄懂磁道.扇区.柱面.簇等几 ...

  7. vue e.path 移动端兼容

    作用 e.path 用来获取点击元素及以上所有父元素的一个数组 问题 当在移动端会有获取不到e.path的问题 不兼容 解决 let path = event.path || (event.compo ...

  8. Java NIO 三大组件之 Buffer

    NIO大三组件 之Buffer 一.什么是Buffer Buffer是用于特定原始类型的数据的容器. 它的实质就是一组数组,用于存储不同类型的数据. 二.缓冲区的类型 缓冲区类型除了Boolean值类 ...

  9. python logging模块小记

    1.简单的将日志打印到屏幕 import logging logging.debug('This is debug message') logging.info('This is info messa ...

  10. 中标麒麟neokylin信息查看

    中标麒麟Neokylin系统版本信息: # nkvers ############## NeoKylin Linux Version################# Release: NeoKyli ...