JavaScript中的同步与异步

5,739 阅读13分钟

前言

平日的编码中,你能列出你常用的异步编码?怎么理解同步与异步?

如果仅仅停留在文字上的理解,个人觉得有口无心,每当屡屡面试时,这都是一个常问的话题,牵扯到的是事件的执行顺序,任务队列,在js当中对于异步处理任务,是一个非常重要知识.

如何看待同步?

由于js是单线程的,换句话说,就是,在同一段时间内,只能处理一个任务,干一件事情,然后再去处理下一个任务,浏览器解析网页中的js代码,是逐行进行读取,从上至下执行的

实例场景:打电话就是一个同步的例子,必须等待打完了一个,然后再接着打下一个的

在如何看待同步之前,有必要了解下计算机中两个专业术语概念,就是进程和线程

进程: 它是系统进行资源分配和调度的一个独立单位,具有一定独立功能的程序关于某个数据集合上的一次运行活动,可以粗俗的理解为主(大)任务

线程: 安排CPU执行的最小单位,可以理解为子任务

关系: 线程可以视作为进程的子集,一个进程可以有多个线程并发执行

区别:进程和线程的主要差别在于,它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。

线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。

但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程

在后文中会用具体的代码,来认识同步的

为什么js是单线程?

JavaScript之所以设计为单线程,这与它的用途有关。它作为浏览器脚本语言,主要用途是负责与页面的交互,以及操作DOM(添加,删除等),它只能是单线程的,否则它就会带来很复杂的同步问题。

比如,你在网页上有若干个操作,也就是在主线程中有多个任务,一个线程任务是在某个DOM节点上添加内容,另一个线程任务是删除这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程的,这已经是这门语言的核心特征,将来也不会改变

而单线程,是指在JS引擎中负责解释和执行JavaScript代码的线程只有一个,但浏览器是多线程的,而js是单线程的,两者并不矛盾,浏览器只是js宿主的运行环境

怎么理解异步?

浏览器是多线程的,但解析我们的js代码,却是单线程的,但有些任务是需要消耗时间的(比如:上传,读取文件,下载等),如果按照普通的同步方式,就会阻塞我们的代码,主线程的任务没有做完,那么下面的任务将不会执行

实例场景:给女票打电话,必须等待到对方接听,有反应后,才能继续后面的热恋,你得一直等待,干不了别的事情,在那苦等的耗着

但发短信,微信就是一个异步的例子,也许对方正忙,没有及时回复,你不必等待对方及时回应,你仍可以继续干其他的事情。等到对方看见了,便会回应你.

单线程中有一些任务需要耗费一些时间,让用户去等待确认,把一些耗时的事情任务通过新开的线程方式来实现,浏览器会针对对于那些耗时间的任务,会开一些新的进程单独去处理

主线程继续往下走,那么这个时候,它既不影响后续代码的执行,同时还能通过另外的线程去做事,然后等待另外的线程做完事之后

比如说:通过回调,事件的方式去通知我们的主线程,然后把Ajax等异步处理要做的事情,在推到主线程当中进行执行

有哪些东西是需要重新开线程的?既然js是单线程的,那么他是如何是实现异步操作的?我们把这些任务称为:异步任务 同一段时间内可以做多个任务,例如

setTimeout
setInterval
ajax
...

监听DOM,修改页面的操作,渲染我们的样式,都是需要浏览器去处理的

这样的话,所谓的异步请求就很好理解了

指web服务器对请求作出响应时不要求你等待,这说明,浏览器解析js代码,当遇到异步任务时,不会僵持在那里不动,它会继续做主线程的任务,并会在服务器处理完请求时通知你.

那么在具体的代码中,是怎么体现的? 这里以Ajax为例: 我们先看写一段简单的后端代码

/**
 * 
 * @authors 川川 (itclancode@163.com)
 * @ID suibichuanji
 * @weChatNum 微信公众号:itclancoder
 * @version $Id$
 */
