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同时操作而造成渲染冲突,需要
- JS代码执行的时候,浏览器的渲染会暂停
- 两端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程序一样阻塞程序,这时候就需要异步了,即:
所有“等待的情况”都需要异步
故需要“等待”的情况也是通常前端使用的异步的场景:
- 定时任务:
setTimeout
,setInverval
和process.nextTick
,setImmediate
(node特有的定时器)- 网络请求:ajax请求,动态加载
- 事件绑定(比如点击事件)
// 有定时器任务
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 机制的核心:
- 代码分为同步代码和异步代码(异步任务会有对应的回调函数)
- 同步代码放在主执行栈里,直接执行
- 异步函数先放在异步任务队列里,暂时先不执行
- 待同步函数执行完毕,轮询执行异步队列里的函数(执行的就是相关异步任务对应的回调函数)
第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.代码没按照书写形式执行,导致可读性变差
- 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问题