JavaScript中的同步与异步

前言

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

若是仅仅停留在文字上的理解,我的以为有口无意,每当屡屡面试时,这都是一个常问的话题,牵扯到的是事件的执行顺序,任务队列,在js当中对于异步处理任务,是一个很是重要知识.前端

如何看待同步?

因为js是单线程的,换句话说,就是,在同一段时间内,只能处理一个任务,干一件事情,而后再去处理下一个任务,浏览器解析网页中的js代码,是逐行进行读取,从上至下执行的 实例场景:打电话就是一个同步的例子,必须等待打完了一个,而后再接着打下一个的node

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

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

**线程:**安排CPU执行的最小单位,能够理解为子任务ajax

**关系:**线程能够视做为进程的子集,一个进程能够有多个线程并发执行chrome

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

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

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

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

为何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中是一个很是重要的问题,每每牵扯到什么宏任务,微任务,不少时候,这些抽象的概念,面试的时候,是虐人的

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

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

相关文章
相关标签/搜索