Insidelookatmodernwebbrowser是介绍浏览器实现原理的系列文章。共有4篇文章。本次精读介绍第四篇。概述前几章介绍了浏览器的基本进程、线程以及它们之间的协作关系,重点介绍了渲染进程是如何处理页面绘制的,所以最后一章也深入探讨了浏览器是如何处理页面中的事件的。整篇文章都站在浏览器实现的角度来思考问题,很有意思。输入合成器这是第一个小节的标题。乍一看,你可能不明白你在说什么,但这句话是本文的核心知识点。为了更好的理解这句话,先解释一下input和synthesizer是什么:input:不仅仅包括输入框的输入,实际上所有的用户操作在浏览器眼里都是输入,比如滚动,点击,鼠标运动等等。Compositor:在第三节中提到,渲染的最后一步是在GPU上进行光栅化。如果与浏览器主线程解耦,效率会很高。所以输入到合成器中意味着在浏览器实际运行的环境中,合成器要响应输入,这可能会导致合成器本身被渲染阻塞,导致页面卡顿。“非快速”滚动区域由于js代码可以绑定事件监听器,事件监听器中有一个preventDefault()API可以防止滚动等事件的原生效果,所以在一个页面中,浏览器会把所有创建的此侦听块被标记为“非快速”滚动区域。注意只要创建了onwheel事件监听,就会被标记,而不是调用preventDefault()的时候,因为浏览器不可能知道业务什么时候调用的,所以只能一刀切。为什么这个区域被称为“非快速”?因为当这个区域有事件触发时,合成器必须和渲染进程通信,让渲染进程执行js事件监听代码,获取用户指令,比如是否调用preventDefault()来防止滚动?如果被阻塞,则滚动终止,如果没有被阻塞,则继续滚动。如果最终结果没有被阻塞,但是这个等待时间消耗是巨大的。在手机等低性能设备上,滚动延迟甚至有10-100ms。不过这并不是设备性能不好造成的,因为滚动发生在合成器中,如果不能和渲染进程通信,那么即使是500元的安卓机也能流畅滚动。更有趣的是,事件委托是浏览器支持一个事件委托API,它可以将事件委托给它们的父节点并监听它们。这是一个非常方便的API,但对于浏览器实现来说可能是一场灾难:document.body.addEventListener('touchstart',event=>{if(event.target===area){event.preventDefault();}});如果浏览器解析上面的代码,只能用无语来形容。因为这意味着必须将整个页面标记为“非快速”,因为代码委托给了整个文档!这会导致滚动非常慢,因为每次滚动页面时都会发生合成器-渲染器进程通信。所以最好的办法就是不要写这种监控。但是另一种解决方案是告诉浏览器你不会preventDefault(),这是因为chrome在对应用程序源码进行统计后发现大约80%的事件监听器没有preventDefault(),而只是做其他事情,所以synthetic处理程序应该能够与渲染过程的事件处理并行运行,这样既不会卡顿,也不会丢失逻辑。所以添加了一个passive:true标志,表示当前事件可以并行处理:document.body.addEventListener('touchstart',event=>{if(event.target===area){event.preventDefault()}},{被动:真});这样虽然不会卡死,但是preventDefault()也会失效。检查事件是否可以取消。在passive:true的情况下,事件实际上变成了不可撤销的,所以我们最好在代码中进行判断:document.body.addEventListener('touchstart',event=>{if(event.cancelable&&event.target===area){event.preventDefault()}},{passive:true});但是,这只是阻止了无意义的preventDefault()的执行,并没有阻止滚动。在这种情况下,最好的办法就是通过css声明来防止水平移动,因为这个判断不会发生在渲染过程中,所以不会导致合成器和渲染进程通信:#area{touch-action:pan-x;}事件合并因为事件触发频率可能高于浏览器帧率(每秒120次),如果浏览器硬要对每个事件进行响应,而在js中每个事件必须响应一次,会造成很大的影响阻塞的事件个数,因为当FPS为60时,一秒钟只能进行60个事件响应,所以事件积压是不可避免的。为了解决这个问题,浏览器在针对滚动事件等可能造成积压的事件时,将多个事件合并到一个js中,只保留最终状态。如果不想丢失事件的中间过程,可以使用getCoalescedEvents从合并事件中获取事件每一步的状态:window.addEventListener('pointermove',event=>{constevents=event.getCoalescedEvents();for(leteventofevents){constx=event.pageX;consty=event.pageY;//使用x和y坐标画一条线。}});只要我们意识到事件监听器必须在渲染过程中运行,而现代浏览器很多性能“渲染”实际上是由GPU在合成层完成的,所以看似方便的事件监听肯定会拖慢页面的流畅度。但是在React17Touch/WheelEventPassivenessinReact17中有讨论过这个问题(其实这个问题在即将到来的18React18notpassivewheel/toucheventlistenerssupport中还在讨论中),因为React可以MonitorTouch而Wheel事件直接在元素上,但实际上框架是采用委托的方式在文档中(后面在app的根节点)统一监听,这样用户就无法判断事件是否是被动的。如果框架默认为passive,会导致preventDefault()无效,否则性能不会优化。就结论而言,React对几个受影响的事件touchstarttouchmovewheel还是采用被动方式,即:constTest=()=>(
