在学习前端的时候看过很多帖子讲js的运行机制、js事件循环。说实话一看就忘,所以想自己整理一遍,以加深自己的理解。自己写过的东西即使是重复的,但至少在这个过程我也在思考和总结,也避免面试的时候被问到的时候支支吾吾说不出来,或者模糊的似懂非懂。
首先深入理解js的异步机制,必须要先理解几个概念。
Q1:什么是进程和线程?
A1:这是操作系统的基本概念,忘记的回去复习操作系统。
Q2:什么是同步和异步?
A2:同步是调用一旦开始,调用者必须等到调用方法返回后,才能继续后续的行为。调用这会等待主动结果。异步是当一个异步调用发出后,这个调用就立刻返回,调用者不会立即得到结果。而是通过某些通知来通知调用者,通过回调函数来处理这个调用。打个比喻,我是个很喜欢早上起来很喜欢喝一杯温开水的人,第一天我很笨,我点了烧开水的按钮之后我就一直在旁边等它烧开,在这等待期间我什么事情都没做,只是在等待我的水烧开,这就是同步。第一天晚上我躺下去睡觉的时候觉得我早上真的是愚蠢至极,这样等待完全是在浪费时间而且只能做一件事情。于是第二天,我在等烧开水的时候我去刷牙去了,等到水烧开了我才回来倒水,这就是异步。看,第二天做的事情比第一天多而且时间上利用率更大。
Q3:js为什么是单线程?
A3:js是浏览器的脚本语言,主要用于与用户进行交互以及操作DOM,这就决定了它只能是单线程的。假设js它有两个线程,一个节点说在某在DOM节点上添加内容,另一个线程说在直接删除这个DOM节点,你说浏览器要听哪一个线程呢?在HTML5有一个Worker线程,这是为了提高计算能力允许js创建多个线程,但是子线程完全受主线程控制的,而且它是不能操作DOM的。所以本质上还是单线程的原理,没有改变本质。
Q4:浏览器是多进程的?
A4:浏览器是多进程的,每次在浏览器打开一个Tab页面,就会产生一个进程。日常生活中我们不建议打开多个页面不关闭,会造成电脑越来越卡,很消耗CPU,像我们这种贫民窟女大学生还是爱护电脑一点,毕竟买不起新的。
Q:什么是事件循环机制?
A:js引擎线程只会执行执行栈中的事件,执行栈中的代码执行完毕,就会读取事件队列中的事件并添加到执行栈中继续执行,这样Loop循环反复就是Event Loop。
先执行宏任务,然后执行微任务,这个是基础,任务可以有同步任务和异步任务,同步的进入主线程,异步的进入Event Table并注册函数,异步事件完成后,会将回调函数放入Event Queue中(宏任务和微任务是不同的Event Queue),同步任务执行完成后,会从Event Queue中读取事件放入主线程执行,回调函数中可能还会包含不同的任务,因此会循环执行上述操作。
执行 宏任务--微任务的Event Queue--宏任务的Event Queue
每一个宏任务都会从头执行到尾,不会执行其他。js引擎线程和GUI渲染线程是互斥关系。在一个宏任务完成之后在下一个宏任务完成之前,GUI渲染会对页面进行渲染。
宏任务--GUI渲染--宏任务
ES6引入promise,微任务可以理解成当前宏任务执行后立即执行的任务。
宏任务--微任务--GUI渲染--宏任务
setTimeout不能精准执行的问题
setTimeOut并不是直接的把回掉函数放进异步队列中去,而是在定时器的时间到了之后,把回掉函数放到执行异步队列中去。需要满足两个条件:主进程必须是空闲的状态,如果不空闲,时间到了也不会执行;回调函数必须等到插入异步队列前面的异步函数执行完毕才会执行。
(1)判断是否为同步,异步则进入异步进程,最终事件回调给事件触发线程的任务队列等待执行,同步继续执行
(2)执行栈为空,询问队列中是否有事件回调
(3)任务队列中有事件回调则把它加入执行栈末尾
(4)任务队列中没有事件回调则不停发起询问
完整执行顺序
同步
异步
在前面三点的知识点都能掌握的前提下,要理解这一道面试题并不是难事
async function async1() {
console.log('async1 start');
await async2();
console.log('asnyc1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeOut');
}, 0);
async1();
new Promise(function (reslove) {
console.log('promise1');
reslove();
}).then(function () {
console.log('promise2');
})
console.log('script end');
解析
事件执行顺序是宏任务—微任务。执行顺序是宏任务—微任务Event Queue—宏任务Event Queue。
任务有同步任务和异步任务。同步进入主线程,异步先在Event Table注册函数,等待异步事件完成后,将它的回调函数放到Event Queue。宏任务和微任务的Event Queue不一样。同步任务完成后,在主进程是空闲的状态下,从Event Queue中读取事件放入主线程。
宏任务是整体代码script/setTimeout/seterval,微任务是Promise/.then。执行顺序。
new Promise是同步任务,放入主线程。.then()是异步任务,等promise状态结束的时候,放入异步队列。async关键词函数返回一个promise对象。await关键字在async关键字的函数内部,在外部会报错。await等待右侧表达式完成,await让出线程,阻塞asnyc关键字的函数的内部代码,先去执行async关键字函数的外部代码。等外部的执行完之后才会执行内部的。
以下这个图是在看解析的时候,觉得写得很清晰的图(来源于网路,不是原创!)
① 先执行async 关键字函数的外部代码。前两个async1()/async2()是正常的函数声明,往下看执行console.log(‘script start’),输出script start;
②执行setTimeout,setTimeout是一个异步任务,放入宏任务的异步队列中。等时间到了,主线程空闲才会去调用。
③执行async1(),输出async1 start 继续向下执行。
④执行async2(),输出async2,并且返回一个promise对象,await让出线程,把返回的promise放入微任务异步队列,并且阻塞async内部代码的执行,即console.log(‘async1 end’),且async1()后面的代码要等待上面完成。
⑤执行new Promise,输出promise1,然后将resolve()放入微任务的异步队列。
⑥执行 console.log(‘script end’),输出script end。
⑦到这里同步代码全部执行完成,接着去异步任务队列获取任务,先微任务Event Queue再宏任务Event Queue
⑧执行resolve,这个resolve是async2返回的promise返回的,输出async1 end
⑨执行resolve,这个resolve是new Promise的,输出promise2
⑩微任务异步队列执行完,执行宏任务异步队列的setTimeout,输出setTimeOut
优点:解决了同步的缺点,只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行顺序
缺点:回调地狱、不能用try catch捕获错误,不能return
优点:解决回调地狱的问题
缺点:无法取消Promise,错误需要通过回调函数捕获
特点:可以控制函数的执行,可以配合 co 函数库使用
优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。
callback -> promise -> generator -> async + await
ajax(url, () => {
// 处理逻辑
})
如果多个请求存在依赖性,就会面临回调地狱,各个部分之间高度耦合,使程序结构混乱,难以追踪(多个函数嵌套,套娃)每个任务只能返回一个回调函数。不能通过try catch捕获错误,不能return。
ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})
优点:去耦合、利于实现模块化。可以绑定多个事件,每个事件可以有多个回调函数
缺点:整个程序变成事件驱动型,运行流程不清晰。
//当f1发生done事件,就执行f2
f1.on('done', f2);
//改写
function f1() {
setTimeout(function () {
// ...
f1.trigger('done');
}, 1000);
}
解析:setTimeout()第一个参数是一个函数,第二个参数是以毫秒为单位的时间间隔,setTimeout()是注册回调函数的函数,也可以表示在什么异步条件下调用回调函数,setTimeout()方法只会调用一次回调函数。
//给目标 DOM 绑定一个监听函数,用addEventListener
document.getElementById('#test').addEventListener('click', (e) => {
console.log('click')
}, false);
解析:通过id给 test 的一个元素绑定点击事件,任务的执行时机推迟到当点击这个动作。addEventListener`注册了回调函数,这个方法的第一个参数是一个字符串,指定要注册的事件类型,如果用户点击了指定的元素,浏览器就会调用回调函数,并给他传入一个对象,其中包含着事件的详细信息。异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生
网络请求是一种典型的异步操作
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功时
if (this.status === 200) {
handle(this.response);
} else {
console.error(this.statusText);
}
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
console.error(this.statusText);
};
// 发送 Http 请求
xhr.send(null);
nodejs服务端在js环境底层就是异步的,定义了很多回调和事件的API
//读取文件的API是异步的,读取文件内容之后调用一个回调函数
const fs = require('fs');
let options = {}
// 读取配置文件,调用回调函数
fs.readFile('config.json', 'utf8', (err, data) => {
if(err) {
throw err;
}else{
Object.assign(options, JSON.parse(data))
}
startProgram(options)
});
解析:fs.readFile()
方法以接收两个参数的回调作为最后一个参数。它会异步读取指定文件,如果读取成功就会将第二个参数传递给回调的第二个参数,如果发生错误,就会将错误传递给回调的第一个参数。
Promise是一个对象,表示异步操作的结果。Promise可以让多层嵌套回调以一种更线性的链式形式表达出来。
then
方法接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved
时调用,第二个回调函数是Promise对象的状态变为rejected
时调用。其中第二个参数可以省略。then
方法返回的是一个新的Promise实例。因此可以采用链式写法。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
Promise对象的catch方法指向reject
的回调函数。catch
方法还有一个作用,就是在执行resolve
回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch
方法中。
p.then((data) => {
console.log('resolved',data);
},(err) => {
console.log('rejected',err);
});
调用all
方法时的结果成功的时候是回调函数的参数是一个数组,这个数组按顺序保存着每一个promise对象resolve
执行时的值
let promise1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(1);
},2000)
});
let promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(2);
},1000)
});
let promise3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(3);
},3000)
});
Promise.all([promise1,promise2,promise3]).then(res=>{
console.log(res);
})
race
方法和all
一样,接受的参数是一个每项都是promise
的数组,但与all
不同的是,当最先执行完的事件执行完之后,就直接返回该promise
对象的值。当需要执行一个任务,超过多长时间就不做了,就可以用这个方法来解决。
let promise1 = new Promise((resolve,reject) => {
setTimeout(() => {
reject(1);
},2000)
});
let promise2 = new Promise((resolve,reject) => {
setTimeout(() => {
resolve(2);
},1000)
});
let promise3 = new Promise((resolve,reject) => {
setTimeout(() => {
resolve(3);
},3000)
});
Promise.race([promise1,promise2,promise3]).then(res => {
console.log(res);
},rej => {
console.log(rej)});
finally
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的
promise.then(result => {···})
.catch(error => {···})
.finally(() => {···});
链式操作:
new Promise(resolve => {
resolve(1);
})
.then(result => console.log(result)) //1
.then(result => {
console.log(result); //undefined
return 2;
})
.then(result => {
console.log(result); //2
throw new Error("err");
})
.then((result) =>{
console.log(result);
}, (err)=>{
console.log(err); //Error: err
return 3;
})
.then((result) => {
console.log(result); //3
})
Promise.prototype.catch,用于指定发生错误时的回调函数,返回一个新的promise对象。Promise对象后面要跟catch方法,这样可以处理Promise内部发生的错误。catch方法返回的还是一个Promise对象,因此后面还可以接着调用then方法。Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。
new Promise(() => {
throw new Error('err1');
})
.then(() => {console.log(1);})
.then(() => {console.log(2);})
.catch((err) => {
console.log(err); //Err: err1
throw new Error('err2');
})
.catch((err) => {console.log(err);})//Err: err2
异常处理还有一个好处就是很好解决地狱回调问题,但是无法取消promise,错误要通过回调函数捕获。
//地狱回调
ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})
//promise解决地狱回调
ajax(url)
.then(res => {
console.log(res)
return ajax(url1)
}).then(res => {
console.log(res)
return ajax(url2)
}).then(res => console.log(res))
Generator生成器函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,严格意义上说,Generator不是函数,是一个带星函数。
最大的特点就是可以控制函数的执行,通过yield关键字控制函数的暂时或next控制函数的执行
每次返回的是yield后的表达式结果,yield表达式本身没有返回值,或者说总是返回undefined
Generator函数可以理解为一个封装机,封装很多内部状态
function* test(x) {
let y = 2 * (yield(x + 1))
let z = yield(y / 3)
return (x + y + z)
}
let it = test(5)
console.log(it.next())
console.log(it.next(12))
console.log(it.next(13))
代码分析
①Generator不是普通的函数,甚至都不是函数,返回的是一个迭代器
②执行第7行next时,传入的参数停留到第2行yield,返回5+1 = 6
③执行第8行next时,传入的12参数会被当成上一个表达式(第2行)的返回值,如果不传入参数,返回的是undefined,此时第2行y=2*12=24,第3行 z = 24/3=8
④执行第9行next时,传入的13参数被当成上一个表达式(第3行)的返回值,如果不传入参数,返回的是undefined,此时第4行retrun x=5,z=13,y=24,return42
依赖紧密,代码就会冗长且不容易看通逻辑
//3个本地文件 1.txt 2.txt 3.txt
let fs = require('fs')
function read(file) {
return new Promise(function(resolve, reject) {
fs.readFile(file, 'utf8', function(err, data) {
if (err) reject(err)
resolve(data)
})
})
}
function* r() {
let r1 = yield read('./1.txt')
let r2 = yield read(r1)
let r3 = yield read(r2)
console.log(r1)
console.log(r2)
console.log(r3)
}
let it = r()
let { value, done } = it.next()
value.then(function(data) { // value是个promise
console.log(data) //data=>2.txt
let { value, done } = it.next(data)
value.then(function(data) {
console.log(data) //data=>3.txt
let { value, done } = it.next(data)
value.then(function(data) {
console.log(data) //data=>结束
})
})
})
解决方案:配合co库(一个nodejs和浏览器打造的基于生成器的流程控制工具)
function* r() {
let r1 = yield read('./1.txt')
let r2 = yield read(r1)
let r3 = yield read(r2)
console.log(r1)
console.log(r2)
console.log(r3)
}
let co = require('co')
co(r()).then(function(data) {
console.log(data)
})
// 2.txt=>3.txt=>结束=>undefined
//解决地狱回调
function *fetch() {
yield ajax(url, () => {})
yield ajax(url1, () => {})
yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
可以理解为Generator+co,可以完成这两个工作。是ES7新增了两个关键字: async和await。是 Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。
①基于Promise实现的,它不能用于普通的回调函数
②Promise一样,是非阻塞的
③异步代码看起来像同步代码
async是“异步”的简写,await则为等待。async 用来声明异步函数,这个关键字可以用在函数声明、函数表达式、箭头函数和方法上,异步主要是一些不会马上完成的任务,需要控制它的暂停和执行。await关键字就是暂停异步代码的执行,等待Promise解决。
await究竟在等待什么?await 是在等待一个 async 函数完成,一个表达式的值 Promise 对象或其它值,是一个返回值。
①等promise对象:await会阻塞后面的代码,等promise对象resolve,得到的值作为表达式的值
②等的不是promise对象:await等待什么就返回什么
let fs = require('fs')
function read(file) {
return new Promise(function(resolve, reject) {
fs.readFile(file, 'utf8', function(err, data) {
if (err) reject(err)
resolve(data)
})
})
}
function readAll() {
read1()
read2()//这个函数同步执行
}
async function read1() {
let r = await read('1.txt','utf8')
console.log(r)
}
async function read2() {
let r = await read('2.txt','utf8')
console.log(r)
}
readAll() // 2.txt 3.txt
//async 关键字声明一个异步函数
async function httpRequest() {
}
//加上await关键字 会转化为一个返回值或者抛出一个异常
async function httpRequest() {
let res1 = await httpPromise(url1)
console.log(res1)
}
async function testAsy(){
return 'hello world';
}
let result = testAsy();
console.log(result)
代码解析
async 函数返回的是 Promise 对象。如果异步函数使用return关键字返回了值(如果没有return则会返回undefined),这个值则会被 Promise.resolve()
包装成 Promise 对象。异步函数始终返回Promise对象。
function getSomething() {
return "something";
}
async function testAsync() {
return Promise.resolve("hello async");
}
async function test() {
const v1 = await getSomething();
const v2 = await testAsync();
console.log(v1, v2);
}
test();
const exe = (flag) => () => new Promise((resolve, reject) => {
console.log(flag);
setTimeout(() => {
flag ? resolve("yes") : reject("no");
}, 1000);
});
const run = async () => {
try {
await exe(false)();
await exe(true)();
} catch (e) {
console.log(e);
}
}
run();
代码解析
异步run()方法,await后面需要跟promise对象,因此通过额外的一个方法调用()把原来的exe方法内部的Thunk包装拆掉,即执行 exe(false)() 或 exe(true)() 返回的就是 Promise 对象。在 try 块之后,使用 catch 来捕捉。
①处理 then 的调用链,简介清晰写出代码,看起来像同步代码,同时优雅解决回调地狱问题
②内置执行器。Generator函数执行必须要执行器(co函数库因此而出现),async函数自带执行器。
③广泛的实用性。co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值。
④更好的语义。
①Promise的出现解决了传统callback函数导致的“地域回调”问题,形成回调链,在复杂的开发环境中语法会显得不美观。async /await代码看起来会简洁些,使得异步代码看起来像同步代码。
②async/await与Promise一样,是非阻塞的
③async/await是基于Promise实现的,理解为进阶的Promise,它不能用于普通的回调函数
执行的时间的区别。执行时间,defer会在文档解析完之后执行,并且多个defer会按照顺序执行,而async则是在js加载好之后就会执行,并且多个async,哪个加载好就执行哪个。
①在没有defer或者async的情况下:会立即执行脚本,所以通常建议把script放在body最后
<script src="script.js"></script>
②async:有async的话,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。
但是多个js文件的加载顺序不会按照书写顺序进行。
<script async src="script.js"></script>
③derer:有derer的话,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成,并且多个defer会按照顺序进行加载。
<script defer src="script.js"></script>
promise和 async await 区别
①Promise的出现解决了传统callback函数导致的“地域回调”问题,形成回调链,在复杂的开发环境中语法会显得不美观。async /await代码看起来会简洁些,使得异步代码看起来像同步代码。
②async/await与Promise一样,是非阻塞的
③async/await是基于Promise实现的,理解为进阶的Promise,它不能用于普通的回调函数
执行的时间的区别。执行时间,defer会在文档解析完之后执行,并且多个defer会按照顺序执行,而async则是在js加载好之后就会执行,并且多个async,哪个加载好就执行哪个。
①在没有defer或者async的情况下:会立即执行脚本,所以通常建议把script放在body最后
<script src="script.js"></script>
②async:有async的话,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。
但是多个js文件的加载顺序不会按照书写顺序进行。
<script async src="script.js"></script>
③derer:有derer的话,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成,并且多个defer会按照顺序进行加载。
<script defer src="script.js"></script>