浏览器的内核是指支持浏览器运行的最核心的程序,分为两个部分的,一是渲染引擎,另一个是JS引擎。渲染引擎在不同的浏览器中也不是都相同的。目前市面上常见的浏览器内核可以分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。这里面大家最耳熟能详的可能就是 Webkit 内核了,Webkit 内核是当下浏览器世界真正的霸主。 本文我们就以 Webkit 为例,对现代浏览器的渲染过程进行一个深度的剖析。
zhuanlan.zhihu.com/p/371786505
浏览器的渲染过程主要围绕以下两个引擎展开
- 渲染引擎
- js 引擎
当用户与网页进行交互时(如滚动页面、点击链接等),渲染引擎可能需要重新绘制部分或整个页面。这种情况下,渲染引擎会尽量只重新绘制需要更新的部分,以提高性能。如果需要重新计算页面的布局和样式信息,这个过程称为回流(reflow);如果只需要重新绘制部分元素,则称为重绘(repaint)。
JavaScript引擎也是浏览器渲染引擎的一部分。JavaScript引擎负责解析和执行JavaScript代码,这些代码可以修改网页的内容和样式信息。当遇到JavaScript代码时,渲染引擎会调用JavaScript引擎来处理这些代码,并根据需要更新DOM树和CSSOM树。这可能导致渲染树的重新构建和页面的重新绘制。
前端页面的渲染机制
浏览器向服务器请求资源,接收到来自服务器的响应资源后,会对资源进行分析。
首先查看 Response header,根据不同状态码做不同的事(比如上面提到的重定向)。
如果响应资源进行了压缩(比如 gzip),还需要进行解压。
然后,对响应资源做缓存。
接下来,根据响应资源里的 MIME[3] 类型去解析响应内容(比如 HTML、Image各有不同的解析方式)。
浏览器渲染页面的过程可以大致分为以下几个步骤:
- 解析HTML:
浏览器首先获取HTML文件,然后解析该文件构建DOM树(Document Object Model)。DOM树是一个节点树,它代表了文档的结构,使得开发者能够方便地操作和修改文档。HTML解析器会逐字节读取HTML文件,识别出标签和文本内容,并将它们转换成DOM节点。 - 解析CSS:
在解析HTML的同时,浏览器也会加载并解析页面中的CSS样式信息,生成CSSOM(CSS Object Model)。CSSOM与DOM类似,但是它表示的是文档的样式信息。浏览器会结合DOM和CSSOM来渲染页面。 - 合并DOM和CSSOM:
当DOM和CSSOM都构建完毕后,它们会被合并成一个渲染树(Render Tree)。渲染树只包含需要显示在屏幕上的节点和它们的样式信息。例如,head标签内的内容通常不会显示在屏幕上,因此它们不会出现在渲染树中。 - 布局:
接下来,浏览器会进行布局操作,计算出渲染树中每个节点在屏幕上的确切位置和大小。这个过程也称为重排(reflow)。布局是一个从上到下、从左到右的递归过程,每个节点都会被告知要在屏幕上的哪个区域进行绘制。 - 绘制:
最后,浏览器会根据计算好的布局信息,将每个节点转换成屏幕上的实际像素。这个过程称为绘制(painting)或重绘(repaint)。绘制通常是在多个层上进行的,每一层对应着页面上的某个部分。这些层最终会被合并成一个单一的图像显示在屏幕上。 - 合成与渲染:
在现代浏览器中,为了提高渲染性能,通常会使用一种称为合成(Compositing)的技术。合成允许浏览器将页面的某些部分(如具有特定属性如transform、opacity、filter等的元素)提升到单独的层(称为合成层),然后对这些层进行独立的渲染和合成。这样做可以避免不必要的重排和重绘,提高页面的渲染速度。
需要注意的是,浏览器的渲染过程是一个持续的过程,它会根据用户的交互、网络状态的变化等因素进行动态调整。例如,当用户滚动页面或点击按钮时,浏览器可能需要重新计算布局和绘制部分或全部页面。此外,为了提高用户体验,浏览器还会使用各种优化技术,如异步加载、缓存等。
渲染基本流程
1. HTML 解析
主要解析HTML/SVG/XHTML,HTML字符串描述了一个页面的结构,浏览器会把HTML结构字符串解析转换DOM树形结构。
首先浏览器解析是从上往下一行一行地解析的。
解析的过程可以分为四个步骤:
① 解码(encoding)
传输回来的其实都是一些二进制字节数据,浏览器需要根据文件指定编码(例如UTF-8)转换成字符串,也就是HTML 代码。
② 预解析(pre-parsing)
预解析做的事情是提前加载资源,减少处理时间,它会识别一些会请求资源的属性,比如img标签的src属性,并将这个请求加到请求队列中。
③ 符号化(Tokenization)
符号化是词法分析的过程,将输入解析成符号,HTML 符号包括,开始标签、结束标签、属性名和属性值。
它通过一个状态机去识别符号的状态,比如遇到<,>状态都会产生变化。
④ 构建树(tree construction)
注意:符号化和构建树是并行操作的,也就是说只要解析到一个开始标签,就会创建一个 DOM 节点。
在上一步符号化中,解析器获得这些标记,然后以合适的方法创建DOM对象并把这些符号插入到DOM对象中。
浏览器容错进制
你从来没有在浏览器看过类似”语法无效”的错误,这是因为浏览器去纠正错误的语法,然后继续工作。
事件
当整个解析的过程完成以后,浏览器会通过DOMContentLoaded事件来通知DOM解析完成。
2. CSS 解析
一旦浏览器下载了 CSS,CSS 解析器就会处理它遇到的任何 CSS,根据语法规范[4]解析出所有的 CSS 并进行标记化,然后我们得到一个规则表。CSS规则树
CSS 匹配规则
在匹配一个节点对应的 CSS 规则时,是按照从右到左的顺序的,例如:div p { font-size :14px }会先寻找所有的p标签然后判断它的父元素是否为div。
所以我们写 CSS 时,尽量用 id 和 class,千万不要过度层叠。
3. 渲染树
- Rendering Tree 渲染树并不等同于DOM树,渲染树只会包括需要显示的节点和这些节点的样式信息。
- CSS 的 Rule Tree主要是为了完成匹配并把CSS Rule附加上Rendering Tree上的每个Element(也就是每个Frame)。
- 然后,计算每个Frame 的位置,这又叫layout和reflow过程。
其实这就是一个 DOM 树和 CSS 规则树合并的过程。
为了提高渲染性能和效率,浏览器在构建渲染树时会忽略那些不需要渲染的节点比如设置了display:none的节点。
计算
通过计算让任何尺寸值都减少到三个可能之一:auto、百分比、px,比如把rem转化为px。
级联
浏览器需要一种方法来确定哪些样式才真正需要应用到对应元素,所以它使用一个叫做specificity的公式,这个公式会通过:
-
- 标签名、class、id
- 是否内联样式
- !important
然后得出一个权重值,取最高的那个。
渲染阻塞
当遇到一个script标签时,DOM 构建会被暂停,直至脚本完成执行,然后继续构建 DOM 树。
但如果 JS 依赖 CSS 样式,而它还没有被下载和构建时,浏览器就会延迟脚本执行,直至 CSS Rules 被构建。
所有我们知道:
-
- CSS 会阻塞 JS 执行
- JS 会阻塞后面的 DOM 解析
为了避免这种情况,应该以下原则:
-
- CSS 资源排在 JavaScript 资源前面
- JS 放在 HTML 最底部,也就是 前
另外,如果要改变阻塞模式,可以使用 defer 与 async,详见:这篇文章[5]
4. 布局与绘制
确定渲染树种所有节点的几何属性,比如:位置、大小等等,最后输入一个盒子模型,它能精准地捕获到每个元素在屏幕内的准确位置与大小。
然后遍历渲染树,调用渲染器的 paint() 方法在屏幕上显示其内容。
5. 合并渲染层
把以上绘制的所有图片合并,最终输出一张图片。
6. 回流与重绘
回流(reflow)
当浏览器发现某个部分发现变化影响了布局时,需要倒回去重新渲染,会从html标签开始递归往下,重新计算位置和大小。
reflow基本是无法避免的,因为当你滑动一下鼠标、resize 窗口,页面就会产生变化。
重绘(repaint)
改变了某个元素的背景色、文字颜色等等不会影响周围元素的位置变化时,就会发生重绘。
每次重绘后,浏览器还需要合并渲染层并输出到屏幕上。
回流的成本要比重绘高很多,所以我们应该尽量避免产生回流。
比如:
-
- display:none 会触发回流,而 visibility:hidden 只会触发重绘。
7. JavaScript 编译执行
大致流程三个步
- 语法分析
- 预编译
- 执行
可以分为三个阶段:
1. 词法分析
JS 脚本加载完毕后,会首先进入语法分析阶段,它首先会分析代码块的语法是否正确,不正确则抛出“语法错误”,停止执行。
几个步骤:
-
- 分词,例如将var a = 2,,分成var、a、=、2这样的词法单元。
- 解析,将词法单元转换成抽象语法树(AST)。
- 代码生成,将抽象语法树转换成机器指令。
2. 预编译
JS 有三种运行环境:
-
- 全局环境
- 函数环境
- eval
每进入一个不同的运行环境都会创建一个对应的执行上下文,根据不同的上下文环境,形成一个函数调用栈,栈底永远是全局执行上下文,栈顶则永远是当前执行上下文。
创建执行上下文
创建执行上下文的过程中,主要做了以下三件事:
- 创建变量对象
-
- 参数、函数、变量
- 建立作用域链
-
- 确认当前执行环境是否能访问变量
- 确定 This 指向
3. 执行
JS 线程
虽然 JS 是单线程的,但实际上参与工作的线程一共有四个:
其中三个只是协助,只有 JS 引擎线程是真正执行的
- JS 引擎线程:也叫 JS 内核,负责解析执行 JS 脚本程序的主线程,例如 V8 引擎
- 事件触发线程:属于浏览器内核线程,主要用于控制事件,例如鼠标、键盘等,当事件被触发时,就会把事件的处理函数推进事件队列,等待 JS 引擎线程执行
- 定时器触发线程:主要控制setInterval和setTimeout,用来计时,计时完毕后,则把定时器的处理函数推进事件队列中,等待 JS 引擎线程。
- HTTP 异步请求线程:通过XMLHttpRequest连接后,通过浏览器新开的一个线程,监控readyState状态变更时,如果设置了该状态的回调函数,则将该状态的处理函数推进事件队列中,等待JS引擎线程执行。
注:浏览器对同一域名的并发连接数是有限的,通常为 6 个。
宏任务
分为:
-
- 同步任务:按照顺序执行,只有前一个任务完成后,才能执行后一个任务
- 异步任务:不直接执行,只有满足触发条件时,相关的线程将该异步任务推进任务队列中,等待JS引擎主线程上的任务执行完毕时才开始执行,例如异步Ajax、DOM事件,setTimeout等。
微任务
微任务是ES6和Node环境下的,主要 API 有:Promise,process.nextTick。
微任务的执行在宏任务的同步任务之后,在异步任务之前。
常见问题
1.async和defer的作用是什么?有什么区别?
接下来我们对比下 defer 和 async 属性的区别:
其中蓝色线代表JavaScript加载;红色线代表JavaScript执行;绿色线代表 HTML 解析。
1)情况1
没有 defer 或 async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。
2)情况2 (异步下载)
async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于, 如果已经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。
3)情况3 (延迟执行)
defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。
defer 与相比普通 script,有两点区别:载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后。 在加载多个JS脚本的时候,async是无顺序的加载,而defer是有顺序的加载。
2.为什么操作 DOM 慢
把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁连接。——《高性能 JavaScript》
JS 是很快的,在 JS 中修改 DOM 对象也是很快的。在JS的世界里,一切是简单的、迅速的。但 DOM 操作并非 JS 一个人的独舞,而是两个模块之间的协作。
因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们用 JS 去操作 DOM 时,本质上是 JS 引擎和渲染引擎之间进行了“跨界交流”。这个“跨界交流”的实现并不简单,它依赖了桥接接口作为“桥梁”(如下图)。
过“桥”要收费——这个开销本身就是不可忽略的。我们每操作一次 DOM(不管是为了修改还是仅仅为了访问其值),都要过一次“桥”。过“桥”的次数一多,就会产生比较明显的性能问题。因此“减少 DOM 操作”的建议,并非空穴来风。
3.你真的了解回流和重绘吗
渲染的流程基本上是这样(如下图黄色的四个步骤):1.计算CSS样式 2.构建Render Tree 3.Layout – 定位坐标和大小 4.正式开画
注意:上图流程中有很多连接线,这表示了Javascript动态修改了DOM属性或是CSS属性会导致重新Layout,但有些改变不会重新Layout,就是上图中那些指到天上的箭头,比如修改后的CSS rule没有被匹配到元素。
这里重要要说两个概念,一个是Reflow,另一个是Repaint
- 重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。
- 回流:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)
我们知道,当网页生成的时候,至少会渲染一次。在用户访问的过程中,还会不断重新渲染。重新渲染会重复回流+重绘或者只有重绘。 回流必定会发生重绘,重绘不一定会引发回流。重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。
1)常见引起回流属性和方法
任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流,
- 添加或者删除可见的DOM元素;
- 元素尺寸改变——边距、填充、边框、宽度和高度
- 内容变化,比如用户在input框中输入文字
- 浏览器窗口尺寸改变——resize事件发生时
- 计算 offsetWidth 和 offsetHeight 属性
- 设置 style 属性的值
2)常见引起重绘属性和方法
3)如何减少回流、重绘
- 使用 transform 替代 top
- 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
- 不要把节点的属性值放在一个循环里当成循环里的变量。
for(let i = 0; i < 1000; i++) { // 获取 offsetTop 会导致回流,因为需要去获取正确的值 console.log(document.querySelector('.test').style.offsetTop) } 复制代码
- 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
- 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
- CSS 选择符从右往左匹配查找,避免节点层级过多
- 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。
4、浏览器中的渲染引擎和 js 引擎会相互阻塞吗
浏览器中的渲染引擎和JavaScript引擎确实会相互阻塞。这是因为JavaScript可以修改页面的内容、结构和样式,所以在JavaScript代码执行期间,浏览器需要确保页面的状态是一致的。如果JavaScript在修改页面的同时,渲染引擎也在尝试渲染页面,就可能会导致页面状态的不一致,从而产生不可预期的结果。
因此,当浏览器遇到需要执行的JavaScript代码时,它会暂停渲染进程的执行,等待JavaScript引擎执行完毕后再继续渲染。这就是为什么如果JavaScript代码执行时间过长,或者存在大量的同步网络请求等操作,会导致页面渲染被阻塞,用户会感觉到页面卡顿或者失去响应。
为了优化用户体验,开发者需要尽量减少JavaScript代码的执行时间,避免阻塞渲染进程。一些常用的优化手段包括将JavaScript代码放在页面的底部或者使用异步加载的方式加载JavaScript代码等。此外,现代浏览器也提供了一些机制来优化JavaScript的执行性能,如Web Workers等技术可以在不阻塞主线程的情况下执行JavaScript代码。
总之,浏览器中的渲染引擎和JavaScript引擎是相互依赖的,但它们之间也存在一定的阻塞关系。开发者需要了解这种关系,并采取合适的措施来优化页面的性能。
5、假如 js 只是处理数据不会修改页面,那渲染引擎和 js 引擎可以同时进行工作吗
即使 JavaScript 只是处理数据而不直接修改页面,渲染引擎和 JavaScript 引擎通常仍然不能严格地“同时进行”工作,因为它们运行在同一个主线程中。JavaScript 的执行和页面的渲染都是在这个主线程上同步进行的。
然而,现代浏览器和 JavaScript 引擎采用了一些优化策略来尽量减少这种阻塞的影响,并允许一定程度的并行处理:
- 异步编程:JavaScript 提供了异步编程的能力,如 Promises、async/await 等,这使得开发者可以编写不会长时间阻塞主线程的代码。当 JavaScript 在等待异步操作(如网络请求)完成时,渲染引擎可以利用这段时间来进行页面的渲染。
- Web Workers:Web Workers 允许在浏览器后台运行 JavaScript,而不会干扰页面的性能。通过使用 Web Workers,开发者可以将计算密集型或长时间运行的任务移到后台线程,从而避免阻塞主线程和渲染引擎。
- 请求动画帧(requestAnimationFrame) :这是一个浏览器提供的 API,允许开发者在下一次重绘之前执行 JavaScript 代码。这有助于确保动画的平滑性,并允许 JavaScript 代码与渲染引擎更好地协同工作。
- 渲染优化:浏览器会尝试优化渲染过程,例如通过合并多次重绘或重排操作来减少不必要的渲染工作。此外,浏览器还可能会将某些渲染任务推迟到 JavaScript 执行完成后进行。
尽管有这些优化策略,但在某些情况下,JavaScript 的执行仍然可能会阻塞渲染引擎。例如,如果 JavaScript 代码执行时间过长(即使它不直接修改页面),它仍然会占用主线程的执行时间,从而间接地影响渲染性能。因此,最佳实践是尽量减少 JavaScript 对主线程的占用时间,并通过使用异步编程和 Web Workers 等技术来优化性能。
6、requestIdleCallback、Web Workers和requestAnimationFrame有什么区别 他们通常在什么情况下使用
requestIdleCallback、Web Workers和requestAnimationFrame都是浏览器提供的API,它们各自有不同的用途和使用场景。下面是它们之间的区别和常见使用情况:
- requestIdleCallback
-
- 用途:当系统处于闲置状态时,允许开发者在主线程上调度后台或低优先级的任务。
- 工作原理:浏览器会在空闲时段内调用传入的回调函数,这些时段通常是在主线程完成重要任务(如用户输入、动画、布局和渲染)之后。
- 使用场景:适合执行非紧急、非阻塞性的任务,例如数据分析、日志记录、预加载资源等。
- Web Workers
-
- 用途:在浏览器后台线程中运行JavaScript,与主线程并行执行,不会阻塞用户界面。
- 工作原理:Web Workers 在独立的全局上下文中运行,与主线程完全分离,它们通过消息传递进行通信。
- 使用场景:适用于需要长时间运行、计算密集型或可能阻塞主线程的任务,如复杂的数学运算、数据处理、文件读取等。
- requestAnimationFrame
-
- 用途:在下一次重绘之前调用指定的函数,通常用于创建平滑的动画和游戏循环。
- 工作原理:浏览器会在下一次绘制前调用传入的函数,确保动画或视觉更新与设备的刷新率同步。
- 使用场景:适用于任何需要精确时间控制和平滑动画的场景,如页面滚动、CSS变换、canvas绘图等。
总结来说:
- requestIdleCallback 用于执行不紧急的任务,在系统空闲时运行。
- Web Workers 用于执行计算密集型任务,与主线程并行运行,避免阻塞用户界面。
- requestAnimationFrame 用于创建与屏幕刷新率同步的动画和游戏循环。
这些API都允许开发者更精细地控制浏览器中的任务调度和执行,从而提升用户体验和页面性能。在实际开发中,应根据具体需求选择合适的API来优化应用程序。
其他面试题
1、基于浏览器渲染机制请给出性能优化策略
基于上面介绍的浏览器渲染原理,DOM 和 CSSOM 结构构建顺序,初始化可以对页面渲染做些优化,提升页面性能。
- JS优化:
-
- defer属性: 用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。
- async属性: HTML5新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。
- CSS优化: 标签的 rel属性 中的属性值设置为 preload 能够让你在你的HTML页面中可以指明哪些资源是在页面加载完成后即刻需要的,最优的配置加载顺序,提高渲染性能
2、目前有个 task 函数 需要执行一万次 如何实现执行过程中不影响页面渲染和用户操作
在前端项目中,如果你需要执行一个函数(比如叫 task)一万次,而且不希望这个过程影响到页面的渲染和用户操作,你可以考虑以下几种方法来实现:
1. 使用 Web Workers
Web Workers 允许你在浏览器的后台线程中运行 JavaScript,这样就不会阻塞主线程,从而不会影响到页面的渲染和用户操作。你可以创建一个 Web Worker,然后在其中执行你的 task 函数。
javascript复制代码
// worker.js
self.onmessage = function(event) {
const { count } = event.data;
for (let i = 0; i < count; i++) {
// 执行你的 task 函数
task();
}
self.postMessage('Done');
};
在主线程中:
javascript复制代码
const worker = new Worker('worker.js');
worker.onmessage = function(event) {
console.log('Tasks completed:', event.data);
};
worker.postMessage({ count: 10000 });
2. 使用 requestIdleCallback
requestIdleCallback 是一个允许你在主线程空闲时执行低优先级任务的 API。这个方法适合执行一些不那么紧急的任务,而且它会尽量避免影响到页面的性能。
javascript复制代码
let count = 0;
const maxCount = 10000;
function performTask() {
if (count < maxCount) {
task();
count++;
window.requestIdleCallback(performTask);
} else {
console.log('All tasks completed');
}
}
window.requestIdleCallback(performTask);
3. 分批处理
你还可以考虑将一万次任务分成多个小批次,然后使用 setTimeout 或 requestAnimationFrame 来调度每个批次的执行。这样也可以避免长时间占用主线程。
const batchSize = 100; // 例如每个批次执行100次任务
let count = 0;
const maxCount = 10000;
function performBatch() {
const end = Math.min(count + batchSize, maxCount);
for (; count < end; count++) {
task();
}
if (count < maxCount) {
setTimeout(performBatch, 0); // 或者使用 requestAnimationFrame
} else {
console.log('All tasks completed');
}
}
performBatch();
注意事项:
- 确保你的 task 函数是纯函数,或者至少没有副作用会影响到页面的状态或用户交互。
- 监控任务的执行时间和内存使用情况,以确保不会引发性能问题或内存泄漏。
- 如果可能的话,考虑优化你的 task 函数,以减少其执行时间和资源消耗。
3、页面中点击按钮后整个页面无法滚动和点击,请问有哪些可能导致的,如果排除内存卡顿还有哪些可能?
当页面中的按钮点击后导致整个页面无法滚动和点击,可能有多种原因。除了内存卡顿或性能问题外,以下是一些可能导致这种情况的其他常见原因:
- 阻塞操作:按钮的点击事件可能触发了一个长时间运行的同步操作,例如一个复杂的计算或者一个未优化的循环,这会阻塞主线程,导致页面无法响应用户的其他交互。
- 模态窗口或遮罩层:按钮点击可能触发了一个模态窗口(modal)或遮罩层的显示,这些元素通常会覆盖在页面上方,并且阻止对底层内容的滚动和点击。检查是否有任何此类元素被添加到页面中。
- CSS 样式问题:某些CSS样式,如pointer-events: none;或者overflow: hidden;等,如果被错误地应用到了页面或父级元素上,可能会阻止滚动和点击。
- JavaScript 错误:页面中的JavaScript代码可能存在错误,导致事件处理程序没有按预期工作,或者阻止了其他事件监听器的执行。
- 事件冒泡与默认行为阻止:在事件处理程序中,如果不正确地使用了event.stopPropagation()或event.preventDefault(),可能会阻止滚动或点击事件的正常传播或默认行为。
- 第三方库或框架的问题:如果页面中使用了第三方库或框架(如jQuery、React、Vue等),它们可能存在bug或者与其他代码不兼容,导致滚动和点击被阻止。
- 浏览器兼容性问题:不同的浏览器可能对某些CSS属性或JavaScript特性的支持不同,这可能导致在某些浏览器中页面无法滚动或点击。
- 页面布局问题:复杂的页面布局或者使用了某些特殊的CSS技巧(如fixed或sticky定位)可能导致滚动或点击被意外地阻止。
要诊断这个问题,你可以:
- 检查浏览器的控制台(Console)以查看是否有JavaScript错误。
- 使用浏览器的开发者工具(DevTools)来检查是否有模态窗口、遮罩层或其他覆盖在页面上的元素。
- 检查点击事件处理程序和任何可能阻止事件传播或默认行为的代码。
- 尝试在不同的浏览器和设备上重现问题,以排除浏览器兼容性问题。
- 逐步排除或禁用页面上的第三方库和自定义代码,以确定问题的来源。
4、js 中执行多个 promise 时 页面可以同时开始渲染或者用户交互吗
在JavaScript中,当执行多个Promise时,页面的渲染和用户交互不会立即被阻塞,因为Promise是异步的。这意味着当Promise开始执行时,它们会将控制权返回给事件循环,允许浏览器继续处理其他任务,如渲染和用户交互。
然而,需要注意的是,如果Promise的执行涉及到大量的计算或长时间运行的任务,它们可能会占用主线程的时间,导致页面渲染和用户交互变得不流畅或响应缓慢。这是因为JavaScript是单线程的,并且浏览器使用同一个线程来处理JavaScript代码、页面渲染和用户交互。
为了避免这种情况,可以采取以下策略:
- 分批处理:将长时间运行的任务分解成较小的批次,并使用setTimeout、requestIdleCallback或requestAnimationFrame等方法在浏览器空闲时调度这些批次。这样可以确保主线程有时间处理其他任务。
- Web Workers:如前所述,使用Web Workers可以将计算密集型任务移到后台线程中执行,从而避免阻塞主线程。Web Workers允许在浏览器后台的独立线程中运行JavaScript代码,不会干扰页面的渲染和用户交互。
- 优化代码:确保Promise内部的代码尽可能高效,避免不必要的计算和内存消耗。使用合适的数据结构和算法,以及避免长时间运行的循环。
- 异步I/O:如果Promise涉及到网络请求或其他异步I/O操作,确保这些操作是真正的异步的,并且不会长时间占用主线程。
- 监控性能:使用浏览器的开发者工具来监控页面的性能,识别并解决性能瓶颈。
通过合理地管理异步任务和优化代码,可以确保在执行多个Promise时,页面的渲染和用户交互能够保持流畅和响应。