浅析JavaScript异步
<p>一直以来都知道<code>JavaScript</code>是一门单线程语言,在笔试过程中不断的遇到一些输出结果的问题,考量的是对异步编程掌握情况。一般被问到异步的时候脑子里第一反应就是<code>Ajax</code>,<code>setTimseout</code>...这些东西。在平时做项目过程中,基本大多数操作都是异步的。<code>JavaScript</code>异步都是通过回调形式完成的,开发过程中一直在处理回调,可能不知不觉中自己就已经处在<code>回调地狱</code>中。</p><blockquote>
<p>浏览器线程</p>
</blockquote>
<p>在开始之前简单的说一下浏览器的线程,对浏览器的作业有个基础的认识。之前说过<code>JavaScript</code>是单线程作业,但是并不代表浏览器就是单线程的。</p>
<p>在<code>JavaScript</code>引擎中负责解析和执行<code>JavaScript</code>代码的线程只有一个。但是除了这个主进程以外,还有其他很多辅助线程。那么诸如<code>onclick</code>回调,<code>setTimeout</code>,<code>Ajax</code>这些都是怎么实现的呢?即浏览器搞了几个其他线程去辅助<code>JavaScript</code>线程的运行。</p>
<p>浏览器有很多线程,例如:</p>
<ol>
<li>GUI渲染线程 - GUI渲染线程处于挂起状态的,也就是冻结状态</li>
<li>JavaScript引擎线程 - 用于解析JavaScript代码</li>
<li>定时器触发线程 - 浏览器定时计数器并不是 js引擎计数</li>
<li>浏览器事件线程 - 用于解析BOM渲染等工作</li>
<li>http线程 - 主要负责数据请求</li>
<li>EventLoop轮询处理线程 - 事件被触发时该线程会把事件添加到待处理队列的队尾</li>
<li>等等等</li>
</ol>
<p>从上面来看可以得出,浏览器其实也做了很多事情,远远的没有想象中的那么简单,上面这些线程中<code>GUI渲染线程</code>,<code>JavaScript引擎线程</code>,<code>浏览器事件线程</code>是浏览器的常驻线程。</p>
<p>当浏览器开始解析代码的时候,会根据代码去分配给不同的辅助线程去作业。</p>
<p><strong>进程</strong></p>
<p>进程是指在操作系统中正在运行的一个应用程序</p>
<p><strong>线程</strong></p>
<p>线程是指进程内独立执行某个任务的一个单元。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)。</p>
<p>进程中包含线程,一个进程中可以有N个进程。我们可以在电脑的任务管理器中查看到正在运行的进程,可以认为一个进程就是在运行一个程序,比如用浏览器打开一个网页,这就是开启了一个进程。但是比如打开3个浏览器,那么就开启了3个进程。</p>
<blockquote>
<p>同步&异步</p>
</blockquote>
<p>既然要了解同步异步当然要简单的说一下同步和异步。说到同步和异步最有发言权的真的就属<code>Ajax</code>了,为了让例子更加明显没有使用<code>Ajax</code>举例。(●ˇ∀ˇ●)</p>
<p><strong>同步</strong></p>
<p>同步会逐行执行代码,会对后续代码造成阻塞,直至代码接收到预期的结果之后,才会继续向下执行。</p>
<pre><code class="language-JavaScript">console.log(1);
alert("同步");
console.log(2);
//结果:
//1
//同步
//2
</code></pre>
<p><strong>异步</strong></p>
<p>如果在函数返回的时候,调用者还不能够得到预期结果,而是将来通过一定的手段得到结果(例如回调函数),这就是异步。</p>
<pre><code class="language-JavaScript">console.log(1);
setTimeout(() => {
alert("异步");
},0);
console.log(2);
//结果:
//1
//2
//异步
</code></pre>
<blockquote>
<p>为什么JavaScript要采用异步编程</p>
</blockquote>
<p>一开始就说过,<code>JavaScript</code>是一种单线程执行的脚本语言(这可能是由于历史原因或为了简单而采取的设计)。它的单线程表现在任何一个函数都要从头到尾执行完毕之后,才会执行另一个函数,界面的更新、鼠标事件的处理、计时器(<code>setTimeout、setInterval</code>等)的执行也需要先排队,后串行执行。假如有一段<code>JavaScript</code>从头到尾执行时间比较长,那么在执行期间任何<code>UI</code>更新都会被阻塞,界面事件处理也会停止响应。这种情况下就需要异步编程模式,目的就是把代码的运行打散或者让<code>IO</code>调用(例如<code>AJAX</code>)在后台运行,让界面更新和事件处理能够及时地运行。</p>
<p><code>JavaScript</code>语言的设计者意识到,这时主线程完全可以不管<code>IO</code>设备,挂起处于等待中的任务,先运行排在后面的任务。等到<code>IO</code>设备返回了结果,再回过头,把挂起的任务继续执行下去。</p>
<p>异步运行机制:</p>
<ol>
<li>所有同步任务都在主线程上执行,形成一个执行栈。</li>
<li>主线程之外,还存在一个<code>任务队列</code>。只要异步任务有了运行结果,就在<code>任务队列</code>之中放置一个事件。</li>
<li>一旦<code>执行栈</code>中的所有同步任务执行完毕,系统就会读取<code>任务队列</code>,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。</li>
<li>主线程不断重复上面的第三步。</li>
</ol>
<pre><code class="language-html"><button onclick="updateSync()">同步</button>
<button onclick="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>
</code></pre>
<p>点击<code>同步</code>按钮会调用<code>updateSync</code>的同步函数,逻辑非常简单,循环体内每次更新<code>output</code>结点的内容为<code>i</code>。如果在其他多线程模型下的语言,你可能会看到界面上以非常快的速度显示从<code>0</code>到<code>999999</code>后停止。但是在<code>JavaScript</code>中,你会感觉按钮按下去的时候卡了一下,然后看到一个最终结果<code>999999</code>,而没有中间过程,这就是因为在<code>updateSync</code>函数运行过程中<code>UI</code>更新被阻塞,只有当它结束退出后才会更新<code>UI</code>。反之,当点击<code>异步</code>的时候,会明显的看到<code>Dom</code>在逐步更新的过程。</p>
<p>从上面的例子中可以明显的看出,异步编程对于<code>JavaScript</code>来说是多么多么的重要。</p>
<blockquote>
<p>异步编程有什么好处</p>
</blockquote>
<p>从编程方式来讲当然是同步编程的方式更为简单,但是同步有其局限性一是假如是单线程那么一旦遇到阻塞调用,会造成整个线程阻塞,导致<code>cpu</code>无法得到有效利用,而浏览器的<code>JavaScript</code>执行和浏览器渲染是运行在单线程中,一旦遇到阻塞调用不仅意味<code>JavaScript</code>的执行被阻塞更意味整个浏览器渲染也被阻塞这就导致界面的卡死,若是多线程则不可避免的要考虑互斥和同步问题,而互斥和同步带来复杂度也很大,实际上浏览器下因为同时只能执行一段<code>JavaScript</code>代码这意味着不存在互斥问题,但是同步问题仍然不可避免,以往回调风格中异步的流程控制(其实就是同步问题)也比较复杂。浏览器端的编程方式也即是<code>GUI编程</code>,其本质就是事件驱动的(鼠标点击,<code>Http</code>请求结束等)异步编程更为自然。</p>
<p>突然有个疑问,既然如此为什么<code>JavaScript</code>没有使用多线程作业呢?就此就去<code>Google</code>了一下<code>JavaScript多线程</code>,在<code>HTML5</code>推出之后是提供了多线程只是比较局限。在使用多线程的时候无法使用<code>window</code>对象。若<code>JavaScript</code>使用多线程,在<code>A</code>线程中正在操作<code>DOM</code>,但是<code>B</code>线程中已经把该<code>DOM</code>已经删除了(只是简单的小栗子,可能还有很多问题,至于这些历史问题无从考究了)。会给编程作业带来很大的负担。就我而言我想这也就说明了为什么<code>JavaScript</code>没有使用多线程的原因吧。</p>
<blockquote>
<p>异步与回调</p>
</blockquote>
<p>回调到底属于异步么?会想起刚刚开始学习<code>JavaScript</code>的时候常常吧这两个概念混合在一起。在搞清楚这个问题,首先要明白什么是回调函数。</p>
<p>百科:回调函数;回调函数是一个函数,它作为参数传递给另一个函数,并在父函数完成后执行。回调的特殊之处在于,出现在“父类”之后的函数可以在回调执行之前执行。另一件需要知道的重要事情是如何正确地传递回调。这就是我经常忘记正确语法的地方。</p>
<p>通过上面的解释可以得出,回调函数本质上其实就是一种设计模式,例如我们熟悉的<code>JQuery</code>也只不过是遵循了这个设计原则而已。在<code>JavaScript</code>中,回调函数具体的定义为:函数<code>A</code>作为参数(函数引用)传递到另一个函数<code>B</code>中,并且这个函数<code>B</code>执行函数<code>A</code>。我们就说函数<code>A</code>叫做回调函数。如果没有名称(函数表达式),就叫做匿名回调函数。</p>
<p>简单的举个小例子:</p>
<pre><code class="language-javaScript">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
</code></pre>
<p>通过上面的代码输出的结果可以得出回调函数不一定属于异步,一般同步会阻塞后面的代码,通过输出结果也就得出了这个结论。回调函数,一般在同步情境下是最后执行的,而在异步情境下有可能不执行,因为事件没有被触发或者条件不满足。</p>
<p><strong>回调函数应用场景</strong></p>
<ol>
<li>资源加载:动态加载js文件后执行回调,加载iframe后执行回调,ajax操作回调,图片加载完成执行回调,AJAX等等。</li>
<li>DOM事件及Node.js事件基于回调机制(Node.js回调可能会出现多层回调嵌套的问题)。</li>
<li>setTimeout的延迟时间为0,这个hack经常被用到,settimeout调用的函数其实就是一个callback的体现</li>
<li>链式调用:链式调用的时候,在赋值器(setter)方法中(或者本身没有返回值的方法中)很容易实现链式调用,而取值器(getter)相对来说不好实现链式调用,因为你需要取值器返回你需要的数据而不是this指针,如果要实现链式方法,可以用回调函数来实现。</li>
<li>setTimeout、setInterval的函数调用得到其返回值。由于两个函数都是异步的,即:调用时序和程序的主流程是相对独立的,所以没有办法在主体里面等待它们的返回值,它们被打开的时候程序也不会停下来等待,否则也就失去了setTimeout及setInterval的意义了,所以用return已经没有意义,只能使用callback。callback的意义在于将timer执行的结果通知给代理函数进行及时处理。</li>
</ol>
<blockquote>
<p>JavaScript中的那些异步操作</p>
</blockquote>
<p><code>JavaScript</code>既然有很多的辅助线程,不可能所有的工作都是通过主线程去做,既然分配给辅助线程去做事情。</p>
<p><strong>XMLHttpRequest</strong></p>
<p><code>XMLHttpRequest</code>对象应该不是很陌生的,主要用于浏览器的数据请求与数据交互。<code>XMLHttpRequest</code>对象提供两种请求数据的方式,一种是<code>同步</code>,一种是<code>异步</code>。可以通过参数进行配置。默认为异步。</p>
<p>对于<code>XMLHttpRequest</code>这里就不作太多的赘述了。</p>
<pre><code class="language-JavaScript">var xhr = new XMLHttpRequest();
xhr.open("GET", url, false); //同步方式请求
xhr.open("GET", url, true); //异步
xhr.send();
</code></pre>
<p>同步<code>Ajax</code>请求:</p>
<p>当请求开始发送时,<code>浏览器事件线程</code>通知<code>主线程</code>,让<code>Http线程</code>发送数据请求,主线程收到请求之后,通知<code>Http线程</code>发送请求,<code>Http线程</code>收到<code>主线程</code>通知之后就去请求数据,等待服务器响应,过了<code>N</code>年之后,收到请求回来的数据,返回给<code>主线程</code>数据已经请求完成,<code>主线程</code>把结果返回给了<code>浏览器事件线程</code>,去完成后续操作。</p>
<p>异步<code>Ajax</code>请求:</p>
<p>当请求开始发送时,<code>浏览器事件线程</code>通知,<code>浏览器事件线程</code>通知<code>主线程</code>,让<code>Http线程</code>发送数据请求,主线程收到请求之后,通知<code>Http线程</code>发送请求,<code>Http线程</code>收到<code>主线程</code>通知之后就去请求数据,并通知<code>主线程</code>请求已经发送,<code>主进程</code>通知<code>浏览器事件线程</code>已经去请求数据,则<br>
<code>浏览器事件线程</code>,只需要等待结果,并不影响其他工作。</p>
<p><strong>setInterval&setTimeout</strong></p>
<p><code>setInterval</code>与<code>setTimeout</code>同属于异步方法,其异步是通过回调函数方式实现。其两者的区别则<code>setInterval</code>会连续调用回调函数,则<code>setTimeout</code>会延时调用回调函数只会执行一次。</p>
<pre><code>setInterval(() => {
alert(1)
},2000)
//每隔2s弹出一次1
setTimeout(() => {
alert(2)
},2000)
//进入页面后2s弹出2,则不会再次弹出
</code></pre>
<p><strong>requestAnimationFarme</strong></p>
<p><code>requestAnimationFrame</code>字面意思就是去请求动画帧,在没有<code>API</code>之前都是基于<code>setInterval</code>,与<code>setInterval</code>相比,<code>requestAnimationFrame</code>最大的优势是由系统来决定回调函数的执行时机。具体一点讲,如果屏幕刷新率是<code>60Hz</code>,那么回调函数就每<code>16.7ms</code>被执行一次,如果刷新率是<code>75Hz</code>,那么这个时间间隔就变成了<code>1000/75=13.3ms</code>,换句话说就是,<code>requestAnimationFrame</code>的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。</p>
<p>举个小例子:</p>
<pre><code>var progress = 0;
//回调函数
function render() {
progress += 1; //修改图像的位置
if (progress < 100) {
//在动画没有结束前,递归渲染
window.requestAnimationFrame(render);
}
}
//第一帧渲染
window.requestAnimationFrame(render);
</code></pre>
<p><strong>Object.observe - 观察者</strong></p>
<p><code>Object.observe</code>是一个提供数据监视的<code>API</code>,在<code>chrome</code>中已经可以使用。是<code>ECMAScript 7</code> 的一个提案规范,官方建议的是<code>谨慎使用</code>级别,但是个人认为这个<code>API</code>非常有用,例如可以对现在流行的<code>MVVM</code>框架作一些简化和优化。虽然标准还没定,但是标准往往是滞后于实现的,只要是有用的东西,肯定会有越来越多的人去使用,越来越多的引擎会支持,最终促使标准的生成。从<code>observe</code>字面意思就可以知道,这玩意儿就是用来做观察者模式之类。</p>
<pre><code class="language-javaScript">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)
}
</code></pre>
<p><strong>Promise</strong></p>
<p><code>Promise</code>是对异步编程的一种抽象。它是一个代理对象,代表一个必须进行异步处理的函数返回的值或抛出的异常。也就是说<code>Promise</code>对象代表了一个异步操作,可以将异步对象和回调函数脱离开来,通过<code>then</code>方法在这个异步操作上面绑定回调函数。</p>
<p>在Promise中最直观的例子就是<code>Promise.all</code>统一去请求,返回结果。</p>
<pre><code class="language-javaScript">var p1 = Promise.resolve(3);
var p2 = 42;
var p3 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, 'foo');
});
Promise.all().then(function(values) {
console.log(values);
});
// expected output: Array
</code></pre>
<p><strong>Generator&Async/Await</strong></p>
<p><code>ES6</code>的<code>Generator</code>却给异步操作又提供了新的思路,马上就有人给出了如何用<code>Generator</code>来更加优雅的处理异步操作。<code>Generator</code>函数是协程在<code>ES6</code>的实现,最大特点就是可以交出函数的执行权(即暂停执行)。整个<code>Generator</code>函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。<code>Generator</code>函数的执行方法如下。</p>
<pre><code class="language-javaScript">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}
</code></pre>
<p>粗略实现<code>Generator</code></p>
<pre><code class="language-javaScript">function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array, 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 }
</code></pre>
<p><code>Async/Await</code>与<code>Generator</code>类似,<code>Async/await</code>是<code>Javascript</code>编写异步程序的新方法。以往的异步方法无外乎回调函数和<code>Promise</code>。但是<code>Async/await</code>建立于Promise之上,个人理解是使用了<code>Generator</code>函数做了语法糖。<code>async</code>函数就是隧道尽头的亮光,很多人认为它是异步操作的终极解决方案。</p>
<pre><code class="language-javaScript">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();
}
dome().then((res) => {
console.log(res);
});
</code></pre>
<p><strong>Node.js异步I/O</strong></p>
<p>当我们发起<code>IO</code>请求时,调用的是各个不同平台的操作系统内部实现的线程池内的线程。这里的<code>IO</code>请求可不仅仅是读写磁盘文件,在<code>*nix</code>中,将计算机抽象了一层,磁盘文件、硬件、套接字等几乎所有计算机资源都被抽象为文件,常说的<code>IO</code>请求就是抽象后的文件。完成<code>Node</code>整个异步<code>IO</code>环节的有事件循环、观察者、请求对象。</p>
<p><strong>事件循环机制</strong></p>
<p>单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。于是就有一个概念,任务队列。如果排队是因为计算量大,<code>CPU</code>忙不过来,倒也算了,但是很多时候<code>CPU</code>是闲着的,因为<code>IO</code>设备(输入输出设备)很慢(比如<code>Ajax</code>操作从网络读取数据),不得不等着结果出来,再往下执行。</p>
<p>事件循环是<code>Node</code>的自身执行模型,正是事件循环使得回调函数得以在<code>Node</code>中大量的使用。在进程启动时<code>Node</code>会创建一个<code>while(true)</code>死循环,这个和<code>Netty</code>也是一样的,每次执行循环体,都会完成一次<code>Tick</code>。每个<code>Tick</code>的过程就是查看是否有事件等待被处理。如果有,就取出事件及相关的回调函数,并执行关联的回调函数。如果不再有事件处理就退出进程。</p>
<img src="https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=3205663441,2452156941&fm=26&gp=0.jpg">
<p>线程只会做一件事情,就是从事件队列里面取事件、执行事件,再取事件、再事件。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。</p>
<pre><code class="language-javaScript">while(true) {
var message = queue.get();
execute(message);
}
</code></pre>
<p>我们可以把整个事件循环想象成一个事件队列,在进入事件队列时开始对事件进行弹出操作,直至事件为<code>0</code>为止。</p>
<p><strong>process.nextTick</strong></p>
<p><code>process.nextTick()</code>方法可以在当前"执行栈"的尾部-->下一次<code>Event Loop</code>(主线程读取"任务队列")之前-->触发<code>process</code>指定的回调函数。也就是说,它指定的任务总是发生在所有异步任务之前,当前主线程的末尾。(<code>nextTick</code>虽然也会异步执行,但是不会给其他<code>io</code>事件执行的任何机会);</p>
<pre><code class="language-javaScript">process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){console.log(2);});
});
setTimeout(function C() {
console.log(3');
}, 0);
// 1
// 2
// 3
</code></pre>
<blockquote>
<p>异步过程的构成要素</p>
</blockquote>
<p>异步函数实际上很快就调用完成了,但是后面还有工作线程执行异步任务,通知主线程,主线程调用回调函数等很多步骤。我们把整个过程叫做异步过程,异步函数的调用在整个异步过程中只是一小部分。</p>
<p>一个异步过程的整个过程:主线程发一起一个异步请求,相应的工作线程接收请求并告知主线程已收到通知(异步函数返回);主线程可以继续执行后面的代码,同时工作线程执行异步任务;工作线程完成工作后,通知主线程;主线程收到通知后,执行一定的动作(调用回调函数)。</p>
<p>它可以叫做异步过程的发起函数,或者叫做异步任务注册函数。<code>args</code>是这个函数需要的参数,<code>callbackFn</code>(回调函数)也是这个函数的参数,但是它比较特殊所以单独列出来。所以,从主线程的角度看,一个异步过程包括下面两个要素:</p>
<ol>
<li>发起函数;</li>
<li>回调函数callbackFn</li>
</ol>
<p>它们都是主线程上调用的,其中注册函数用来发起异步过程,回调函数用来处理结果。</p>
<p>举个具体的栗子:</p>
<pre><code class="language-javaScript">setTimeout(function,1000);
</code></pre>
<p>其中<code>setTimeout</code>就是异步过程的发起函数,<code>function</code>是回调函数。</p>
<p>注:前面说得形式<code>A(args...,callbackFn)</code>只是一种抽象的表示,并不代表回调函数一定要作为发起函数的参数,例如:</p>
<pre><code class="language-javaScript">var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx;
xhr.open('GET', url);
xhr.send();
</code></pre>
<blockquote>
<p>总结</p>
</blockquote>
<p><code>JavaScript</code>的异步编程模式不仅是一种趋势,而且是一种必要,因此作为<code>HTML5</code>开发者是非常有必要掌握的。采用第三方的异步编程库和异步同步化的方法,会让代码结构相对简洁,便于维护,推荐开发人员掌握一二,提高团队开发效率。</p><br><br>
来源:https://www.cnblogs.com/aaron---blog/p/10903118.html
頁:
[1]