回流与重绘
-
DOM树的构建
- 浏览器把获取到的HTML代码解析成1个DOM树
- HTML中的每个标签都是DOM树中的1个节点,根节点就是常用的document对象。
- DOM树里包含了所有HTML标签,包括display:none隐藏,还有用JS动态添加的元素等
- 遵循深度优先解析原则,而不是广度优先。
- DOM树构建中节点的解析和资源的加载是异步的:img节点加载完后,图片不会立马显示出来
-
CSS树的构建
- 解析的过程中会去掉浏览器不能识别的样式
- 以css3里面的transform为例子 -moz-tranform (火狐)会被去除
-
渲染树
- DOM和CSS树结合会生成render树
- render tree中每个NDDE都有自己的style
- render tree不包含display:none,heade 之类不需要绘制的节点。
- visibility:hidden和对应的节点是包含在渲染树上的.
- 渲染树上的每一个节点都会被当做一个盒子box,浏览器开发者工具中鼠标移动到每个元素都显示一个蓝色盒子
-
绘制
- 一旦render tree构建完毕后,浏览器就可以根据render tree来绘制页面了
-
回流和重绘
- 当render tree里面元素如尺寸,布局,隐藏这些需要重新构建渲染树的过程称为回流
- reflow:当render树中的一部分或者全部因为大小边距等问题发生改变而需要重建的过程叫做回流
- repaint:当元素的一部分属性发生变化,如外观背景色不会引起布局变化而需要重新渲染的过程叫做重绘
- 回流完成后,浏览器会根据改变的渲染树重新绘制需要改变的画面,这就是重绘,回流一定会引起重绘
- 每个页面需要回流一次,页面加载本身就是一次回流
- dom节点的增加删除
- dom节点位置变化
- 元素的尺寸 边距 填充 边框 宽高
- dom节点 display属性的显示与否
- 页面渲染初始化
- 浏览器窗口尺寸的变化->热size
- 向浏览器请求某些样式信息:offset scroll client width height getComputedStyle
- 不需要进行渲染树重构的操作过程叫重绘
- 回流一定会产生重绘,但重绘不一定是回流导致的
- 在回流的时候,浏览器会使渲染树中受到影响到部分失效效,并重新构造这部分渲染树,回流之后,重新绘制受影响的部分回到屏幕中
- 在body或某个元素中最前面加一个dom节点,它后面的所有元素都会回流,如果dom节点加在最后,只需要一次回流
var s = document.body.style; s.padding = "2px"; // 回流+重绘 s.border = "1px solid red"; // 再一次 回流+重绘 s.color = "blue"; // 再一次重绘 s.backgroundColor = "#ccc"; // 再一次 重绘 s.fontSize = "14px"; // 再一次 回流+重绘 // 添加node,再一次 回流+重绘 document.body.appendChild(document.createTextNode('abc!'));
- 回流比重绘的代价要高,至于具体的花销跟render树有多少节点需要重新构建有关。
- 浏览器的处理
- 浏览器是不是真的每次js语句引起了回流他就执行一下呢?
- 等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。
- 尽管浏览器挺机智地帮我们优化了代码,我们自己作死也是没救的,比如你去请求width的属性的时候,浏览器为了给你返回一个比较精确的答案,他会提前flush队列,因为队列中可能会有影响这些值的操作。
- 编程中避免回流与重绘
- 不要只用用js操作dom的样式,而是提前定义好class style,然后改变对应dom的class即可,这只有一次回流
- style.cssText可以动态添加
document.getElementsByTagName('div')[0].style.cssText = 'width:15px;height:15px'
- 文档碎片DocumentFragment进行缓存操作,引发一次回流和重绘
var oFragment = document.createDocumentFragment(); for(var i=0;i<10;i++){ var oDiv = document.createElement('div'); oDiv.className = 'box'; oFragment.appendChild(oDiv) } document.body.appendChild(oFragment)
-
DocumentFragment 节点不属于文档树,继承的 parentNode 属性总是 null。
不过它有一种特殊的行为,该行为使得它非常有用,即当请求把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。这使得 DocumentFragment 成了有用的占位符,暂时存放那些一次插入文档的节点。它还有利于实现文档的剪切、复制和粘贴操作。
其实他就是一个游离在DOM树外面的容器,所以你在把它插入文档节点之前,随便给他增删节点都不会引起回流
-
- 使用display:none,只引发两次回流和重绘。道理跟上面的一样。因为display:none的元素不会出现在render树
var oBox = document.getElementsByClassName('box')[0]; var oBoxStyle = oBox.style; oBox.onmouseover = function(){ oBoxStyle.display = 'none'; oBoxStyle.width = '200px'; oBoxStyle.height = '200px'; oBoxStyle.backgroundColor = 'green'; oBoxStyle.display = 'block' }
- 不要经常访问会引起浏览器flush队列的属性,非要高频访问的话建议缓存到变量;
- 将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位;
- 尽量不要使用表格布局,如果没有定宽表格,一列的宽度由最宽的一列决定,那么很可能在最后一行的宽度超出之前的列宽,引起整体回流造成table可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。
document.write
- Document.write() 方法将一个文本字符串写入一个由 document.open() 打开的文档流(document stream)。
- 如果document.write()在DOMContentLoaded或load事件的回调函数中,当文档加载完成,则会先清空文档(自动调用document.open()),再把参数写入body内容的开头。
- 在head中运行document.write(),则参数写在body内容的开头。
<!-- 运行前 -->
<head>
<script>
document.write('<p>test</p>');
</script>
</head>
<body>
<h2>write()</h2>
</body>
<!-- 运行后 -->
<head>
<script>
document.write('<p>test</p>');
</script>
</head>
<body>
<p>test</p>
<h2>write()</h2>
</body>
- 在body中运行document.write(),则参数写在运行的script标签后面
<!-- 运行前 -->
<div>
<script>
document.write('<p>test</p>');
</script>
<p>content</p>
</div>
<!-- 运行后 -->
<div>
<script>
document.write('<p>test</p>');
</script>
<p>test</p>
<p>content</p>
</div>
- 在onload中运行document.write()
页面只会显示document.write()的内容,之前所有内容被覆盖
<script>
window.onload = function () {
document.write('<p>test</p>');
}
// document.addEventListener('DOMContentLoaded', function () {
// console.log('DOMContentLoaded')
// document.write('<p>test</p>');
// })
// 同理
</script>
<div>
hello
</div>
<!-- ============ -->
<html><head></head><body><p>test</p></body></html>
- 同步引用外部js,参数也是写在运行的script标签后面
- 异步引用外部js,必须先运行document.open()清空文档,然后才能运行document.write(),参数写在body内容的开头。如果不先运行document.open(),直接运行document.write(),则无效
时间线
浏览器时间线
定义:在浏览器加载页面开始的那一刻到页面加载完全结束的这个过程中,按顺序发生的每一件事情的总流程
-
创建Document,这个阶段 document.readyState = ‘loading’。
-
开始解析Web文档,创建dom树.
-
遇到link外部css,创建线程加载,并继续解析文档。
-
遇到script外部js,并且没有设置async、defer,浏览器加载,并阻塞整个页面,等待js加载完成并执行该脚本,然后继续解析文档。
-
遇到script外部js,并且设置有async、defer,浏览器创建线程加载src资源,并继续解析文档。
- 对于async属性的脚本,脚本加载完成后立即执行(异步禁止使用document.write())。
- 对于deferc属性的脚本,等待文档加载完成后执行js。
- defer:这个属性的用途是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在script元素中设置defer属性,相当于告诉浏览器立即下载,但延迟执行。
HTML5规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个延迟脚本执行,而这两个脚本会先于DOMContentLoaded事件执行。
- async:同样与defer类似,async只适用于外部脚本文件,并告诉浏览器立即下载文件。但与defer不同的是,标记为async的脚本并不保证按照它们的先后顺序执行。
- 概括来讲,就是这两个属性都会使script标签异步加载,然而执行的时机是不一样的。
- 同时使用async和defer是为了解决兼容性问题
- 最稳妥的办法:如果存在dom操作还是把script写在body底部,没有兼容性问题,没有白屏问题,没有执行顺序问题,如果js不存在dom操作也没有依赖关系推荐async,如果有依赖关系使用defer。
-
遇到img等,先正常解析dom结构,然后浏览器异步加载src资源,并继续解析文档。
-
当文档解析完成时
- document.readyState = ‘interactive’。interactive交互的意思
- 文档解析完成是渲染树构建完成,但资源不一定加载完成
- 文档解析完成后,所有设置有defer的脚本会按照顺序执行(注意与async的不同,但同样禁止使用document.write())
- document对象触发DOMContentLoaded事件,这也标志着程序执行从同步脚本执行阶段,转化为事件驱动阶段。
-
当所有async的脚本加载完成并执行后、img等加载完成后
- document.readyState = ‘complete’ 文档加载完成
- window对象触发onload事件。
-
此后,以异步响应方式处理用户输入、网络事件等。
console.log(document.readyState)
document.onreadystatechange = function(){
console.log(document.readyState)
}
//DOMContentLoaded需要用addEventListener的方式添加
document.addEventListener('DOMContentLoaded',function(){
console.log('DOMContentLoaded','文档解析完成这里就可以操作dom了')
})
window.onload = function(){
console.log('window.onload','文档资源加载完成')
}
function domReady(fn){
if(document.addEventListener){
document.addEventListener('DomContentLoaded',function(){
document.removeEventListener('DomContentLoaded',arguments.callee,false);
fn();
})
}else if(document.attachEvent){
document.attachEvent('onreadystatechange',function(){
if(this.readyState === 'completed'){
fn()
}
})
}
}
浏览器请求网页过程
DNS解析->IP地址->TCP三次握手四次挥手->http连接
浏览器模型
- 浏览器是多进程的
- browser进程
- 第三方插件进程
- GPU进程
- 浏览器渲染引擎进程(浏览器内核)(多线程)
- js引擎线程(单线程)->为什么是单线程?->为了解决多线程情况下DOM处理冲突
- 解决js单线程大数据的处理 -> ssr服务端渲染 webworker 异步
- GUI线程(与js线程互斥)
html <div> <button>button</button> </div> <script> var btn = document.getElementsByTagName('button')[0]; btn.onclick = function () { console.log(1); while (1) { //阻塞js线程运行 // button GUI 动画不会被渲染 // js和gui线程互斥 } } </script>
- http网络请求线程 webAPIs 异步处理 事件队列
- 定时器触发线程 webAPIs
- 浏览器事件处理线程 webAPIs
- js引擎线程(单线程)->为什么是单线程?->为了解决多线程情况下DOM处理冲突
- js是单线程的但同时可以执行异步任务
- 异步的原理是通过事件驱动模型来模拟的
事件驱动模型
https://www.codercto.com/a/45633.html
-
在单线程同步模型中,任务按照顺序执行。如果某个任务因为I/O而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。如果任务之间并没有互相依赖的关系,但仍然需要互相等待的话这就使得程序不必要的降低了运行速度。
-
在多线程版本中,任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。与完成类似功能的同步程序相比,这种方式更有效率,但程序员必须写代码通过线程同步机制如锁来保护共享资源,防止其被多个线程同时访问。
-
在事件驱动版本的程序中,多个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时,注册一个回调到事件循环中,然后当I/O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽可能的得以执行而不需要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为,因为程序员不需要关心线程安全问题。
-
JS 引擎有很多个,但是用的最多,性能最好的是 Google 用 C++开发的 V8 引擎, 下文的例子实在 Chrome 上运行的。
- 事件循环(Event Loop): 处理主线程(V8 引擎的代码执行线程)和其他线程的通讯
- JS 引擎(V8):解释、执行 JS 代码
- WebAPIs:浏览器提供的一些 API(DOM 操作,异步,定时器等)
- 回调队列(Callback Queue):回调函数在这里排队,等待主线程翻牌子。
-
Event Loop
简单理解, Event Loop 就是负责主线程合其他进程之间的通讯的线程栗子。
如果某个任务很耗时,比如涉及很多 I/O(输入/输出)操作,那么线程的运行大概是下面的样子。
上图的绿色部分是程序的运行时间,红色部分是等待时间。
可以看到,由于 I/O 操作很慢,所以这个线程的大部分运行时间都在空等 I/O 操作的返回结果。
这种运行方式称为 “同步模式”(synchronous I/O) 或 “堵塞模式”(blocking I/O) 。
Event Loop 就是为了解决这个问题而提出的。
“Event Loop 是一个程序结构,用于等待、发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)”
简单说,就是在程序中设置两个线程:一个负责程序本身的运行,称为”主线程”;另一个负责主线程与其他进程、线程(主要是各种 I/O 操作)的通信,被称为”Event Loop 线程”(可以译为”消息线程”)。
上图主线程的绿色部分,还是表示运行时间,而橙色部分表示空闲时间。
每当遇到 I/O 的时候,主线程就让 Event Loop 线程去通知相应的 I/O 程序,然后接着往后运行,所以不存在红色的等待时间。
等到 I/O 程序完成操作,Event Loop 线程再把结果返回主线程。主线程就调用事先设定的回调函数,完成整个任务。
可以看到,由于多出了橙色的空闲时间,所以主线程得以运行更多的任务,这就提高了效率。这种运行方式称为”异步模式”(asynchronous I/O)或”非堵塞模式”(non-blocking mode)。
-
JS 引擎的两个重要组成部分
-
内存堆
计算机程序在运行期中动态分配使用内存(比如, let list = [1,2,3,4,5] , list 变量的值就存放在内存堆里面) -
调用栈
调用栈是解析器(如浏览器中的的 javascript 解析器)的一种机制,可以在脚本调用多个函数时,跟踪每个函数在完成执行时应该返回控制的点。(如什么函数正在执行,什么函数被这个函数调用,下一个调用的函数是谁)
参考: https://developer.mozilla.org/zh-CN/docs/Glossary/Call_stackfunction greeting() { sayHi() } function sayHi() { console.log('Hi') } greeting()
- 执行步骤描述如下:
- 函数声明,变量定义先不管
- 执行到 greeting() , 压入栈中
- greeting 中调用了 sayHi() , 把 sayHi() 压入栈中
- sayHi 中执行 console.log ,压入栈中
- 执行 console.log(‘hi’) , 控制台看到 Hi,把 console.log 从栈中删除
- sayHi 执行结束,把 sayHi 从栈中删除
- greeting 执行结束,把 greeting 从栈中删除
- 本次执行结束(栈为空)
- 执行步骤描述如下:
-
-
同步获取数据在有什么问题? ==》 阻塞问题
JavaScript 是单线程的语言,执行同步请求(或者耗时操作)的时候,页面就卡住了,等请求结束,页面才可以操作, 这种现象叫『阻塞(Blocking)』。 -
阻塞问题什么解决呢?==》 异步操作
-
异步操作在调用栈什么样的呢?
console.log('hi') setTimeout(function() { console.log('I am a setTimeout') }, 1000) console.log('end')
- 执行步骤描述:
- 执行 console.log(‘Hi’) [压入栈,执行,执行完,从栈中删除(后面同样操作,统一说『执行』)]
- 执行 setTimeout , 检测到是浏览器提供的 API,因此交给浏览器的线程去执行,主线程继续往下执行。
- 执行 console.log(‘end’)
- 等待浏览器线程处理异步操作完成(注意:异步代码执行 和 步骤3 是 并行 的)
- 浏览器线程 处理 setTimeout 结束,就会把相应的回调函数放到 回调队列(Calback Queue) 中,等待主线程翻牌子
- 主线程调用栈为空时,会去判断 回调队列 是否有函数需要执行, 有则拿出回调函数,压入到 调用栈 去执行,没有就等会再来看看。
- 执行回调函数
- 执行 console.log(‘I am a setTimeout’)
- 结束,调用栈为空
- 执行步骤描述:
-
JavaScript 是单线程,又是异步的,这两者有冲突吗?
- 单线程
因为 JavaScript 引擎只有一个主线程在执行 JS 的代码,所以同一时刻只会有一段代码在执行。 - 异步操作
异步操作是浏览器的多个常驻线程去执行的, 这个是浏览器的行为。- 异步 Http 请求,是浏览器的 执行线程 和 事件触发线程 共同完成的, 执行线程去执行异步请求, 事件触发线程监测 异步请求是否完成,完成则把回调函数插入 回调队列(Callback Queue),JS 引擎主线程在执行完调用栈的函数后(调用栈为空时),会去查看 回调队列是否有需要执行的回调,有则一个一个拿出来执行,没有就等一会儿在来看。
- 单线程
异步加载
CSS异步加载
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
<link rel="stylesheet" href="index.css"/>
<!-- link 默认异步加载 新开了一个线程解析css tree -->
<script src="index.js"></script>
<!-- js 默认同步加载 阻塞模式-->
<!-- js中经常要操作dom.必须在dom tree 构建完成之后, 异步的话 无法控制执行时机 -->
<!-- js中操作dom的话 要放在body后面 -->
</head>
<body>
</body>
</html>
JS异步加载defer,async
- defer
- async
- js动态script就是异步加载的过程
- 解决defer兼容性
<!-- 等其他资源load结束后,在异步加载js --> <!DOCTYPE html> <html lang="en"> <head> <title>Document</title> <script></script> <script> (function () { function async_load() { var s = document.createElement('script'); var sc = document.getElementsByTagName('script')[0]; s.type = 'text/javascript'; // s.async = true // 无实际意义 s.src = 'utils.js' // 只是加载但不执行 // document.body.appendChild(s)//执行 sc.parentNode.insertBefore(s, sc) } if (window.attachEvent) { window.attachEvent('onload', async_load); } else { window.addEventListener('load', async_load, false) //window.onload整个页面资源加载完毕后才会触发 } })() </script> </head> <body> </body> </html>
- 判断script加载完成执行指定事件,但IE只有window有onload事件,chrome中script有onload事件
<!-- 判断script异步加载完成后执行指定事件 --> <!DOCTYPE html> <html lang="en"> <head> <title>Document</title> <script></script> <script> loading_sc('utils.js','test') function loading_sc(url,fn){ var s = document.createElement('script'); var sc = document.getElementsByTagName('script')[0]; s.type = 'text/javascript'; if(s.readyState){ s.onreadstatechange = function(){ var state = s.readyState; if(state === 'complete' || state === 'loaded'){ fn(); } } }else{ s.onload = function(){ fn(); } } s.src = url;//已经监听load,这里加载资源,如果是loaded状态执行回调 sc.parentNode.insertBefore(s,sc); } </script> </head> <body> </body> </html>
- 微信的header里面会同步阻塞加载sdk.js,所以我们用微信访问网站比较慢,会有明显的首页白屏
图片懒加载和预加载
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="wrap">
<ul class="img-list"></ul>
</div>
<div style="display: none;" id="img-data">
[{"img":"https://gitee.com/jingyu7/pic/raw/master/202112260910338.png","name":"图片1"},
{"img":"https://gitee.com/jingyu7/pic/raw/master/202112260910338.png","name":"图片2"},
{"img":"https://gitee.com/jingyu7/pic/raw/master/202112260910338.png","name":"图片3"},
{"img":"https://gitee.com/jingyu7/pic/raw/master/202112260910338.png","name":"图片4"},
{"img":"https://gitee.com/jingyu7/pic/raw/master/202112260910338.png","name":"图片5"},
{"img":"https://gitee.com/jingyu7/pic/raw/master/202112260910338.png","name":"图片6"},
{"img":"https://gitee.com/jingyu7/pic/raw/master/202112260910338.png","name":"图片7"},
{"img":"https://gitee.com/jingyu7/pic/raw/master/202112260910338.png","name":"图片8"},
{"img":"https://gitee.com/jingyu7/pic/raw/master/202112260910338.png","name":"图片9"},
{"img":"https://gitee.com/jingyu7/pic/raw/master/202112260910338.png","name":"图片10"},
{"img":"https://gitee.com/jingyu7/pic/raw/master/202112260910338.png","name":"图片11"},
{"img":"https://gitee.com/jingyu7/pic/raw/master/202112260910338.png","name":"图片12"},
{"img":"https://gitee.com/jingyu7/pic/raw/master/202112260910338.png","name":"图片13"},
{"img":"https://gitee.com/jingyu7/pic/raw/master/202112260910338.png","name":"图片14"},
{"img":"https://gitee.com/jingyu7/pic/raw/master/202112260910338.png","name":"图片15"}]
</div>
<script type="text/html" id="imp-tpl">
<li class="img-item">
<div class="img-wrap">
<img src="" class="list-img" data-src="{{img}}" alt="{{name}}">
</div>
<div class="img-tt">
<h1>{{name}}</h1>
</div>
</li>
</script>
</body>
<script>
// window.onload return void
function throttle(fn, timeout) {
clearTimeout(fn._st);
fn._st = setTimeout(() => {
fn()
}, timeout);
}
(function (win, doc) {
var imgList = doc.getElementsByClassName('img-list')[0];
var data = JSON.parse(doc.getElementById('img-data').innerHTML);
var imgTpl = doc.getElementById('imp-tpl').innerHTML;
var imgs = doc.getElementsByClassName('list-img')
var init = function () {
imgList.innerHTML = renderList(data)
bindEvent();
setTimeout(() => {
win.scrollTo(0, 0)
}, 150);
}
function bindEvent() {
// win.onload = win.onscroll = throttle(imgLazyLoad(imgs),300)
win.onload = win.onscroll = function () {
// imgLazyLoad(imgs)()
throttle(imgLazyLoad(imgs), 3000)
}
}
function renderList(data) {
var list = '';
data.forEach(function (e) {
list += imgTpl.replace(/{{(.*?)}}/g, function (node, key) {
return {
img: e.img,
name: e.name
}[key]
})
})
return list;
}
init()
})(window, document)
function imgLazyLoad(imgage) {
var imgLen = imgage.length;
var n = 0;
return function () {
var cHeight = document.documentElement.clientHeight;
var sTop = document.documentElement.scrollTop || document.body.scrollTop;
var imgItem;
for (let i = n; i < imgLen; i++) {
const imgItem = imgage[i];
if (imgItem.offsetTop < cHeight + sTop) {
imgItem.src = imgItem.getAttribute('data-src');
// imgItem.removeAttribute('data-src')
// 假如触发多次滚动事件,不知道哪一个会移除data-src,后面的就没有data-src数据
// n++
}
}
}
}
</script>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="wrap">
</div>
</body>
<script>
var div = document.getElementsByTagName('div')[0];
// 单张图片预加载
var img = new Image() // 创建img标签
img.src = "https://gitee.com/jingyu7/pic/raw/master/202112260910338.png";
img.onload = function(){
//图片加载完毕
div.appendChild(img);
}
// 多张图片预加载 foreach -> 单张预加载
</script>
</html>