彻底理清JavaScript的单线程,异步,Event Loop,Promise的关系

JavaScript的三座大山:单线程与异步,原型与原型链(继承),作用域和闭包。
接下来就其中的单线程与异步,和延扩涉及的事件轮询,Promise写下我个人的理解,算是做下总结,顺便给对这几个概念的关系有些模糊的朋友提供一些思路。
若有错误的,敬请指正。

JS的单线程

其他面向对象语言JAVA,C++,都是多线程的,即一个进程中可以并发多个线程,而每条线程并行执行不同的任务,也就是说在同一时刻可以同时进行多个任务
而单线程:没有多个线程可供主程序来调用,简单来说,就是同一时刻只能做一件事情
JS正是单线程的语言

console.log("abc")
alert("小鱼你好")
console.log("e")

运行结果只打印出了“abc”,对话框点确定之前,“e”是不会被打印的,说明JS是顺序执行的,并且前面的代码执行完才能继续执行后面的代码,没有其它的多余线程可以在执行第二行代码的时候同时执行第3行

为什么JS是单线程的

JS语言的应用场景注定了它只能是单线程的语言,可以说,单线程是JavaScript的本质
原因就在于:

避免dom渲染冲突

JavaScript是一种属于网络的脚本语言,已经被广泛用于Web应用开发,最早就是在HTML网页上使用,进行网页的交互功能

我们都知道浏览器利用HTML文件内容可以渲染dom结构,JS也可以操作dom结构,则两者都可以对dom结构产生影响。
所以为了避免出现二者都对同一dom同时操作而造成渲染冲突,需要

  1. JS代码执行的时候,浏览器的渲染会暂停
  2. 两端JS不能同时执行(否则有多个源头修改dom,会产生冲突)

所以:
JS本身必须是单线程的,且必须和浏览器渲染共用一个线程

JS的异步:单线程的解决方案

为什么要使用异步

上面说了由于JavaScript用于为网页添加各式各样的动态功能,能够操作dom,为了避免dom渲染冲突,所以JavaScript必须是单线程的。
但是单线程又会带来一系列的问题,比如卡顿,即前面的代码没有执行完,后面的代码又只能一直等待
比如定时器任务setTimeout/serInteral

var i,sum = 0;
for(i=0;i<1000000000;i++){
    sum+=i
}
//循环执行期间,JS执行和dom渲染暂时卡顿
console.log("abc") 
//由于前面的代码没有执行完,后面的代码也只能是一直等待着,即没有“abc”被打印出

为了解决单线程带来的问题,有了异步这个解决方案
在可能需要等待的情况下,为了不让这些等待的过程像alert程序一样阻塞程序,这时候就需要异步了,即:
所有“等待的情况”都需要异步
故需要“等待”的情况也是通常前端使用的异步的场景:

  1. 定时任务: setTimeoutsetInvervalprocess.nextTicksetImmediate(node特有的定时器)
  2. 网络请求:ajax请求,动态加载
  3. 事件绑定(比如点击事件)
// 有定时器任务
console.log("aaa")
setTimeout(function(){   //反正1s后才执行,先不管它,先让其他的代码执行
    console.log("bbb") 
},1000)
console.log("ccc")
console.log("ddd")

运行结果:


在这里插入图片描述
console.log("aaa")
$.ajax({        
    url:'xxxxxx',
    success:function(result){  //ajax加载完才执行
        console.log(result)    //先不管它,先让其他JS代码执行
    }
})
console.log("ccc")
console.log("ddd")

运行结果是aaa,ccc,ddd,之后才是ajax请求返回的内容

异步的实现机制---Event Loop事件轮询

JavaScript是怎么实现可以不依照代码顺序执行,实现部分代码(也就是异步任务)的异步执行的呢?
就是通过eventLoop即事件轮询的机制。
换句话说,event loop是JavaScript实现异步的具体方案
event loop 机制的核心:

  1. 代码分为同步代码异步代码(异步任务会有对应的回调函数)
  2. 同步代码放在主执行栈里,直接执行
  3. 异步函数先放在异步任务队列里,暂时先不执行
  4. 待同步函数执行完毕,轮询执行异步队列里的函数(执行的就是相关异步任务对应的回调函数)

第3点 将异步任务放入任务队列时分为三种情况:

1.若异步任务没有延时,则直接将其放入异步队列

2.若有延时,则等延时时间到了才会放入异步队列

