Appearance
浏览器相关一览
浏览器简史
浏览器列表
- 主流浏览器:
- Chrome、FireFox、IE、Opera、Safari;
- Google Chrome、Amazon Silk、Apple Safari、QQ browser、UC Browser、Samsung Internet
- 内核分类:
- Trident内核: 360 Secure Browser、Internet Explorer、RealPlayer、Tencent Traveler(腾讯TT浏览器)
- Gecko内核: Mozilla Firefox、Firefox for mobile、Yahoo! Browser
- Presto内核: Opera、Internet Channel
- Blink内核: Chromium:Google Chrome、Amazon Silk、Microsoft Edge
- Webkit内核: Safari、Google Chrome for iOS、Firefox for iOS、GNOME Web、X5内核(手机QQ、微信)
浏览器市场占有率分析
专栏学习-《浏览器工作原理与实践》
推荐阅读李兵老师的《浏览器工作原理与实践》专栏,从浏览器的运行机制,Http请求的执行过程,页面渲染流程,再细化到JS的执行机制(包括块级作用域、作用域链以及执行上下文),再到V8引擎的工作原理,浏览器的事件循环机制等。整个浏览器的知识体系都梳理了一遍,很值得进行学习。课程内容主要包含下面几个部分:
- 开篇词 (1讲)
- 宏观视角下的浏览器 (6讲)
- 浏览器中的JavaScript执行机制 (5讲)
- V8工作原理 (3讲)
- 浏览器中的页面循环系统 (6讲)
- 浏览器中的页面 (8讲)
- 浏览器中的网络 (3讲)
- 浏览器安全 (5讲)
- 结束语 (3讲)
- 课外加餐 (6讲)
事件代理
如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上
html
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
let ul = document.querySelector('##ul')
ul.addEventListener('click', (event) => {
console.log(event.target);
})
</script>
事件代理的方式相对于直接给目标注册事件来说,有以下优点
- 节省内存
- 不需要给子节点注销事件
JS运行机制
浏览器缓存机制
浏览器内核
默认每个Tab页面一个进程,互不影响,控制页面渲染,脚本执行,事件处理等(有时候会优化,如多个空白tab会合并成一个进程 Chrome浏览器为每个tab页面单独启用进程,因此每个tab网页都有由其独立的渲染引擎实例。 一个浏览器通常由以下常驻线程组成:
- GUI 渲染线程
- JavaScript引擎线程
- 定时触发器线程
- 事件触发线程
- 异步http请求线程
GUI 渲染线程
GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时, 该线程就会执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态
JavaScript引擎线程
Javascript引擎,也可以称为JS内核,主要负责处理Javascript脚本程序,例如V8引擎。 Javascript引擎线程理所当然是负责解析Javascript脚本,运行代码。
GUI 渲染线程 与 JavaScript引擎线程互斥
为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JavaScript引擎为互斥的关系,
当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。
定时触发器线程
浏览器定时计数器并不是由JavaScript引擎计数的, 因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。
事件触发线程
当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。
异步http请求线程
XMLHttpRequest在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript引擎的处理队列中等待处理。
非I/O的异步API setTimeout()、setInterval()、process.nextTick()、setImmediate()
setTimeout()和setInterval()与浏览器中的API是一致的,分别用于单次和多次定时执行任务。
调用setTimeout()和setInterval()创建的定时器会被插入到定时器观察者内部的一个红黑树中,每次Tick执行时,会从该红黑树中迭代取出定时器对象,检查是否超过定时时间,如果超过,就形成一个时间,它的回调函数将立即执行。
弊端:
1)并非精确的(在容忍范围内)。尽管事件循环十分快,但是如果某一次循环占用的时间较多,那么下次循环时,它也许已经超时很久了。譬如通过setTimeout()设定一个任务在10毫秒后执行,但是在9毫秒后,有一个任务占用了5毫秒的CPU时间片,再次轮到定时器执行时,时间已经过期4毫秒。
2)采用定时器需要动用红黑树,创建定时器对象和迭代等操作,而setTimeout(fn,0)的方式较为浪费性能。使用红黑树的操作时间复杂度为O(lg(n))
一个进程就是一个程序的实例 (操作系统创建内存,用来存放代码、运行中数据和一个执行任务的主线程),线程是不能单独存在的,它是由进程来启动和管理的。
进程中任意一线程执行错误,都会导致整个进程的崩溃。线程之间共享进程中的数据。进程隔离是为保护操作系统中进程互不干扰的技术,每一个进程只能访问自己占有的数据。
早在2007年之前,市面上浏览器都是单进程的,问题:不稳定、不流畅、不安全。后进入多进程浏览器时代。
JS事件循环 Event Loop
JS引擎执行顺序
script脚本当做第一个宏任务开始顺序执行,将代码分为 “同步任务”、“异步任务”;
同步任务会直接进入主线程依次执行;
异步任务会再分为宏任务和微任务;
宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
微任务(优先执行)也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
当主线程内的任务执行完毕,主线程(script中同步任务)为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务;
上述过程会不断重复,这就是Event Loop事件循环;
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
宏任务(宿主: 浏览器/node环境、会触发新一轮Tick)
- script (可以理解为外层同步代码)
- setTimeout/setInterval
- UI rendering/UI事件(用户点击事件)
- postMessage,MessageChannel
- setImmediate,I/O(Node.js)
微任务(JS引擎发起)
- Promise
- MutaionObserver
- Object.observe(已废弃;Proxy 对象替代)
- process.nextTick(Node.js)
4. setTimeout、setInterval、requestAnimationFrame
setInterval()和setTimeout()共享同一个ID池,并且clearInterval()和clearTimeout()在技术上是可互换使用的。
向setInterval()传递一个方法或者函数的时候,需要注意this指针的问题。
js
let myArray = ['zero', 'one', 'two'];
myArray.myMethod = function (sProperty) {
alert(arguments.length > 0 ? this[sProperty] : this);
};
myArray.myMethod(); // 输出 "zero,one,two"
myArray.myMethod(1); // 输出 "one"
setTimeout(myArray.myMethod, 1000); // 1S后输出 "[object Window]"
setTimeout(myArray.myMethod, 1500, "1"); // 1.5S后输出 "undefined"
// 开始我们想通过.call绑定this指向myArray对象,从而使得myArray.myMethod中this指针能够指向myArray
// 而setTimeout内部的this本应该指向window对象,this指向被修改后setTimeout执行报错
setTimeout.call(myArray, myArray.myMethod, 2000); // 报错 Uncaught TypeError: Illegal invocation
setTimeout.call(myArray, myArray.myMethod, 2500, 2); // 报错 Uncaught TypeError: Illegal invocation
// 方法1:改写window.setTimeout和setInterval,当入参为回调函数时重新绑定this指针
var __nativeST__ = window.setTimeout, __nativeSI__ = window.setInterval;
window.setTimeout = function (vCallback, nDelay /*, argumentToPass1, argumentToPass2, etc. */) {
var oThis = this, aArgs = Array.prototype.slice.call(arguments, 2);
return __nativeST__(vCallback instanceof Function ? function () {
vCallback.apply(oThis, aArgs);
} : vCallback, nDelay);
};
window.setInterval = function (vCallback, nDelay /*, argumentToPass1, argumentToPass2, etc. */) {
var oThis = this, aArgs = Array.prototype.slice.call(arguments, 2);
return __nativeSI__(vCallback instanceof Function ? function () {
vCallback.apply(oThis, aArgs);
} : vCallback, nDelay);
};
setTimeout(alert, 1500, 'Hello world!'); // 原来的调用方式功能正常
setTimeout.call(myArray, myArray.myMethod, 2000); // 2S后输出 "zero,one,two"
setTimeout.call(myArray, myArray.myMethod, 2500, 2); // 2.5S后输出 "two"
// 方法2 Function.prototype.bind()
// 方法3 箭头函数
setTimeout((a)=>{
myArray.myMethod(a)
}, 1000, 1) // 1S后输出 "one"
requestAnimationFrame
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。回调函数执行次数通常是每秒60次,但在大多数遵循W3C建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。为了提高性能和电池寿命,因此在大多数浏览器里,当requestAnimationFrame() 运行在后台标签页或者隐藏的<iframe>
里时,requestAnimationFrame() 会被暂停调用以提升性能和电池寿命。
外部css 文件的加载会阻塞 dom解析 么?会阻塞渲染么?
不会阻塞DOM解析,但会阻塞DOM渲染 延迟加载的css会导致样式重新计算,从而导致 无样式闪烁 问题。
默认情况下,CSS 被视为阻塞渲染的资源,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕。 css不阻塞 dom 解析,但是阻塞渲染。按照 google 的文档阐述是这样的,在 domcontentloaded 之前不会执行渲染,因为这个事件标志着 dom 和 cssom 的完成,然后才会开始构建 render tree。
浏览器并不是css外部链接并不会阻塞浏览器的渲染,但是会阻塞文档的 loaded,以及出现样式闪烁。
从event loop规范探究javaScript异步及浏览器更新渲染时机
- 进程:CPU资源分配最小单位,可包含多个线程
- 线程:CPU调度的最小单位,同进程下的线程共享程序的内存空间