var http = require('http') // 使用http对象来引用http模块
var url = require('url');
var jsonData = { "name": "川川", "age": 20, "job": "weber" };
var app = http.createServer(function(req, res) { // 使用http模块的createServer方法来创建用于接收HTTP客户端请求并返回的响应的HTTP服务器应用程序,在createServer方法中定义了当服务器接收到客户端请求时所执行的回调函数,在该回调函数中指定当服务器接收到客户端请求时所要执行的处理,第一个参数req代表的是客户端请求对象,第二个参数代表服务器端所做出的响应对象
    res.writeHead(200, {
        'Content-Type': 'application/json;charset=utf-8',
        'Access-Control-Allow-Credentials': true,
        'Access-Control-Allow-Origin': '*' //可以是*,也可以是跨域的地址
    })



    // url.parse 方法来解析 URL 中的参数
    var pathname = url.parse(req.url, true).pathname;
    if (pathname == '/index') {
        setTimeout(function() {
            res.end(JSON.stringify(jsonData)); // 通过响应对象res的end方法输出一json对象,并结束响应流
        }, 3000)
    }

})

app.listen(8083, "127.0.0.1"); // createServer方法将返回被创建的HTTP服务器对象,我们使用该对象的listen方法指定服务器使用端口及服务器绑定的地止,并对该端口进行监听

console.log('server running at http:127.0.0.1:8083/');

将这段代码命名为server.js,然后在当前目录下执行node server.js,就会启动后端的服务 在浏览器端地止栏:输入http://127.0.0.1:8083/index

那么在浏览器前端: 如果想要把这个数据添加到浏览器前端页面上,那该怎么操作? 如下代码所示:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>01异步与同步</title>
<meta name="description" content="">
<meta name="keywords" content="">
<link href="" rel="stylesheet">
<style>
    *{
        padding: 0;
        margin: 0;
    }

    #box{
        width: 100px;
        height: 100px;
        background: red;
    }
</style>
</head>
<body>
        <button id="btn">按钮</button>
        <div id="list-wrap"></div>
        <div id="box"></div>

        <script>
            var oBtn = document.querySelector('#btn');
            var oBox = document.querySelector('#box');
            var oListWrap = document.querySelector("#list-wrap");
 
            var ul = document.createElement('ul');
            oListWrap.appendChild(ul);
            var str = "";
            oBtn.onclick = function(){
                console.log("任务2");
                var xhr  = new XMLHttpRequest();
                xhr.onload = function(){
                   console.log(xhr.readyState);
                   if(this.readyState== 4 ){
                      if(this.status == 200){
                        var data = JSON.parse(this.responseText);
                      }
                        
                   }
                   console.log("任务3");
                   console.log(data);
                   var listAttrs = Object.keys(data).map(function(item){
                      return data[item]
                   });
                   var attrs = Object.keys(data).map(function(item){
                      return item;
                   })
                   console.log(listAttrs);
                   for(let i = 0;i<attrs.length;i++){
                     
                     str += "<li>"+attrs[i]+":"+listAttrs[i]+"</li>";
                     

                     ul.innerHTML = str;
                   }
                   
                }
               
               xhr.open('get', 'http:127.0.0.1:8083/index',true);
                
                console.log("任务4");
                // true表示异步,ajax的事情还没有处理完成的时候,我们点击div,可以立马变色,ajax的事情并不影响当前页面中其他效果,开启了一个新的线程去完成ajax的事情,并不影响主线程,其他页面在主线程当中的其他任务的
                // false同步,当前线程直接处理
                xhr.send();
            }
            // 点击操作
            oBox.onclick =  function(){
                this.style.background = "green";
            }

            console.log("任务1");
</script>
</body>
</html>

上面代码的主要功能是:点击按钮,加载后端数据,将数据添加到前端页面中

如果把xhr.open()的第三个参数设置为false,则是同步的,当你点击按钮后,你点击下面的方块框,点击事件它是不会执行的,必须得等到上面的事情(加载数据)做完了,在次点击时,它才会生效

在使用Ajax的时候,应该推荐使用异步的方式,而不应该是同步的,不然的话,它就会阻塞我们后续的代码执行

如果你把xhr.open()的第三个参数设置为false,那么当你点击按钮后,在点击红色的box,它是不会起作用的,只有等待响应的结果执行完后,点击红色的box,才会生效执行

JS为什么需要异步?

JS是单线程的,那肯定只能同步(排队)顺序执行代码,是没有疑问的,写同步代码的好处就是好理解,坏处就是容易阻塞,只能等待上一次任务做完了,在接着做下一个任务.

而写异步代码的好处,就是实现让程序可控,想让它按照我们的想要的结果进行输出,坏处显然就是不好理解,射出去的弓箭,又要绕回来. 如果JS中不存在异步,只能自上而下执行,万一上一行解析代码的时间很长,那么下面的代码就会被阻塞。对于用户而言,阻塞就意味着"卡死",这样就导致了很差的用户体验