3.若有ajax请求,则等ajax加载完成才放入异步队列

第4点 “轮询”过程理解:
JS搜索引擎会轮询监听异步队列里的函数,主要主线程里空了(即主执行栈里的同步代码都执行完了),就会去读取任务队列里的事件,调到主线程里执行,这个过程是循环重复

// 代码演示
$.ajax({
   url:'xxxxx',
   success:function(result) {
        console.log('a')
   }
})
setTimeout(function () {
     console.log('b')
},100)
setTimeout(function () {
    console.log('c')
)
console.log('d')

分析:


在这里插入图片描述

运行结果是dcba或dcab
(若该ajax加载完成时间小于100ms,则ajax的回调函数的执行先于延时100ms的定时器的回调函数,则'a'会先于'b'打印)

微任务和宏任务

异步任务又分为“微队列”和“宏队列”里的任务,微队列里的都执行完才会去执行宏队列里的异步任务
微队列:

  • process.nextTick(Node独有)
  • Promise的then方法
  • Object.observe
  • MutationObserver

宏队列:

  • setTimeout
  • setInterval
  • setImmediate(Node独有)
  • requestAnimationFarme(浏览器独有)
  • I/O
  • UI rendering(浏览器独有)

常用的异步任务和执行顺序整理在下图(图略丑...)

在这里插入图片描述

其中要特别注意的是promise对象一旦建立就执行,只不过promise对象的then方法是异步的

//直接简单粗暴的用下面简单代码演示
console.log('a'))
setImmediate(() => console.log('b'));
new Promise((resolve, reject) => {console.log('c');resolve()}).then(() => 
Promise.resolve().then(() => console.log('d'));
--------------------- 
// 结果: a c d b 

JS的promise:异步的解决方案

由于JS单线程的本质,需要通过异步来解决单线程带来的问题。
而异步也会带来问题:
1.代码没按照书写形式执行,导致可读性变差

  1. callback(回调函数)中不容易模块化

回调嵌套是解决异步最直接的方法,即将后一个的操作放在前一个操作的异步回调里,但回调的多层嵌套会导致使代码很冗杂,而且导致检查代码会费劲。
Promise可以用来优雅避免callback hell问题

promise的基本语法

通过new Promise()生成一个promise实例(promise对象),传入一个函数,函数有两个参数,
第一个为resolve,第二个参数为reject,这两个参数都是函数形式,分别是成功和失败时执行的函数
promise对象可调用then方法,then方法可传入两个参数,
第一个参数:成功时的回调,第二个参数:失败时的回调

注意:
当then()没有return时则默认返回的仍是调该then方法的promise对象
当then()里有return则返回的是指定的promise对象

  function loadImg(src) {
        return new Promise(function (resolve, reject) {
            var img = document.createElement('img')
            img.onload = function () {
                resolve(img)
            }
            img.onerror = function () {
                reject()

            }
            img.src = src
        })
    }
    var src = "xxxxx"
    var result = loadImg(src)  //result是调用loadImg函数后返回的promise对象
    result.then(function (img) {  //promise调用then方法,传入两个参数
        console.log(img.width)
    },function () {
        console.log('failed')
    }).then(function (img) {  //可多次调用then方法
       console.log(img.height)
    })

promise捕获异常

这里顺便也附上如何用promise捕获异常
想要捕获异常时:

1. then方法只传入一个参数:成功时的回调函数(不再传入失败时的回调)
2. 最后统一用catch方法捕获异常:catch方法传入一个函数,函数的参数就是想要捕获的产生异常的对象

 var src='xxxxx'
    var result=loadImg(src)
    result.then(function (img) {
        console.log(1,img.width) 
    }).then(function (img) {
        console.log(2.img.height)
    }).catch(function (ex) { //catch传入的函数的参数就是产生异常的那个对象
        // 统一捕获异常
        console.log(ex)
    })

总结

  • 为了避免dom渲染冲突,要求JavaScript的本质就是单线程

  • 为了解决单线程带来的可能造成卡顿和等待的问题,需要JavaScript的异步

  • 为了实现JavaScript的异步,利用的是Event Loop 事件轮询的机制

  • 为了解决异步里回调函数嵌套带来的问题,利用Promise 优雅避免callback hell问题

你可能感兴趣的:(彻底理清JavaScript的单线程,异步,Event Loop,Promise的关系)