1.浏览器中的主要进程
1.浏览器进程
主要负责界面展示,用户交互,子进程管理等,网络进程和渲染进程其实是浏览器进程的子进程
2.网络进程
负责加载网络资源,网络进程内部会启动多个线程来处理不同的网络任务
3.渲染进程
渲染进程启动后,会开启一个渲染主进程,主线程负责执行HTML\CSS\JS代码 默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同标签页之间互不影响
老版本的谷歌浏览器会特别的占内存,原因可能就是开了很多标签页,起了很多的渲染进程
谷歌想要同域下的内容按照规则会只起一个渲染进程,还在兼容运行中
2.渲染主线程是如何工作的
渲染主线程是浏览器最繁忙的线程,需要处理的任务包括但不限于
- 解析HTML
- 解析CSS
- 计算样式
- 布局
- 处理图层
- 每秒把页面画60次
- 执行全局的js代码
- 执行事件处理函数
- 执行计时器的回调函数
- ……
思考题:为什么渲染进程不适用于多个线程来处理这些事情
这么多任务,渲染主线程遇到了一个前所未有的问题:如何调度任务?排队
- 在最开始的时候,渲染主线程会进入一个无限循环
- 每一次循环会检查消息队列中是否有任务存在,如果有,就取出第一个任务执行,执行完一个后进入下一次循环,如果没有,则进入休眠状态
- 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新的任务会添加到消息队列的末尾,在添加新任务时,如果主任务是休眠状态,则会将其唤醒以继续循环拿取任务
整个过程称之为(事件循环)消息循环
下面这部分是浏览器源码中对事件循环部分的书写
void MessagePumpDefaule:: Run(Delegate* delegate) {
AutoReset<bool> auto_reset_keep_running(& keep_running_, true)
for(;;) {
#if BUILDFLAG(IS_APPLE)
mac::ScopedNSAutoreleasePool autorelease_pool;
#endif
Delegate::NextWorkInfo next_work_info =
delegate->DoWork():
bool has_more_immediate_work = next_work_info.is_immediate();
if(!keep_running_)
break;
if(has_more_immediate_work)
continue;
}
3异步
代码在执行的过程中会遇到一些无法立即处理的任务:
- 计时完成后的任务 ---> setTimeOut setInterval
- 网络通信完成后需要执行的任务 ---> xhr fetch
- 用户操作后需要执行的任务 ---> addEventListener 如果让浏览器的渲染主线程等待这些任务运行到可以在其中运行的内容,就会造成浏览器卡死的现象
如何理解JS的异步?
在浏览器中,js是一门运行在浏览器唯一一个渲染主线程中的单线程语言,渲染主线程承担着诸多工作,渲染页面,执行js都会在其中运行。
如果使用同步的方式就极有可能导致主线程阻塞,从而导致消息队列中很多其他的任务无法执行,这样一来,一方面等待的这部分时间白白消耗了渲染主线程的时间,另一方面,页面迟迟无法更新,给用户造成浏览器卡死的感觉
所以浏览器采用异步的方式来避免阻塞的问题。具体的做法是:当某些比如计时器、网络求请、事件监听等任务发生时,渲染主线程将这些任务交给其他线程去处理,而自身立即结束当前任务的执行,从而执行后续的代码。当被渲染主线程交付这部分任务的其他线程完成时,将事先传递的回调函数,包装成任务(js中我们看到的是回调函数,其实在浏览器底层都封装成了一个个对象,一个对象结构就是一个任务),加入到消息队列的末尾,进行排队,等待渲染主线程的调度执行
在这种异步的模式下,浏览器永不阻塞,永远都在执行消息队列里的任务,最大程度的保证单线程的高效运行ps: 其实就跟饭店一样,每桌顾客点的单就是一个异步的任务,厨师不停的炒菜就相当于渲染主线程不停的在调度消息队列中的任务,
JS为什么会阻碍渲染?
因为浏览器的渲染主线程是单线程,而JS和浏览器的渲染都在浏览器的渲染主线程上
4任务有优先级吗
任务没有优先级,在消息队列中先进先出
但是消息队列是有优先级的
根据w3c的最新解释
- 每个任务都有一个任务类型,同一个类型的任务必须在同一个队列,不同类型的任务可以分属于不同的队列。在每一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行
- 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行
随着浏览器复杂程度的急剧提升,w3c不再使用宏队列的说法
w3c对于浏览器队列的解释
When a user agent is to perform a microtask checkpoint:
- If the event loop's performing a microtask checkpoint is true,then return.
- Set the event loop's performing a microtask checkpoint to true.
- While the event loop's microtask queue is not empt
1.Let oldestMicrotask be the result of dequeuing from the event loop's microtask queue.
2.Set the event loop's currently running task to oldestMicrotask.
3.Run oldestMicrotask.This might involve invoking scripted callbacks,which eventually calls the clean up after running script steps,which call this perform a microtask checkpoint algorithm again,which is why we use the performing a microtask checkpoint flag to avoid reentrancy. 4.Set the event loop's currently running task back to null.
- For each environment settings object whose responsible event loop is this event loop,notify about rejected promises on that environment settings object.
- Cleanup Indexed Database transactions.
- Perform ClearKeptObjects().
When weakRef.prototype.deref()returns an object,that object is kept alive until the next invocation of ClearKeptobjects0,after which it is again subject to garbage collection.
- Set the event loop's performing a microtask checkpoint to false.
在目前的chrome浏览器的视线中,至少包含了下面的队列:
- 延时队列:用于存放计时器到达后的回调任务,任务优先级【中】
- 交互队列:用于存放用户操作后产生的事件处理任务,任务优先级【高】
- 微队列:用户存放需要最快执行的任务,任务优先级【最高】
添加任务到微队列的主要方式是使用 Promise、MutationObserver 例如:
// 立即把一个函数添加到微队列
Promise.resolve().then(函数)
其他队列与开发关系不大,无需考虑
如何理解JS的事件循环?
事件循环又叫消息循环,官方叫做event loop,浏览器内部的实现的那个函数叫message loop,是浏览器渲染主线程的工作方式,在Chrome源码中,它会开启一个不会结束的for循环,每次循环,从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。
过去把消息队列简单分为宏队列和微队列,这种说法目前已经无法满足复杂的浏览器环境,取而代之的是一种更为灵活多变的的处理方式
根据w3c官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同的队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务,但是浏览器必须有一个微队列,微队列的任务优先级一定是最高的,必须有限调度执行
JS中的计时器能做到精准计时吗?为什么
不行
- 计算机硬件没有原子钟,无法做到精确计时,他是靠着cpu寄存器做的时间
- 操作系统的计时函数本身就有少量偏差,mac和win的实现方式都有不同,所有由于JS的计时器最终调用的还是操作系统的函数,所以也就携带了偏差
- 按照w3c的标准,浏览器实现计时器时,如果嵌套层级超过五层,则会带有4毫秒的最少事件,这样的计时事件少于4毫秒又带来了偏差
- 受事件循环的影响,计时器的回调函数只能在主线程空闲是运行,因此又带来了偏差