现代浏览器必须运行复杂的应用程序,同时保持对用户操作的响应。这样做意味着选择要优先处理的众多任务中的哪些任务,以及将哪些任务推迟到以后的任务,如JavaScriptcallbacks、用户输入和呈现。此外,浏览器工作必须跨多个线程进行,不同的线程并行运行事件,以最大限度地提高响应能力。
到目前为止,我们的浏览器所做的大部分工作都来自用户的操作,比如滚动、按下按钮和点击链接。但随着浏览器运行的web应用程序越来越复杂,它们开始查询远程服务器,显示动画,并为以后预取信息。虽然用户速度慢且考虑周全,在操作之间留下了很长的差距,以便浏览器跟上,但应用程序可能要求很高。这就需要在视角上做出改变:浏览器现在有一个永无止境的任务队列。
现代浏览器通过多任务、优先排序和重复数据消除工作来适应这种现实。浏览器加载页面、运行脚本和响应用户操作所做的每一点工作都会转化为一个任务,可以在以后执行,其中一个任务只是一个可以执行的函数(加上它的参数):通过将*args作为参数写入任务,我们表明一个任务可以用任意数量的参数构造,然后可以在列表args中使用。然后,使用*args调用函数将列表解压回多个元素。
类任务:def__init_________;(self,任务代码,*args):self。任务代码=任务代码自身。args=args self__姓名_=";任务34;def run(self):self。任务代码(*self.args)self。任务代码=无自身。args=None
任务的意义在于,根据调度算法,它可以在某个时间点创建,然后由某种类型的任务运行程序在稍后的某个时间运行。我们在第2章和第11章讨论的事件循环是任务运行程序,其中要运行的任务由操作系统提供。在我们的浏览器中,任务运行程序将任务存储在先进先出队列中:
当运行一个任务的时候,我们的任务运行器可以从队列中移除第一个任务并运行它:先入先出是一种简单的方式来选择接下来要运行的任务,而真正的浏览器有许多考虑不同因素的调度程序。
要运行这些任务,我们需要在我们的TaskRunner上调用run方法,我们可以在主事件循环中这样做:
类选项卡:def_____;init(self):self。task_runner=TaskRunner()如果_name_=="__主要内容";:尽管如此:#。。。浏览器选项卡[浏览器.活动选项卡]。任务跑者。跑()
在这里,我选择只在活动选项卡上运行任务,这意味着后台选项卡不能降低浏览器的速度。
有了这个简单的任务运行程序,我们现在可以将任务排队并在以后执行。例如,现在,当加载网页时,我们的浏览器将下载并运行所有脚本,然后再执行呈现步骤。这使得页面加载速度变慢。我们可以通过为运行脚本创建任务来解决这个问题:
class选项卡:def run_script(self,url,body):try:print(";脚本返回:";,self.js.run(body))dukpy除外。JSRuntimeError as e:print(";Script";,url,";crash";,e)def load(self):对于脚本中的脚本:#。。。标题,正文=请求(脚本url,url)任务=任务(self.run_脚本,脚本url,正文)self。任务跑者。计划任务(任务)
现在,我们的浏览器将不会运行脚本,直到加载完成,事件循环再次出现。
JavaScript也是围绕基于任务的eventloop构建的,即使它没有嵌入浏览器中。它允许将消息传递给事件循环,使用run to completion语义,一般来说,它使用大量异步回调和事件。JavaScript的编程模型是以同样的方式构建浏览器的另一个重要原因。
任务也是支持多个JavaScriptAPI的自然方式,这些API要求函数在将来的某个时间运行。例如,setTimeoutlets可以在几毫秒后运行JavaScript函数。这段代码在一秒钟后将“回调”打印到控制台:
我们可以在Python的threadingmodule中使用Timerclass实现setTimeout。您可以这样使用该类:另一种方法是记录每个任务应该发生的时间,并与事件循环中的currenttime进行比较。这称为轮询,例如,SDL事件循环用于查找事件和任务。然而,这可能意味着在循环中浪费CPU周期,直到任务完成,所以我希望计时器更高效。
这将在一秒钟后的新蟒蛇上运行回调。易于理解的但使用定时器来实现setTimeout会有点棘手,因为会涉及多个线程。
与第9章中的addEventListener一样,对setTimeout的调用将回调保存在JavaScript变量中,并创建一个句柄,Python端代码可以通过该句柄调用它:
SET_TIMEOUT_REQUESTS={}函数setTimeout(callback,time_delta){var handle=Object.keys(SET_TIMEOUT_REQUESTS).length;SET_TIMEOUT_REQUESTS[handle]=callback;call_python(";setTimeout";,handle,time#delta)}
导出的setTimeout函数将创建一个计时器,等待请求的时间段,然后要求JavaScript运行时运行回调。最后一部分将通过_runSetTimeout实现:请注意,我们从未从SET_TIMEOUT_请求字典中删除回调。如果回调函数保留对某个大型数据结构的最后一个引用,这可能会导致amemory泄漏。我们在第九章看到了类似的问题。一般来说,在浏览器和浏览器应用程序之间共享数据结构时,避免内存泄漏需要非常小心。
然而,Python方面要复杂得多,因为线程。计时器在一个新的Pythonthread上执行其回调。该线程不能直接调用evaljs:我们将在同一时间在两个Python线程上运行JavaScript,这是不好的。JavaScript不是一种多线程编程语言。在网络上创建各种各样的工作人员是可能的,但它们都是独立运行的,只通过特殊的消息传递API进行通信。相反,计时器只需向任务队列中添加一个新任务,我们的主线程将在稍后执行:这段代码有一个非常微妙的错误,其中一个页面可能会创建一个setTimeout,然后在用户访问另一个网页时触发该计时器。在我们的浏览器中,这将允许一个页面运行JavaScript来修改另一个页面,这将导致巨大的安全漏洞!我认为你可以通过自我保护来避免这种情况。js。当你导航到一个新页面时,你应该做一些更仔细的事情,比如跟踪JSContext产生的所有子线程,并在导航之前结束所有子线程。随着我们的浏览器变得越来越复杂,我们的bug和相关修复也变得越来越复杂!
SETTIMEOUT_CODE="__runSetTimeout(dukpy.handle)和#34;类JSContext:def _uinit__________;(self,tab):#。。。自己interp。export_函数(";setTimeout";self.setTimeout)def dispatch#u setTimeout(self,handle):self。interp。evaljs(SETTIMEOUT_CODE,handle=handle)def SETTIMEOUT(self,handle,time):def run_callback():task=task(self.dispatch_SETTIMEOUT,handle)self。标签。任务跑者。计划_任务(任务)线程。计时器(time/1000.0,运行_回调)。开始()
这样,调用evaljs的最终是主线程。这很好,但现在我们有两个线程访问task_runner:运行任务的主线程和添加任务的时间线程。这是一个竞争条件,可能会导致各种不好的事情发生,所以我们需要确保一次只有一个线程访问task_runner。
为此,我们使用一个锁对象,它一次只能由一个线程持有。每个线程将在读取或写入任务运行程序之前尝试获取锁,以避免同时访问:获取的阻塞参数指示线程在继续之前是否应等待锁可用;在本章中,你将始终将其设置为真。(当线程正在等待时,据说它被阻塞了。)
类任务运行程序:def _________________。。。自己锁=穿线。Lock()def schedule_task(self,task):self。锁获得(阻碍=真正的)自我。任务。添加任务(task)self。锁release()定义运行(self):self。锁如果len(self.tasks)>;0:task=self。任务。pop(0)self。锁如果任务:task,则释放()。跑()
在使用锁时,记住最终释放锁并尽可能短地保持它是非常重要的。例如,上面的代码在运行任务之前释放锁。这是因为任务从队列中移除后,其他线程无法访问它,因此在任务运行时不需要持有锁。
不幸的是,Python目前有一个globalinterpreter锁(GIL),因此Python线程不能真正并行运行。这是Python的一个不幸的局限性,它不会影响真正的浏览器,所以在本章中,试着假装它不存在。尽管有全局解释器锁,我们仍然需要锁。每个Python线程都可以在字节码操作之间进行转换,因此您仍然可以获得对共享变量的并发访问,竞争条件仍然是可能的。事实上,在调试本章的代码时,当我忘记添加锁时,我经常遇到这种竞争条件;尝试从浏览器中移除一些锁,以便自己查看!
线程还可以用于添加浏览器多任务处理。例如,在第10章中,我们实现了XMLHttpRequest类,它允许脚本向服务器发出请求。但在我们的实现中,整个浏览器会在等待请求完成时阻塞。那太糟糕了。因此,我们在第10章中实现的API的同步版本不是很有用,而且性能也很高。一些浏览器现在开始反对同步XMLHttpRequest。
创建一个运行回调函数的新线程。重要的是,此代码立即返回,回调与任何其他代码并行运行。我们将使用线程实现异步XMLHttpRequest调用。具体来说,我们将让浏览器启动一个线程,对该线程执行请求并解析响应,然后安排一个任务将响应发送回脚本。
与setTimeout一样,我们将回调存储在JavaScript端,并用句柄引用它:
当脚本在XMLHttpRequest对象上调用open方法时,我们现在允许is_async标志为true:在浏览器中,is_async的默认值为true,下面的代码没有实现。
XMLHttpRequest。原型open=function(method,url,is_async){this.is_async=is_async this.method=method;this.url=url;}
在浏览器端,XMLHttpRequest_send Handler将由三部分组成。第一部分将解析URL和dosecurity检查:
类JSContext:def XMLHttpRequest_send(self、方法、url、body、isasync、handle):如果不是self,则full_url=resolve_url(url、self.tab.url)。标签。允许的请求(完整url):引发异常(";跨源XHR被CSP阻止";)如果url_来源(完整url)!=url_origin(self.tab.url):引发异常(";不允许跨源XHR请求";)
然后,我们将定义一个函数,使请求和队列成为用于运行回调的任务:
类JSContext:def XMLHttpRequest_send(self、方法、url、body、isasync、handle):#。。。def run_load():标题,响应=请求(完整url,self.tab.url,有效负载=正文)任务=任务(self.dispatch_xhr_onload,响应,句柄)self。标签。任务跑者。计划任务(任务)如果不是isasync:返回响应
最后,根据is_async标志,浏览器将立即调用此函数,或在新线程中调用:
类JSContext:def XMLHttpRequest_send(self、方法、url、body、isasync、handle):#。。。如果不是isasync:返回run_load(),否则:线程。线程(目标=运行\加载)。开始()
注意,在异步情况下,XMLHttpRequest_sendmethod启动一个线程,然后立即返回。该线程将与浏览器的主要工作并行运行,直到请求结束。
XHR_ONLOAD_CODE="__runXHROnload(dukpy.out,dukpy.handle)和#34;类JSContext:def dispatch_xhr_onload(self、out、handle):do_default=self。interp。evaljs(XHR_ONLOAD_代码,out=out,handle=handle)
函数#u runXHROnload(body,handle){var obj=XHR_REQUESTS[handle];var evt=new Event(';load';);obj。responseText=身体;如果(obj.onload)obj。加载(evt);}
如您所见,任务不仅允许浏览器,还允许浏览器中运行的应用程序将任务延迟到以后。
XMLHttpRequest在帮助Web发展方面发挥了关键作用。在90年代,点击一个链接或提交一个表格需要加载一个新页面。有了XMLHttpRequest,网页的行为就更像一个动态应用程序;GMail是早期一个著名的例子。GMail可以追溯到2004年4月,在足够多的浏览器完成添加对API的支持后不久。1999年,第一个使用XMLHttpRequest的应用程序是Outlook WebAccess,但API花了一段时间才进入其他浏览器。如今,一个使用DOMS而不是页面加载来更新其状态的web应用程序被称为单页面应用程序。单页应用程序实现了更具交互性和复杂性的web应用程序,这反过来又使浏览器的速度和响应能力变得更加重要。
任务不仅仅是实现一些JavaScript API。一旦某件事成为一项任务,任务运行者就可以控制它运行的时间:可能是现在,可能是以后,或者最多每秒一次,或者甚至是以不同的速率运行活动页面和非活动页面,或者根据其优先级。浏览器甚至可以有多个任务运行程序,针对不同的用例进行优化。
现在,浏览器可能很难确定运行哪个JavaScript回调的优先级,或者为什么它可能希望以固定的节奏执行JavaScripttasks。但是除了JavaScript之外,浏览器还必须读取页面,正如您在第2章中所回忆的,我们希望浏览器能够以显示硬件刷新的速度读取页面。在大多数计算机上,这是每秒60次,或每帧16毫秒。
让我们建立16ms我们的理想刷新率:16毫秒不是那么精确,因为它是16.66666…ms的60倍,大约等于1秒。但它是一个玩具浏览器!
现在,这里有一些复杂性,因为我们有多个标签。我们不需要每16毫秒重新绘制一个标签,因为用户一次只能看到一个标签。我们只需要重新绘制活动选项卡本身。因此,应该由浏览器来控制何时更新显示,而不是单个选项卡。
让我们实现这一点。首先,让我们编写一个schedule_animation_frame方法,它被称为“animationframe”,因为不同像素的顺序渲染是一个动画,每次渲染它都是一个“帧”——就像pictureframe中的一幅画。在计划渲染任务以运行渲染管道一半选项卡的浏览器上:
类浏览器:def schedule_animation_frame(self):def callback():active_tab=self。tabs[self.active_tab]task=task(active_tab.render)active_tab。任务跑者。计划_任务(任务)线程。计时器(刷新速率秒,回调)。开始()
请注意,每次安排一帧时,我们都会设置一个计时器来安排下一帧。我们可以在启动浏览器时启动该过程:
接下来,让我们将浏览器执行的光栅和绘图任务放入它们自己的方法中:
在顶层循环中,在活动选项卡上运行任务后,浏览器将需要光栅和绘制,如果该任务是渲染任务:
现在我们正计划每16毫秒执行一次新的渲染任务,正如我们所希望的那样。
每秒60帧没有什么特别的。有些显示器每秒刷新72次,刷新频率更高的显示器越来越常见。电影通常以每秒24帧的速度拍摄(尽管一些导演提倡48帧),而电视节目通常以每秒30帧的速度拍摄。一致性通常比实际帧速率更重要:一致的每秒24帧比在60到24帧之间变化的帧速率看起来更平滑。
如果你在你的电脑上运行这个程序,你的CPU很有可能会出现峰值,电池也会开始耗尽。这是因为我们正在调用render every frame,这意味着我们的浏览器现在不断地为元素设计样式、构建布局树和绘制显示列表。大部分工作都被浪费了,因为在大多数框架上,网页根本不会改变,所以旧的样式、布局树和显示列表会和新的一样工作。
让我们用一个脏点来解决这个问题,这个状态告诉我们一些复杂的数据结构是否是最新的。既然我们想知道是否需要运行render,我们就把脏的部分称为needs_render:
类选项卡:def _uinit________________。。。自己needs_render=False def set_needs_render(self):self。需要_render=True def render(self):如果不是self。需要渲染:返回#。。。自己需要_render=False
这个标志的一个优点是,我们现在可以在HTML发生更改时设置Requireds_render,而不是直接调用render。渲染仍将进行,但将在稍后进行。这使得脚本速度更快,尤其是当脚本多次修改页面时。在innerHTML_集合、加载、单击和按键中进行此更改。例如,在load中,执行以下操作:
我们的实现中的另一个问题是,浏览器现在在每次活动选项卡运行atask时都会进行光栅_和_绘制。但有时,该任务只是运行JavaScript,而JavaScript不会触及网页,raster_和_draw调用是awaste。
我们可以使用另一个脏位来避免这种情况,我将其称为“需要”“光栅”“和”“绘制脏位”:需要”“光栅”“和”“绘制脏位并不会让浏览器更高效。在本章的后面,我们将添加多个浏览器线程,在这一点上,这个脏的部分是必要的,以避免动画时的不稳定行为。试着把它移到水里,自己看看!
类浏览器:def____;init(self):self。needs_raster_and_draw=False def set_needs_raster_and_draw(self):self。需要_光栅_和_绘图=真实的定义光栅_和_绘图(self):如果不是self。需要_光栅_和_绘制:返回#。。。自己需要光栅和画图=假
每次浏览器更改浏览器浏览器的某些内容,或者选项卡更改其渲染时,我们都需要调用set_needs_raster_和_draw。浏览器chrome由事件处理程序更改:
类浏览器:def handle_click(self,e):如果e.y<;CHROME_PX:#。。。自己set_needs_raster_and_draw()def handle_key(self,char):如果self。焦点==";地址栏";:#。。。自己set_needs_raster_and_draw()def handle_enter(self):如果self。焦点==";地址栏";:#。。。自己设置需要光栅和绘图()
类选项卡:def _uinit________________。。。自己browser=浏览器def render(self):#。。。自己浏览器设置需要光栅和绘图()
现在,渲染管道仅在必要时运行,浏览器应该再次具有可接受的性能。
直到21世纪的第二个十年,所有的现代研究者都完成了采用一种有计划的、基于任务的研究方法。随着复杂的交互式web应用程序的出现,这种需求变得明显,但安全性仍然需要多年的努力
......