单线程模型
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函数。
由于异步任务不能直接拿到结果,所以我们会传一个回调函数给异步任务,等到异步任务完成时,调用这个回调函数,这时候会将结果作为参数来做一些处理。