第十章 正则表达式
用到再看
第十一章 代码模块化
AMD
AMD 的设计明确基于浏览器,最流行的实现是 RequireJS,有以下几项优点:
- 自动处理依赖,无需考虑模块引入的顺序
- 异步加载模块,避免阻塞
- 在同一个文件中可以定义多个模块
CommonJS
CommonJS 的设计是面向通用 JavaScript 环境。在 Node.js 社区具有最多的用户,不显示地支持浏览器。
CommonJS 使用基于文件的模块,文件同步加载,每个文件中只能定义一个模块。
CommonJS 在服务端更流行,因为模块加载相对更快,只需要读取文件系统,而在客户端则必须从远程服务器下载文件。
ES6 模块
ES6 模块结合了两者的有点:
- 与 CommonJs 类似,ES6 模块语法相对简单,并且基于文件(每个文件就是一个模块)
- 与 AMD 类似,ES6 模块支持异步模块加载
第十二章 DOM 操作
DOM 的特性和属性
当访问元素的特性值时,我们可以使用 DOM 方法: setAttribute getAttribute,或使用 DOM 对象上与之相对应的属性(div.id)。
下面的代码说明并非所有的元素特性都能被属性表示,自定义的特性并不能被元素属性表示,必须通过 setAttribute getAttribute 才能访问。
1 | <div data-a="a"></div> |
HTML5 中,建议使用 data- 作为自定义属性的前缀。
获取计算后的属性
元素的 style 属性并不包含它在样式表中继承的样式信息。
1 | <style> |
使用 window.getComputedStyle 获取计算后的值,最后的 border 是自己加的,在 computedStyles 中是没有的,但还是打印出了 1px solid rgb(220, 20, 60),正则表达式把驼峰转为中横线分隔,因为 getPropertyValue 只接受中横线分隔。
1 | <style type="text/css"> |
测量元素的高度和宽度
height 和 width 这样的 style 属性造成了另外一个特殊问题,在不指定值的情况下,它们的默认值是 auto,以便让元素的大小根据其内容进行决定。因此,除非显式提供特性字符串,不能使用 height 和 width 来获取准确的值的。
值得庆幸的是,offsetHeight和offsetWidth都提供了这样的功能:可以相当可靠地访问实际元素的高度和宽度。但是请注意,这两个属性的值都包含了元素的padding值。
然而,需要当心的是,在高度交互的网站中,元素的隐藏(display值设置为none时),可能会花一些时间,而且一个元素如果不显示的话,它就没有尺寸。在非显示元素上,尝试获取offsetWidth或offsetHeight属性值,结果都是0。
对于这样的隐藏元素,如果需要获取它在非隐藏状态时的尺寸,我们可以使用一个技巧,暂时取消元素的隐藏,然后获取值,然后再将其隐藏。当然,我们希望这种做法不要在视觉上漏出破绽,而是在幕后操作。那如何才能将一个隐藏元素,在不可见的情况下编程不隐藏呢?
具体方法如下:
- 将display属性设置为block。
- 将visibility设置为hidden。
- 将position设置为absolute。
- 获取元素尺寸。
- 恢复先前更改的属性。
将display属性修改为block,可以让我们获取offsetHeight和offsetWidth的真实值,但元素会变成可见。为了使元素不可见,我们将visibility属性设置为hidden。但是这种做法会导致在元素的位置上显示一片空白,所以我们需要将position属性设置为absolute,以便将元素移出正常的可视区。
检查offsetWidth和offsetHeight属性值是否为0,可以非常有效地确定一个元素的可见性
1 | <style type="text/css"> |
避免布局抖动
布局抖动的核心问题在于,每当我们修改DOM时,浏览器必须在读取任何布局信息之前先重新计算布局。这种对性能的损耗十分巨大。
避免布局抖动的一种方法,就是尽量使用不会导致浏览器重排的方式编写代码。
元素的尺寸之间不存在依赖关系时可以进行批量读取和写入,这样可以让浏览器进行批量修改DOM的操作。
1 | const ninja = document.getElementById("ninja"); |
布局抖动对于精简页面无需过分考虑,但是在开发复杂的Web应用程序时需要特别注意,特别是在移动设备上。因此最好能记住所有会引起布局抖动的方法和属性。
第十三章 历久弥新的事件
深入事件循环
宏任务、微任务和事件循环
宏任务(或通常称为任务)。
宏任务的例子很多,包括创建主文档对象、解析HTML、执行主线(或全局)JavaScript代码,更改当前URL以及各种事件,如页面加载、输入、网络事件和定时器事件。从浏览器的角度来看,宏任务代表一个个离散的、独立工作单元。运行完任务后,浏览器可以继续其他调度,如重新渲染页面的UI或执行垃圾回收。
而微任务是更小的任务。微任务更新应用程序的状态,但必须在浏览器任务继续执行其他任务之前执行,浏览器任务包括重新渲染页面的UI。微任务的案例包括promise回调函数、DOM发生变化等。微任务需要尽可能快地、通过异步方式执行,同时不能产生全新的微任务。微任务使得我们能够在重新渲染UI之前执行指定的行为,避免不必要的UI重绘,UI重绘会使应用程序的状态不连续。
事件循环基于两个基本原则:
- 一次处理一个任务。
- 一个任务开始后直到运行完成,不会被其他任务中断。
如图所示:
事件循环通常至少需要两个事件队列:宏任务队列和微任务队列。
细节:
全局来看,图中展示了在一次迭代中,事件循环将首先检查宏任务队列,如果宏任务等待,则立即开始执行宏任务。直到该任务运行完成(或者队列为空),事件循环将移动去处理微任务队列。如果有任务在该队列中等待,则事件循环将依次开始执行,完成一个后执行余下的微任务,直到队列中所有微任务执行完毕。注意处理宏任务和微任务队列之间的区别:单次循环迭代中,最多处理一个宏任务(其余的在队列中等待),而队列中的所有微任务都会被处理。
当微任务队列处理完成并清空时,事件循环会检查是否需要更新UI渲染,如果是,则会重新渲染UI视图。至此,当前事件循环结束,之后将回到最初第一个环节,再次检查宏任务队列,并开启新一轮的事件循环。
因为JavaScript基于单线程执行模型,所以这两类任务都是逐个执行的。当一个任务开始执行后,在完成前,中间不会被任何其他任务中断。除非浏览器决定中止执行该任务,例如,某个任务执行时间过长或内存占用过大。所有微任务会在下一次渲染之前执行完成,因为它们的目标是在渲染前更新应用程序状态。浏览器通常会尝试每秒渲染60次页面,以达到每秒60帧(60 fps)的速度。60fps通常是检验体验是否平滑流畅的标准,比方在动画里——这意味着浏览器会尝试在16ms内渲染一帧。需要注意图中所示的“更新渲染”是何时发生在事件循环内的,因为在页面渲染时,任何任务都无法再进行修改。这些设计和原则都意味着,如果想要实现平滑流畅的应用,我们是没有太多时间浪费在处理单个事件循环任务的。理想情况下,单个任务和该任务附属的所有微任务,都应在16ms内完成。
因此提醒我们注意事件处理函数的发生频率以及执行耗时。例如,处理鼠标移动(mouse-move)事件时应当特别小心。因为移动鼠标将导致大量的事件进入队列,因此在鼠标移动的处理函数中执行任何复杂操作都可能导致Web应用的糟糕体验。
仅含宏任务的示例
1 | <button id="firstButton">First button</button> |
代码需要发挥一些想象空间,避免添加不必要的聚合代码,要求读者想象以下内容。
主线程JavaScript代码执行时间需要15ms。第一个单击事件处理器需要运行8ms。第二个单击事件处理器需要运行5ms。假设一个手快的用户在代码执行后5ms时单击第一个按钮,随后在12ms时单击第二个按钮。
如图所示:
由于JavaScript基于单线程执行模型,单击firstButton并不会立即执行对应的处理器。(记住,一个任务一旦开始执行,就不会被另一个任务中断)firstButton的事件处理器则进入任务队列,等待执行。当单击secondButton时发生类似的情况:对应的事件处理器进入队列,等待执行。注意,事件监测和添加任务是独立于事件循环的,尽管主线程仍在执行,仍然可以向队列添加任务。
本示例强调如果其他任务正在执行,那么事件则需要按顺序等待执行。例如,尽管在第12ms时单击secondButton,但是其对应的事件处理任务在第23ms时才开始执行。
同时含有宏任务和微任务
1 | <button id="firstButton">First button</button> |
与之前的示例唯一的区别是,在firstHandler代码中我们创建立即兑现的promise,并需要运行4ms的传入回调函数。因为promise表示当前未知的一个未来值,因此promise处理函数总是异步执行。
在本例中,我们创建立即兑现的promise。说实话,JavaScript引擎本应立即调用回调函数,因为我们已知promise成功兑现。但是,为了连续性,JavaScript引擎不会这么做,仍然会在firstHandler代码执行(需要运行8ms)完成之后再异步调用回调函数。通过创建微任务,将回调放入微任务队列。
除了宏任务队列之外,本例重点关注微任务队列,在第12ms时微任务队列仍为空。
处理计算复杂度高的任务
1 | <table><tbody></tbody></table> |
个人理解的就是创建了这么多的微任务(DOM操作),会使页面在渲染前等待的时间特别长。
上面的代码中,我们创建了240 000个DOM节点,创建一个20 000行、每行6列的表格,表格中的每个单元格都包含一个文本节点。这个操作的消耗是惊人的,会导致浏览器挂起一段时间,这段时间内用户无法正常操作(如同Bruce叔叔主宰了家庭聚会中的谈话)。
我们需要做的就是让Bruce叔叔定期闭嘴,这样其他人才有机会加入谈话。在代码中,我们可以引入定时器来创建这样的“中断谈话”。如下所示:
1 | <table><tbody></tbody></table> |
令人印象深刻的是,可以使用异步的方法(setTimeout)来优化代码。在本例中,我们使用0作为超时时间。如果关注事件循环是如何工作的,就会知道这并不意味着将在0ms时执行回调。使用0,意味着通知浏览器尽快执行回调,但仍然必须在UI更新之后执行。
通过这种技术,从用户的角度可察觉的最显著的变化是,一个长时间的浏览器挂起,替代为4次(次数可修改)页面更新。尽管浏览器尝试尽可能快地执行代码片段,但仍然是依次执行DOM渲染。在这段代码的初始版本中,页面更新需要等待很长时间。
个人理解的就是借助setTimeout,将这么多的微任务分解成了四个宏任务,每一段宏任务后面有原来总量四分之一的微任务,这样浏览器渲染前等待的时间就会明显变少了。
除了下面的记录,笔记基本完成
第十章 正则表达式用到再看
第十二章 DOM 注入 HTML ,闭合标签用到再看。
第十三章自定义事件用到再看