js的运行机制
浏览器线程
js运作在浏览器中,是单线程的,即js代码始终在一个线程上执行,这个线程称为js引擎线程。
但浏览器是多线程的,除了js引擎线程,它还有:
-
UI渲染线程
-
浏览器事件触发线程
-
http请求线程
-
EventLoop轮询的处理线程
……..
这些线程的作用
- js线程用于执行js任务
- UI线程用于渲染页面
- 浏览器事件触发线程用于控制交互,响应用户
- http线程用于处理请求,ajax是委托给浏览器新开一个http线程
- EventLoop处理线程用于轮询消息队列
浏览器中的js任务
- 执行JavaScript代码
- 对用户的输入(包含鼠标点击、键盘输入等等)做出反应
- 处理异步的网络请求
理解js单线程
- 单线程的含义是js只能在一个线程上运行,也就说,js同时只能执行一个js任务,其它的任务则会排队等待执行。
- js是单线程的,并不代表js引擎线程只有一个。js引擎有多个线程,一个主线程,其它的后台配合主线程。
- 多线程之间会共享运行资源,浏览器端的js会操作dom,多个线程必然会带来同步的问题,所有js核心选择了单线程来避免处理这个麻烦。js可以操作dom,影响渲染,所以js引擎线程和UI线程是互斥的。这也就解释了js执行时会阻塞页面的渲染。
js的运行机制
(1)所有同步任务都在主线程上执行,形成一个执行栈。
(2)主线程之外,还存在一个”任务队列”。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
(3)一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
简单说,浏览器的两个线程:一个负责程序本身的运行,称为”主线程”;另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为”Event Loop线程”(可以译为”消息线程”)。
宏任务和微任务
事件轮询其实就是轮流询问宏任务队列和微任务队列
执行顺序:
- 先执行同步代码再执行异步代码
- 先执行微任务再执行宏任务
常见宏任务: setTimeout 、setInterva、I/O
常见微任务:process.nextTick(nodejs独有) 、 Promise.then 、catch、finally
例子
由于js是单线程的,代码从上往下依次执行。
- setTimeout是宏任务,放入宏任务队列
- promise构造函数中的代码是同步执行的,但setTimeout是宏任务,又放入宏任务队列,打印promise
- 执行同步代码,打印输出33333,接着执行循环代码,输出1到19999
- 根据执行优先级,即使在打印1到19999时,定时器已经到了时间,也不会去执行setTimeout。若打印完19999,根据宏任务队列先进先出原则,打印11111
- 执行剩下的宏任务,在promise中执行resolve()后才会把回调函数then中的代码放入微任务队列中。此时虽然微任务比宏任务的优先级别高,但此刻正在处理宏任务,最节省资源和快捷的方式是先把当前宏任务执行完毕,于是打印22222
- 最后执行then微任务,打印“成功”
setTimeout(() => {
console.log("11111");
}, 0);
let promise = new Promise(resolve => {
setTimeout(() => {
resolve();
console.log("22222");
}, 0);
console.log("promise");
}).then(value => console.log("成功"));
console.log("33333");
for(let i=0;i<20000;i++) {
console.log(i);
}
//promise
//33333
//0
//...
//19999
//11111
//22222
//成功
web worker
Web Worker是h5新增的,它的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。
在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。
Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。
使用注意点
- 同源限制
分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。(所谓同源是指,域名,协议,端口相同。)
-
DOM 限制
Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。
-
通信联系
Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。
-
脚本限制
Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。
-
文件限制
Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。
用法
主线程
-
用new命令,Worker()构造函数,创建一个Worker线程
// main.js var myworker = new Worker('work.js');
-
主线程是通过myworker.postMessage()方法向子线程传递消息
myworker.postMessage('hello world') myworker.postMessage({name: 'charming'})
worker.postMessage()方法的参数,就是主线程传给 Worker 的数据。它可以是各种数据类型,包括二进制数据。
-
主线程是通过myworker.onmessage()方法监听子线程发回来的消息
myworker.onmessage = function (event) { console.log('接收到子线程的消息:' + event.data) }
-
终止子线程
myworker.terminate()
子线程
子线程代码都写在work.js里面
-
子线程内部通过监听message接收主线程传递过来的信息。
self.addEventListener('message', function (e) { self.postMessage('从主线程接收到: ' + e.data); }, false);
在上面代码中,self表示子线程自身。即子线程的全局对象。等同于下面两种写法
this.addEventListener('message', function (e) { this.postMessage('从主线程接收到: ' + e.data); }, false); addEventListener('message', function (e) { postMessage('从主线程接收到: ' + e.data); }, false);
当然直接用onmessage监听message事件也是可以的
onmessage = function(e) { postMessage('从主线程接收到: ' + e.data); }