想想在一个聊天室里,你发一条信息,必须要等待对方回应后,才能在发一条信息,这显然会令人奔溃的

那js单线程又是如何实现异步的呢

是通过事件循环(event loop)实现异步的,这个词在很多前端技术书籍上都提到过,但是每次看完,总是不理解,知道有那么一回事,但就是解释不清楚

下面这个经典的问题:猜猜它的输出结果

console.log('1')
setTimeout(function(){
 console.log('2')
},0)
console.log('3')

想必大家闭着眼都能答上来,输入的顺序是1,3,2,但是解释一下为什么,却总是道不明白.

setTimeout里的匿名函数并没有立即执行,而是延迟了一段时间,等满足一定条件后,才去执行的,匿名函数没有立即被调用栈执行,而是添加一个队列中,专业点称为任务队列,类似这样的代码,我们叫异步代码。

首先我们知道了JS里的一种任务分类方式,就是将任务分为: 同步任务和异步任务

虽然JS是单线程的,但是浏览器的内核却是多线程的,在浏览器的内核中不同的异步操作由不同的浏览器内核模块调度执行,异步任务操作会将相关回调添加到任务队列中。

而不同的异步操作添加到任务队列的时机也不同,比如onclick, setTimeout, ajax 处理的方式都不同,这些异步操作是由浏览器内核来执行的,浏览器内核上包含3种 webAPI,分别是 DOM Binding(DOM绑定)、network(网络请求)、timer(定时器)模块。

按照这种分类方式:JS的执行机制是

  • 首先判断js代码是同步还是异步,不停的检查调用栈中是否有任务需要执行,如果没有,就检查任务队列,从中弹出一个任务,放入栈中,如此往复循环,要是同步就进入主进程,异步就进入事件表
  • 异步任务在事件表中注册函数,当满足触发条件后,被推入事件队列
  • 同步任务进入主线程后一直执行,直到主线程空闲时,才会去事件队列中查看是否有可执行的异步任务,如果有就推入主进程中

以上三步循环执行,这就是事件循环(event loop),它是连接任务队列和控制调用栈的 小结:

同步任务可以保证顺序一致,代码可读性好,相对容易理解,但是容易导致阻塞;异步任务可以解决阻塞问题,但是会改变任务的顺序性,根据不同的需要去写你的代码

显然异步代码是我们常用的一种方式,也是比较复杂的,而在js中处理异步,也就诞生出了很多的工具处理异步问题

例如:回调函数(异步执行或稍后执行的函数,也可以理解为将一个函数的参数作为另一个函数的名字,那么这个参数就叫做回调函数),使用Es6中的承诺(promise),Es7中的async await

为了更好的理解回调函数,下面写了几行代码,命名为callback.js,读取number.txt文件,在number.txt中写了1234,然后执行node callback.js

var fs = require('fs');
var myNumber = undefined;
function addOne(callback){
   fs.readFile('number.txt', function doneReading(err, fileContents){
       myNumber = parseInt(fileContents);
       myNumber++;
       callback();
  })
}
function logMyNumber(){
   console.log(myNumber);
}
addOne(logMyNumber)

上面的logMyNumber函数作为addOne函数的实参传入进去,而在addOne函数声明处,用callback参数变量进行接收,并在addOne函数内进行调用执行(callback()),类似这种将一个函数作为参数传递被另一个函数调用执行的,这样的函数就称为回调函数

结语

整篇文章主要了解js中的同步与异步问题,js是一门单线程的语言,浏览器解析js代码是同步顺序执行的,但是浏览器本身是多线程的,js实现异步是通过事件循环来实现的

定时器setTimeout,setInterval本质上是浏览器提供API,它是异步执行的.也就是说,异步函数代码它不会立即执行调用

一旦遇到异步的任务,会将它安排到一个任务队列中挂起状态,浏览器重新开一个新的线程单独处理它,它并不会阻塞主线程的代码,当主线程任务处理完了,有空闲时,此时,等待执行异步任务队列中的事情

异步处理在js中是一个非常重要的问题,往往牵扯到什么宏任务,微任务,很多时候,这些抽象的概念,面试的时候,是虐人的

实际开发中,很多时候,更多是停留在,知道就是这么用的,但是却道不清楚背后的原理,或者这就是与大神的差距吧...

在遇到复杂的业务逻辑时,处理异步任务肯定是绕不过的,所以还是有必要去了解浏览器解析代码的流程,执行顺序的.

更多内容,您可关注微信itclanCoder公众号,一个只传递和分享给你带来启发智慧有用的号