JS异步

290 阅读6分钟

单线程模型

JS的单线程模型指的是,JS只在一个线程上运行。但是要注意,这并不能代表JS只有单个线程,实际上JS拥有多个线程,但是为什么要在单线程上运行,这涉及到历史问题。

由于JS被设计之初不想让浏览器变得太复杂,由于多线程之间会共享资源,且互相影响运行结果,对于web页面来说,这就有点复杂了。我们假设某个线程插入一个dom节点,而另外的线程同时又删除这个dom节点,那么浏览器到底是以谁为准?于是为了避免脚本语言变得太复杂,JS就被设计成单线程。

这样的设计有好处也有坏处:好处就是执行起来逻辑清楚,执行环境单纯。坏处就是当一个任务执行时间较长时,后面的任务会有等待期,这拖延了整个程序的运行。

很多情况下,JS代码不会大批量加载cpu资源,反而是类似于AJAX之类的请求服务器资源的任务,这类任务往往由于处于等待返回结果的状态,如果长时间没有返回,那么整个程序都会处于停滞阶段。

这时候,JS的设计者发现,可以先挂起其中一些需要等待的任务,执行其他操作,直到等待的任务有返回结果了,再回头执行就可以了。这种机制,就是JS的事件循环机制(Event loop)。

于是,JS变成同步和异步。主线程上执行同步任务,异步任务在其他子线程上配合,但由于子线程不能操作DOM且完全受到主线程控制,本质上JS还属于单线程。

同步任务&&异步任务

同步任务就是没有被JS引擎挂起,在主线程上按照顺序排队执行的任务。

异步任务就是被JS引擎挂起,放入任务队列中的任务,只有等JS引擎觉得可以了,就会回头执行异步任务。 举个AJAX的例子:

 	 var req = new XMLHttpRequest();
    req.open('GET', url);    
    req.onload = function (){};    
    req.onerror = function (){};    
    req.send();
    console.log(1)

上面代码中,req.send会往服务器发送一个请求,这个请求完整的结果是服务器接收到才算完成,此时是异步任务,所以这个函数被调用后,会直接执行后面的代码,然后等到Ajax返回结果会回头调用函数。如果此时是同步任务,(可以在open函数上设置成同步),那么会等Ajax结果返回后才会执行后面的代码。

我们可以这样理解:同步就是直接可以获取结果,异步不能直接获取结果。

任务队列

任务队列就是异步任务的容器,里面存放着需要执行的异步任务。实际上会根据异步任务的类型有多个队列。我们假设只有一个。

JS执行顺序是这样的,首先会执行所有主线程上的同步任务,当执行同步任务后,当满足一定条件,JS会从任务队列中拿出异步任务,继续放到主线程上运行。这时这个异步任务就变成同步任务了,等到执行完,下一个异步任务又会放到主线程上,直到任务队列上的异步任务全部清空,程序执行完毕。

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

回调函数

异步操作通常使用回调函数,一旦任务进入主线程,那就执行这个回调函数。

    function f1(){}
    function f2(){}
    f1()
    f2()

上面的代码如果是同步的情况下,必须要f1执行完后才会执行f2。那么,能不能先执行f2,再执行f1呢?

答案是可以用回调函数,我们把代码修改成以下内容

    function f1(){}
    function f2(callback){
        callback()
    }
    f2(f1)

上面的代码就是一个回调函数,代码先执行f2,再执行f1,f1就是一个回调函数。但是上面代码实际上是同步回调而不是异步回调。

判断异步任务

这里举例几种常见的异步任务,如果回调函数处于下列三样东西的内部,那么就是异步任务。

1、AJAX(AJAX可以设置同步,但是千万不要设置,除非你的项目经理故意要这么做申请项目经费😂😂)

2、setTimeout

3、addEventListener

AJAX举例

来看一段代码

    const request = new XMLHttpRequest() //  readystate=0
    request.open('get', '5.json', false) //这里的false把AJAX设置成同步  readystate=1
    let n = 1
    request.onreadystatechange = () => {
        console.log("state:" + request.readyState)
        n += 1
        if (request.readyState === 4 & (request.status >= 200 && request.status < 300)) {
            console.log('最终的n为:'+n);
        }
    }
    request.send()  // readystate=2
    console.log('我是后面的代码');
    console.log(`n:` + n);

上面的代码把AJAX设置成同步,这时当执行request.send()时,会向服务器发送请求,下面的两行console代码将不会执行,onreadyStatechange这个事件是监听readyState时调用的,当send发送完毕后request实例对象的readyState是2,当服务器响应并下载部分内容时,readyState是3,下载完毕时是4。由于上面设置了同步任务代码,request会一直处于等待状态,直到readyState为4时,先执行onreadystatechange监听下的回调函数,最后执行最下面的两行console代码,所以最终结果为:

	state:4
    最终的n为:2
	我是后面的代码
	n:2

但是如果设置request.open('get', '5.json')时,默认AJAX为异步任务,那么当request.send()发送请求后,后续的任务会被JS引擎挂起到任务队列中,优先执行完剩下的两行console代码。当执行完后onreadystatechange监听到request.state的值为2,代表send发送完毕了,那么就先执行onreadystatechange后面的回调函数,然后每次state发生变化,都会进入一次回调函数,所以n就会每进入一次都自增1,所以最终会出现这样的结果

    我是后面的代码
    n:1
    state:2
    state:3
    state:4
    最终的n为:4

做个异步下callback的总结

异步下经常用到回调,但是异步不一定非要回调,也可以轮询。而回调也不一定出现在异步中,同步也可以执行回调函数,例如数组的forEach就是同步回调函数

回调函数就是你写出来让别人来调用的,把回调函数作为参数传递给别人让别人来调用,例如:fn2(fn),让fn2来调用fn数,

再例如addEventListener('click',callback),执行点击操作告诉事件监听函数,然后由浏览器来执行callback。

再例如setTimeout(callback,1000),告诉浏览器1秒钟后执行callback函数。

由于异步任务不能直接拿到结果,所以我们会传一个回调函数给异步任务,等到异步任务完成时,调用这个回调函数,这时候会将结果作为参数来做一些处理。