javascript 是一门单线程的语言,在同一个时间只能做完成一件任务,如果有多个任务,就必须排队,前面一个任务完成,再去执行后面的任务。作为浏览器端的脚本语言,javascript 的主要功能是用来和用户交互以及操作 dom。假设 javascript 不是单线程语言,在一个线程里我们给某个 dom 节点增加内容的时候,另一个线程同时正在删除这个 dom 节点的内容,则会造成混乱。
由于 js 单线程的设计,假设 js 程序的执行都是同步。如果执行一些耗时较长的程序,例如 ajax 请求,在请求开始至请求响应的这段时间内,当前的工作线程一直是空闲状态, ajax 请求后面的 js 代码只能等待请求结束后执行,因此会导致 js 阻塞的问题。
javascript 单线程指的是浏览器中负责解释和执行 javascript 代码的只有一个线程,即为 js 引擎线程,但是浏览器的渲染进程是提供多个线程的,如下:
js 引擎线程
事件触发线程
定时器触发线程
异步 http 请求线程
GUI 渲染线程
一、异步 & 同步为解决上述类似上述 js 阻塞的问题,js 引入了同步和异步的概念。
1、什么是同步?“同步”就是后一个任务等待前一个任务结束后再去执行。
2、什么是异步?“异步”与同步不同,每一个异步任务都有一个或多个回调函数。webapi 会在其相应的时机里将回调函数添加进入消息队列中,不直接执行,然后再去执行后面的任务。直至当前同步任务执行完毕后,再把消息队列中的消息添加进入执行栈进行执行。
异步任务在浏览器中一般是以下:
网络请求
计时器
DOM 监听事件
...
二、什么是执行栈(stack)、堆(heap)、事件队列(task queue)? 1、执行栈“栈”是一种数据结构,是一种线性表。特点为 LIFO,即先进后出 (last in, first out)。
利用数组的 push 和 shift 可以实现压栈和出栈的操作。
在代码运行的过程中,函数的调用会形成一个由若干帧组成的栈。
function foo(b) { let a = 10; return a + b + 11; } function bar(x) { let y = 3; return foo(x * y); } console.log(bar(7))上面代码最终会在控制台打印42,下面梳理一下它的执行顺序。
console.log 函数作为第一帧压入栈中。
调用 bar,第二帧被压入栈中。帧中包含着 bar 的变量对象。
bar 调用 foo,foo 做一位第三帧被压入栈中,帧中包含着 foo 的变量对象。
foo 执行完毕然后返回。被弹出栈。
bar 执行完毕然后返回,被弹出栈。
log 函数接收到 bar 的返回值。执行完毕后,出栈。此时栈已空。
2、堆对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。
堆和栈的区别首先,stack 是有结构的,每个区块按照一定次序存放,可以明确知道每个区块的大小;heap 是没有结构的,数据可以任意存放。因此,
stack 的寻址速度要快于 heap。
其次,每个线程分配一个 stack,每个进程分配一个 heap,也就是说,stack 是线程独占的,heap 是线程共用的。
此外,stack 创建的时候,大小是确定的,数据从超过这个大小,就发生 stack overflow 错误,而 heap 的大小是不确定的,
需要的话可以不断增加。
上面代码这三个变量和一个对象实例在内存中的存放方式如下。
从上图可以看到,i、y和cls1都存放在stack,因为它们占用内存空间都是确定的,而且本身也属于局部变量。但是,cls1指向的对象实例存放在heap,因为它的大小不确定。作为一条规则可以记住,所有的对象都存放在heap。
接下来的问题是,当Method1方法运行结束,会发生什么事?
回答是整个stack被清空,i、y和cls1这三个变量消失,因为它们是局部变量,区块一旦运行结束,就没必要再存在了。而heap之中的那个对象实例继续存在,直到系统的垃圾清理机制(garbage collector)将这块内存回收。因此,一般来说,内存泄漏都发生在heap,即某些内存空间不再被使用了,却因为种种原因,没有被系统回收。
3、事件队列和事件循环队列是一种数据结构,也是一种特殊的线性表。特点为 FIFO,即先进先出(first in, first out)
利用数组的 push 和 pop 可实现入队和出队的操作。
事件循环和事件队列的维护是由事件触发线程控制的。
事件触发线程线程同样是由浏览器渲染引擎提供的,它会维护一个事件队列。