javascript单线程
浏览器端,复杂的UI环境会限制多线程语言的开发。
例如,一个线程在操作一个DOM元素时,另一个线程需要去删除DOM元素,
这个之间就需要进行状态的同步,何况前端可能不止操作这么一个DOM元素。
所以,为了避免在开发过程中,去进行复杂的同步,选用单线程语言进行开发是最好的解决方案
栈
栈就是和列表类似的一种数据结构,它可以用来解决计算机世界里很多的问题。栈是一种高效的数据结构,因为数据只能在栈顶添加或删除,所以这样的操作很快,而且容易实现。
栈的使用遍布程序语言的方方面面,从表达式求值到处理函数调用; 函数调用形成了一个栈帧
具有一种后入先出(LIFO,last-in-first-out)的数据结构
由于栈具有后入先出的特点,所有任何不在栈顶的元素都无法访问,为了拿到栈底的元素,必选先拿掉上面的元素
function fun1(){
return 'hello hip-hop';
}
function fun2(){
return fun1();
}
function fun3(){
console.log(fun2());
}
fun3(); //'hello hip-hop'
复制代码
堆(引用类型)
当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本开销较大),这个运行时数据区就是堆内存。
堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(方法的参数传递时很常见),则这个对象依然不会被销毁,只有当一个对象没有任何引用变量引用它时,
系统的垃圾回收机制才会在核实的时候回收它对象被分配在一个堆中,即用以表示一个大部分非结构化的内存区域
队列
排队在第一个的人先办理业务,其它人只能排着,直到轮到他们为止只能在末尾插入元素,在队首删除元素。
队列用于存储按顺序排列的数据 先进先出列被用在很多地方。比如提交操作系统执行一系列进程。打印任务池等。一些仿真系统用来模拟银行或杂货店里排队的顾客
事件循环机制
这幅图片中,我们可以看到完整的执行流程,其中涉及到的异步事件有DOM事件、ajax请求和setTimeout。所以,整体的执行流程是这样子的:
- 所有同步任务会在主线程的调用栈中执行。
- 在主线程之外,还有一个任务队列,一旦指定事件发生之后,异步任务就会被放入任务队列中
- 当主线程执行完调用栈中的同步任务时,会遍历任务队列,将任务队列中的任务放入主线程中执行。而之后事件循环一直会去遍历任务队列,一旦有任务放入就会放入主线程中执行。
这样,我们就已经初步了解了同步和异步之间的实现,以及浏览器中的事件循环机制。
任务队列
所谓任务是WebAPIs返回的一个个通知,让JS主线程在读取任务队列的时候得知这个异步任务已经完成,下一步该执行这个任务的回调函数了。
主线程拥有多个任务队列,不同的任务队列用来排列来自不同任务源的任务。
任务源是什么?像setTimeout/Promise/DOM事件等都是任务源,来自同类任务源的任务我们称它们是同源的,比如setTimeout与setInterval就是同源的。
在ES6标准中任务队列又分为宏观任务队列和微观任务队列
宏观与微观任务队列
ES6标准中任务队列存在两种类型,一种就是上边提到的一些队列,
宏观任务队列(macrotask queue):setTimeout、网络请求Ajax、用户IO等,
微观任务队列(microtask queue),Promise就属于微观任务队列
在执行栈执行的过程中会把属于微观任务队列的任务分配到相应的微观任务队列中去。而在调用栈执行空之后,主线程读取任务队列时,会先读取所有微观任务队列,然后读取一个宏观任务队列,再读取所有的微观任务队列
复制代码
setTimeout(function(){console.log(4)},0);
new Promise(function(resolve){
console.log(1)
for( var i=0 ; i<10000 ; i++ ){
i==9999 && resolve()
}
console.log(2)
}).then(function(){
console.log(5)
});
console.log(3);
复制代码
- 脚本开始执行,最先遇到setTimeout,交给浏览器去计时,达到setTimeout限制最短计时之后,把这个任务推入setTimeout队列。
- 遇到Promise构造函数,构造函数参数执行,输出1,调用resolve改变Promise对象的状态,然后输出2。
Promise对象调用then方法,将这个任务推入Promise任务队列。
执行console.log(3),输出3。 - 调用栈为空,读取任务队列,按照 读取所有微观任务队列 -> 执行 ->
- 读取一个宏观任务队列 -> 执行 ->
- 读取所有微观任务队列 -> 执行 ->
- 再读取一个宏观任务队列…的顺序。
- 读取所有微观任务队列中的任务,执行这些任务指定的回调函数。执行then指定的回调函数,输出5(微观任务队列也具有优先级)。
- 最后读取到setTimeout的任务,执行回调函数,输出4。
所以最后的输出顺序是1,2,3,5,4,而不是1,2,3,4,5。如果不清楚微观任务队列的执行机制,很容易将两个异步任务归为一类,将执行顺序判断错误
零延迟
零延迟并不是意味着回调会立即执行。 在零延迟调用 setTimeout 时,其并不是过了给定的时间间隔后就马上执行回调函数。
其等待的时间基于队列里正在等待的消息数量。 在下面的例子中,"this is just a message" 将会在回调
(callback) 获得处理之前输出到控制台, 这是因为延迟是要求运行时 (runtime) 处理请求所需的最小时间,但不是有所保证的时间
(function () {
console.log('this is the start');
setTimeout(function cb() {
console.log('this is a msg from call back');
});
console.log('this is just a message');
setTimeout(function cb1() {
console.log('this is a msg from call back1');
}, 0);
console.log('this is the end');
})();
// "this is the start"
// "this is just a message"
// "this is the end"
// "this is a msg from call back"
// "this is a msg from call back1"复制代码