原文:Optimising the front end for the browser
笔记:涂鸦码龙
优化关乎速度和满意度。
从开发体验(DX)角度,我们希望前端是快速,简洁,规范的
我们希望浏览器打开一个简单的网页
|
|
浏览器从上到下读取标签,把他们分解成节点,从而创建 DOM 。
总体思路是尽可能早的加载样式,尽可能晚的加载脚本。原因是脚本执行之前,需要 HTML 和 CSS 解析完成,因此,样式尽可能的往顶部放,当底部脚本开始执行之前,样式有足够的时间完成计算。
进一步讲讲如何优化
方法可用于所有内容,包括 HTML,CSS,JavaScript,图片和其它资源。
最小化是移除所有多余的字符,包括空格,注释,多余的分号,等等。
压缩比如 GZip,大大压缩下载文件的大小
两种方法都用的情况下,资源加载量减少了 80% 到 90%。比如:bootstrap 节省了 87% 的流量。
不会提升页面的下载速度,但会大大提升残障人士的满意度。给元素加上 aria
标签,图片提供 alt
文本,HTML 5 无障碍参见。
使用诸如 WAVE 的工具鉴别哪些地方可以提高可访问性。
当浏览器发现任何与节点相关的样式时,比如:外部,内部,或行内样式,立即停止渲染 DOM ,并利用这些节点创建 CSSOM。这就是 CSS “渲染阻塞“ 的由来。这里是不同类型样式的优缺点。
|
|
CSSOM 节点创建与 DOM 节点创建类似,随后,两者合并如下:
CSSOM 的构建会阻塞页面的渲染,因此我们想尽早加载样式,
media 属性指定加载样式的条件,比如:符合最大或最小分辨率?还是面向屏幕阅读器?
有些样式,比如:首屏以下的,或者不那么重要的,可以等待首屏最有价值的内容渲染完成再加载,可以使用脚本等待页面加载,然后再插入样式。
这有两个栗子:The future of loading CSS,Defer load CSS
使用 uncss 类似的工具,尽量移除不需要的样式。
浏览器不断构建 DOM / CSSOM 节点,直到发现外部或者行内的脚本。
由于脚本可能需要访问或操作之前的 HTML 或样式,我们必须等待它们构建完成。
因此浏览器必须停止解析节点,完成构建 CSSOM,执行脚本,然后再继续。这就是 JavaScript 被称作“解析器阻塞”的原因。
脚本只能等到先前的 CSS 节点构建完成。
脚本添加 async
属性,可以通知浏览器不要阻塞其余页面的加载,下载脚本处于较低的优先级。一旦下载完成,就可以执行。
async
适用于不影响 DOM 或 CSSOM 的脚本,对一些跟我们的代码无关的,不影响用户体验的外部脚本尤其适用,比如:分析统计脚本。
defer
跟 async
非常相似,不会阻塞页面加载,但会等到 HTML 完成解析后再执行。
使用 defer 策略的 另一个好选择,或者也可以使用 addEventListener
,了解更多,参加这里。
不幸的是 async
和 defer
对于行内的脚本不起作用,浏览器默认会编译执行它们。
多次操作 DOM 时可以尝试,首先克隆整个 DOM 节点更加高效,操作克隆后的节点,然后替换先前的节点,避免了多次重绘,降低了 CPU 和内存消耗,同时也避免了不必要的页面闪烁。
需要注意,克隆的时候并没有克隆事件监听。
这些新属性并不是所有的浏览器都支持。了解详情可以看这里:Prefetching, preloading, prebrowsing
一旦所有节点已被解析,DOM 和 CSSOM 准备合并,浏览器便会构建渲染树。如果我们把节点想象成单词,那么对象模型就是句子,渲染树便是整个页面。
布局阶段需要确定页面上所有元素的大小和位置。
最终的渲染阶段,会真正地光栅化屏幕上的像素,把页面呈现给用户。
整个过程耗时1秒或十分之一秒,我们的任务是让它更快。
如果 JavaScript 事件改变了页面的某部分,便会引起渲染树的重绘,并且迫使布局(Layout)和渲染(Paint)过程再次进行。
当浏览器请求一个 URL,服务端会响应一些 HTML。
我们需要认识一个新术语,关键渲染路径(Critical Rendering Path (CRP)),就是浏览器渲染页面的步骤数,如下图。
关键渲染路径的度量标准是路径长度。最理想的关键路径长度是1。
如果页面包含一些内部样式和 JavaScript ,关键路径发生以下改变。
新增两步,构建 CSSOM和执行脚本,因为我们的 HTML 有内部样式和脚本需要计算。由于没有外部请求,我们的关键路径长度没变。
但是注意,我们的 HTML 大小增加到了 2kb,某些地方还是受了影响。
三个度量标准之二出现了,关键字节数,它用来衡量渲染页面需要传送多少字节数。
如果你认为不需要外部资源,就大错特错了,外部资源可以被缓存。
我们使用一个外部 CSS 文件,一个外部 JavaScript 文件,和一个外部带 async
属性的 JavaScript 文件。关键路径图如下:
浏览器请求页面,构建 DOM,发现外部资源后开始下载,CSS 和 JavaScript 有较高的优先级,其它资源次之。
styles.css
和 app.js
通过另一个关键路径获取。暂时不获取 analytics.js
,因为加了 async
属性,浏览器将用另一个线程下载它,它处于较低优先级,不会阻塞页面渲染,也不影响关键路径。
最后一个度量标准是关键文件,浏览器渲染页面需要下载的文件总量。以上例子,HTML 文件,CSS 和 JavaScript 文件算关键文件,async
的脚本不算。当然是文件越少越好。
以上例子就是最长的渲染路径吗?我认为渲染页面时,我们仅需要下载 HTML,CSS 和 JavaScript 文件,仅通过两次服务器往返就做到了。
我们浏览器的 HTTP1 协议,在同一个域名,同一次,允许下载的文件数有最大限制,范围从 2(老旧的浏览器)到 6(Edge,Chrome)。
各种浏览器请求文件的最大并发数,参见Maximum concurrent connections to the same domain for browsers。
通过把一些资源存放到影子域名,可以绕过这个限制,以达到最佳优化效果。
注意:不要把关键的 CSS 放到根域名之外的其他域名,有些场景下会对 DNS 查找和延迟起反作用。
如果网站使用了 HTTP2,并且用户的浏览器也兼容,则可以完全避开这个下载限制。
这里有个 HTTP2 测试网站。
每一次服务器往返可以传送的最大数据量是 14kb,包括所有 HTML,CSS 和脚本的网络请求。
如果我们的 HTML,或者积累的资源请求超过 14kb时,需要多做一次服务器往返。
我们整个 HTML 页面可以很好的压缩, GZip 可以压缩到 2kb,远低于 14kb 的限制,因此,一次服务器往返就可以搞定。
关键路径度量: 长度 1,文件数 1,字节数 2kb
浏览器发现外部资源(CSS 和 JavaScript)时,发起请求开始下载它们。首要下载的 CSS 文件是 14kb,达到了往返传输的最大限制,因此增加了一条关键路径。
关键路径度量: 长度 2,文件数 2,字节数 16kb
余下的资源低于 14kb,但是总共有 7 个资源,由于网站未启用 HTTP2,我们的 Chrome,每一次往返仅可以下载 6 个文件。
关键路径度量: 长度 3,文件数 8,字节数 28kb
下载完最终文件,并开始渲染 DOM。
关键路径度量: 长度 4,文件数 9,字节数 30kb
基于以上的信息和知识,发起每个连接时,就可以准确地预估页面的性能了。
使用 Insights 鉴别性能问题,Chrome DevTools 也有个 audit
标签。
这篇文章 值得一读,帮你理解网络资源
开发时大可使用 1Tb SSD,32G 内存的 Macbook Pro ,但是性能测试时还是要到 Chrome 的 network
标签下模拟低带宽的情形,从而获取有价值的信息。
基本上,每接收到一个外部 CSS 和 JavaScript 文件,浏览器都会构建 CSSOM,执行脚本。尽管几个文件可以在一次往返中传送,但也浪费了浏览器的宝贵时间和资源,最好还是合并文件,减少不必要的加载。
内部 CSS 和 JavaScript 不需要请求外部资源,相反,外部资源又可以被缓存,并保持 DOM 轻量,两者没有非黑即白。
但是一个非常好的论点是首屏关键内容使用内部样式,可以避免请求额外的资源,节省时间做最有意义的渲染。
最小化/压缩图片
延迟加载图片
异步记载字体
是否真正需要 JavaScript / CSS?
原生 HTML 元素可以实现的行为是否用了脚本?是否有样式或者图标可以行内创建的,不需要内部/外部资源?比如:行内 SVG。
可以利用 CDN(内容分发网络)存储资源,它会从离用户最近,延迟最低的位置分发到用户设备,加载时间更快。
延伸阅读
关键渲染路径是最重要的,它使得网站优化有规律可循。需要关注3个指标:
1—关键字节数
2—关键文件数
3—关键路径长度
原文:Optimising the front end for the browser
笔记:涂鸦码龙
优化关乎速度和满意度。
从开发体验(DX)角度,我们希望前端是快速,简洁,规范的
我们希望浏览器打开一个简单的网页
|
|
原文:“JavaScript Array Methods: Mutating vs. Non-Mutating”
笔记:涂鸦码龙
JavaScript 提供了多种新增,移除,替换数组元素的方法,但是有些会影响原来的数组;有些则不会,它是新建了一个数组。
注意:区分以下两个方法的不同点:
array.splice()
影响原来的数组array.slice()
不影响原来的数组使用 array.push()
和 array.ushift()
新增元素会影响原来的数组。
|
|
两种方式新增元素不会影响原数组,第一种是 array.concat()
。
|
|
第二种方法是使用 JavaScript 的展开(spread)操作符,展开操作符是三个点(…)
|
|
展开操作符会复制原来的数组,从原数组取出所有元素,然后存入新的环境。
使用 array.pop()
和 array.shift()
移除数组元素时,会影响原来的数组。
|
|
array.pop()
和 array.shift()
返回被移除的元素,你可以通过一个变量获取被移除的元素。
|
|
array.splice()
也可以删除数组的元素。
|
|
像 array.pop()
和 array.shift()
一样,array.splice()
同样返回移除的元素。
|
|
JavaScript 的 array.filter()
方法基于原数组创建一个新数组,新数组仅包含匹配特定条件的元素。
|
|
以上代码的条件是“不等于 ‘e’ ”,因此新数组(arr2
)里面没有包含 ‘e’。
箭头函数的独特性:
单行箭头函数,’return’ 关键字是默认自带的,不需要手动书写。
可是,多行箭头函数就需要明确地返回一个值。
另一种不影响原数组的方式是 array.slice()
(不要与 array.splice()
混淆)。
|
|
如果知道替换哪一个元素,可以使用 array.splice()
。
|
|
可以使用 array.map()
创建一个新数组,并且可以检查每一个元素,根据特定的条件替换它们。
|
|
使用 array.map()
转换数据
array.map()
是个强力方法,可以用于转换数据,而不污染原先的数据源。
|
|
原文:“JavaScript Array Methods: Mutating vs. Non-Mutating”
笔记:涂鸦码龙
JavaScript 提供了多种新增,移除,替换数组元素的方法,但是有些会影响原来的数组;有些则不会,它是新建了一个数组。
注意:区分以下两个方法的不同点:
array.splice()
影响原来的数组array.slice()
不影响原来的数组使用 array.push()
和 array.ushift()
新增元素会影响原来的数组。
|
|
言归正传,正文开始
之前没搞过 React ,直接开撸的 React Native,使用过程也是各种踩坑填坑,磕磕绊绊,这里简单总结一下我用过的组件通信的几种方式吧。
|
|
|
|
|
|
|
|
|
|
|
|
global
类似浏览器里的 window
对象,它是全局的,一处定义,所有组件都可以访问,一般用于存储一些全局的配置参数或方法。
使用场景:全局参数不想通过 props
层层组件传递,有些组件对此参数并不关心,只有嵌套的某个组件使用
|
|
RCTDeviceEventEmitter
是一种事件机制,React Native 的文档只是草草带过,也可以使用 DeviceEventEmitter
,它是把 RCTDeviceEventEmitter
封装了一层,用法略不同。
按文档所言,RCTDeviceEventEmitter
主要用于 Native 发送事件给 JavaScript,实际上也可以用来发送自定义事件。
使用场景:多个组件都使用了异步模块,且异步模块之间有顺序依赖时,可以使用。
|
|
这是官方提供的持久缓存的模块,类似浏览器端的 localStorage
,用法也很类似,不过比 localStorage
多了不少 API。
使用场景:当然也类似,退出应用需要保存的少量数据,可以存在这里,至于大小限制,Android 貌似是 6M 。
|
|
综上所述,这是我能想到的组件通信方式,其它想到了再补充吧。
]]>言归正传,正文开始
之前没搞过 React ,直接开撸的 React Native,使用过程也是各种踩坑填坑,磕磕绊绊,这里简单总结一下我用过的组件通信的几种方式吧。
|
|
原文:Filtered background with fallback for legibility
翻译:涂鸦码龙
你知道现在有多火吗?用这种很大的,高质量的,支持 Retina 屏的模糊的 JPEG 图片作为 header 背景 :
See the Pen Web site header, circa 2016 by Taylor Hunt (@tigt) on CodePen.
潜在的问题是如果浏览器不支持滤镜 filter,文字将不可读 。这违背了可访问性的原则,再完美的视觉也无济于事。
支持和不支持 filter 的效果对比
因此呢,你需要提前准备好处理模糊的图片,可是设计师偏要你实现动态模糊呢,图片不固定,跟 Apple 的效果一样一样的,咋办?如果是用户上传的图片又咋办?嗯,你需要一台图片处理服务器,听起来成本很高。
我碰巧想到一个妙招,让不支持 filter
的浏览器用颜色图层代替,方案并不完美,但是具有高可读性:
不支持 filter 的效果
我用伪元素作为背景,因为给元素加 filter
比单纯给背景加 filter 兼容性更好 。
|
|
此时,设计师发话了,让背景图模糊一下,再来个轻微变暗效果就更好了。
|
|
这么做还远远不够,不支持 filter
的浏览器将严重影响阅读。
是否听过 filter
有 opacity() 效果 ?对比广泛支持的 opacity
属性,它显得有点鸡肋,但是它的存在才使得兼容方案得以实现:
|
|
如果支持 filter 滤镜,带图片的伪元素是模糊的,微微变暗的,这一黑色的遮罩层是完全透明的。如果不支持滤镜呢,显示效果并不完美,但是不会阻碍任何人阅读上面的文字。
CodePen 的例子在此,希望你喜欢:
See the Pen Filtered background with fallback for legibility by Taylor Hunt (@tigt) on CodePen.
其它 filter 滤镜的使用或许也能受此启发,我把它放到了我的锦囊里。
浏览器对 CSS 特性检测(@supports
)的支持程度跟 filter 保持一致 ,替换以后代码更加清晰明了:
|
|
你完全可以这么写,全看个人喜好。我查阅了 CanIUse 的使用数据,有一些环境仍不支持 @supports
:
直到2016年5月,以上浏览器的联合使用份额是 ≈ 美国 3.6%、世界范围 10.5%(感谢大洋彼岸 UC 的盛行)。这些数字会随着时间流逝逐渐减少,到那时,如果想写更加清晰的 CSS 就推荐使用 @supports
了。
原文:Filtered background with fallback for legibility
翻译:涂鸦码龙
你知道现在有多火吗?用这种很大的,高质量的,支持 Retina 屏的模糊的 JPEG 图片作为 header 背景 :
See the Pen Web site header, circa 2016 by Taylor Hunt (@tigt) on CodePen.
潜在的问题是如果浏览器不支持滤镜 filter,文字将不可读 。这违背了可访问性的原则,再完美的视觉也无济于事。
]]>原文:4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them
笔记:涂鸦码龙
译者注:本文并没有逐字逐句的翻译,而是把我认为重要的信息做了翻译。如果您的英文熟练,可以直接阅读原文。
本文将探索常见的客户端 JavaScript 内存泄漏,以及如何使用 Chrome 开发工具发现问题。
内存泄漏是每个开发者最终都要面对的问题,它是许多问题的根源:反应迟缓,崩溃,高延迟,以及其他应用问题。
本质上,内存泄漏可以定义为:应用程序不再需要占用内存的时候,由于某些原因,内存没有被操作系统或可用内存池回收。编程语言管理内存的方式各不相同。只有开发者最清楚哪些内存不需要了,操作系统可以回收。一些编程语言提供了语言特性,可以帮助开发者做此类事情。另一些则寄希望于开发者对内存是否需要清晰明了。
JavaScript 是一种垃圾回收语言。垃圾回收语言通过周期性地检查先前分配的内存是否可达,帮助开发者管理内存。换言之,垃圾回收语言减轻了“内存仍可用”及“内存仍可达”的问题。两者的区别是微妙而重要的:仅有开发者了解哪些内存在将来仍会使用,而不可达内存通过算法确定和标记,适时被操作系统回收。
垃圾回收语言的内存泄漏主因是不需要的引用。理解它之前,还需了解垃圾回收语言如何辨别内存的可达与不可达。
大部分垃圾回收语言用的算法称之为 Mark-and-sweep 。算法由以下几步组成:
现代的垃圾回收器改良了算法,但是本质是相同的:可达内存被标记,其余的被当作垃圾回收。
不需要的引用是指开发者明知内存引用不再需要,却由于某些原因,它仍被留在激活的 root 树中。在 JavaScript 中,不需要的引用是保留在代码中的变量,它不再需要,却指向一块本该被释放的内存。有些人认为这是开发者的错误。
为了理解 JavaScript 中最常见的内存泄漏,我们需要了解哪种方式的引用容易被遗忘。
JavaScript 处理未定义变量的方式比较宽松:未定义的变量会在全局对象创建一个新变量。在浏览器中,全局对象是 window
。
|
|
真相是:
|
|
函数 foo
内部忘记使用 var
,意外创建了一个全局变量。此例泄漏了一个简单的字符串,无伤大雅,但是有更糟的情况。
另一种意外的全局变量可能由 this
创建:
|
|
在 JavaScript 文件头部加上
'use strict'
,可以避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。
全局变量注意事项
尽管我们讨论了一些意外的全局变量,但是仍有一些明确的全局变量产生的垃圾。它们被定义为不可回收(除非定义为空或重新分配)。尤其当全局变量用于临时存储和处理大量信息时,需要多加小心。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。与全局变量相关的增加内存消耗的一个主因是缓存。缓存数据是为了重用,缓存必须有一个大小上限才有用。高内存消耗导致缓存突破上限,因为缓存内容无法被回收。
在 JavaScript 中使用 setInterval
非常平常。一段常见的代码:
|
|
此例说明了什么:与节点或数据关联的计时器不再需要,node
对象可以删除,整个回调函数也不需要了。可是,计时器回调函数仍然没被回收(计时器停止才会被回收)。同时,someResource
如果存储了大量的数据,也是无法被回收的。
对于观察者的例子,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。老的 IE 6 是无法处理循环引用的。如今,即使没有明确移除它们,一旦观察者对象变成不可达,大部分浏览器是可以回收观察者处理函数的。
观察者代码示例:
|
|
对象观察者和循环引用注意事项
老版本的 IE 是无法检测 DOM 节点与 JavaScript 代码之间的循环引用,会导致内存泄漏。如今,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法,已经可以正确检测和处理循环引用了。换言之,回收节点内存时,不必非要调用 removeEventListener
了。
有时,保存 DOM 节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组很有意义。此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除。
|
|
此外还要考虑 DOM 树内部或子节点的引用问题。假如你的 JavaScript 代码中保存了表格某一个 <td>
的引用。将来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的 <td>
以外的其它节点。实际情况并非如此:此 <td>
是表格的子节点,子元素与父元素是引用关系。由于代码保留了 <td>
的引用,导致整个表格仍待在内存中。保存 DOM 元素引用的时候,要小心谨慎。
闭包是 JavaScript 开发的一个关键方面:匿名函数可以访问父级作用域的变量。
代码示例:
|
|
代码片段做了一件事情:每次调用 replaceThing
,theThing
得到一个包含一个大数组和一个新闭包(someMethod
)的新对象。同时,变量 unused
是一个引用 originalThing
的闭包(先前的 replaceThing
又调用了 theThing
)。思绪混乱了吗?最重要的事情是,闭包的作用域一旦创建,它们有同样的父级作用域,作用域是共享的。someMethod
可以通过 theThing
使用,someMethod
与 unused
分享闭包作用域,尽管 unused
从未使用,它引用的 originalThing
迫使它保留在内存中(防止被回收)。当这段代码反复运行,就会看到内存占用不断上升,垃圾回收器(GC)并无法降低内存占用。本质上,闭包的链表已经创建,每一个闭包作用域携带一个指向大数组的间接的引用,造成严重的内存泄漏。
Meteor 的博文 解释了如何修复此种问题。在
replaceThing
的最后添加originalThing = null
。
Chrome 提供了一套很棒的检测 JavaScript 内存占用的工具。与内存相关的两个重要的工具:timeline
和 profiles
。
timeline 可以检测代码中不需要的内存。在此截图中,我们可以看到潜在的泄漏对象稳定的增长,数据采集快结束时,内存占用明显高于采集初期,Node(节点)的总量也很高。种种迹象表明,代码中存在 DOM 节点泄漏的情况。
Profiles 是你可以花费大量时间关注的工具,它可以保存快照,对比 JavaScript 代码内存使用的不同快照,也可以记录时间分配。每一次结果包含不同类型的列表,与内存泄漏相关的有 summary(概要) 列表和 comparison(对照) 列表。
summary(概要) 列表展示了不同类型对象的分配及合计大小:shallow size(特定类型的所有对象的总大小),retained size(shallow size 加上其它与此关联的对象大小)。它还提供了一个概念,一个对象与关联的 GC root 的距离。
对比不同的快照的 comparison list 可以发现内存泄漏。
实质上有两种类型的泄漏:周期性的内存增长导致的泄漏,以及偶现的内存泄漏。显而易见,周期性的内存泄漏很容易发现;偶现的泄漏比较棘手,一般容易被忽视,偶尔发生一次可能被认为是优化问题,周期性发生的则被认为是必须解决的 bug。
以 Chrome 文档中的代码为例:
|
|
当 grow
执行的时候,开始创建 div 节点并插入到 DOM 中,并且给全局变量分配一个巨大的数组。通过以上提到的工具可以检测到内存稳定上升。
timeline 标签擅长做这些。在 Chrome 中打开例子,打开 Dev Tools ,切换到 timeline,勾选 memory 并点击记录按钮,然后点击页面上的 The Button
按钮。过一阵停止记录看结果:
两种迹象显示出现了内存泄漏,图中的 Nodes(绿线)和 JS heap(蓝线)。Nodes 稳定增长,并未下降,这是个显著的信号。
JS heap 的内存占用也是稳定增长。由于垃圾收集器的影响,并不那么容易发现。图中显示内存占用忽涨忽跌,实际上每一次下跌之后,JS heap 的大小都比原先大了。换言之,尽管垃圾收集器不断的收集内存,内存还是周期性的泄漏了。
确定存在内存泄漏之后,我们找找根源所在。
切换到 Chrome Dev Tools 的 profiles 标签,刷新页面,等页面刷新完成之后,点击 Take Heap Snapshot 保存快照作为基准。而后再次点击 The Button
按钮,等数秒以后,保存第二个快照。
筛选菜单选择 Summary,右侧选择 Objects allocated between Snapshot 1 and Snapshot 2,或者筛选菜单选择 Comparison ,然后可以看到一个对比列表。
此例很容易找到内存泄漏,看下 (string)
的 Size Delta
Constructor,8MB,58个新对象。新对象被分配,但是没有释放,占用了8MB。
如果展开 (string)
Constructor,会看到许多单独的内存分配。选择某一个单独的分配,下面的 retainers 会吸引我们的注意。
我们已选择的分配是数组的一部分,数组关联到 window
对象的 x
变量。这里展示了从巨大对象到无法回收的 root(window
)的完整路径。我们已经找到了潜在的泄漏以及它的出处。
我们的例子还算简单,只泄漏了少量的 DOM 节点,利用以上提到的快照很容易发现。对于更大型的网站,Chrome 还提供了 Record Heap Allocations 功能。
回到 Chrome Dev Tools 的 profiles 标签,点击 Record Heap Allocations。工具运行的时候,注意顶部的蓝条,代表了内存分配,每一秒有大量的内存分配。运行几秒以后停止。
上图中可以看到工具的杀手锏:选择某一条时间线,可以看到这个时间段的内存分配情况。尽可能选择接近峰值的时间线,下面的列表仅显示了三种 constructor:其一是泄漏最严重的(string)
,下一个是关联的 DOM 分配,最后一个是 Text
constructor(DOM 叶子节点包含的文本)。
从列表中选择一个 HTMLDivElement
constructor,然后选择 Allocation stack
。
现在知道元素被分配到哪里了吧(grow
-> createSomeNodes
),仔细观察一下图中的时间线,发现 HTMLDivElement
constructor 调用了许多次,意味着内存一直被占用,无法被 GC 回收,我们知道了这些对象被分配的确切位置(createSomeNodes
)。回到代码本身,探讨下如何修复内存泄漏吧。
在 heap allocations 的结果区域,选择 Allocation。
这个视图呈现了内存分配相关的功能列表,我们立刻看到了 grow
和 createSomeNodes
。当选择 grow
时,看看相关的 object constructor,清楚地看到 (string)
, HTMLDivElement
和 Text
泄漏了。
结合以上提到的工具,可以轻松找到内存泄漏。
原文:4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them
笔记:涂鸦码龙
译者注:本文并没有逐字逐句的翻译,而是把我认为重要的信息做了翻译。如果您的英文熟练,可以直接阅读原文。
本文将探索常见的客户端 JavaScript 内存泄漏,以及如何使用 Chrome 开发工具发现问题。
内存泄漏是每个开发者最终都要面对的问题,它是许多问题的根源:反应迟缓,崩溃,高延迟,以及其他应用问题。
本质上,内存泄漏可以定义为:应用程序不再需要占用内存的时候,由于某些原因,内存没有被操作系统或可用内存池回收。编程语言管理内存的方式各不相同。只有开发者最清楚哪些内存不需要了,操作系统可以回收。一些编程语言提供了语言特性,可以帮助开发者做此类事情。另一些则寄希望于开发者对内存是否需要清晰明了。
JavaScript 是一种垃圾回收语言。垃圾回收语言通过周期性地检查先前分配的内存是否可达,帮助开发者管理内存。换言之,垃圾回收语言减轻了“内存仍可用”及“内存仍可达”的问题。两者的区别是微妙而重要的:仅有开发者了解哪些内存在将来仍会使用,而不可达内存通过算法确定和标记,适时被操作系统回收。
垃圾回收语言的内存泄漏主因是不需要的引用。理解它之前,还需了解垃圾回收语言如何辨别内存的可达与不可达。
大部分垃圾回收语言用的算法称之为 Mark-and-sweep 。算法由以下几步组成:
现代的垃圾回收器改良了算法,但是本质是相同的:可达内存被标记,其余的被当作垃圾回收。
不需要的引用是指开发者明知内存引用不再需要,却由于某些原因,它仍被留在激活的 root 树中。在 JavaScript 中,不需要的引用是保留在代码中的变量,它不再需要,却指向一块本该被释放的内存。有些人认为这是开发者的错误。
为了理解 JavaScript 中最常见的内存泄漏,我们需要了解哪种方式的引用容易被遗忘。
]]>原文:Debouncing and Throttling Explained Through Examples
笔记:涂鸦码龙
防抖(Debounce)和节流(throttle)都是用来控制某个函数在一定时间内执行多少次的技巧,两者相似而又不同。
当我们给 DOM 绑定事件的时候,加了防抖和节流的函数变得特别有用。为什么呢?因为我们在事件和函数执行之间加了一个控制层。记住,我们是无法控制 DOM 事件触发频率的。
看下滚动事件的例子:
See the Pen Scroll events counter by Corbacho (@dcorb) on CodePen.
当使用触控板,滚动滚轮,或者拖拽滚动条的时候,一秒可以轻松触发30次事件。经我的测试,在智能手机上,慢慢滚动一下,一秒可以触发事件100次之多。这么高的执行频率,你的滚动回调函数压力大吗?
早在2011年,Twitter 网站抛出了一个问题:向下滚动 Twitter 信息流的时候,变得很慢,很迟钝。John Resig 发表了一篇博客解释这个问题,文中解释到直接给 scroll
事件关联昂贵的函数,是多么糟糕的主意。
John(5年前)建议的解决方案是,在 onScroll
事件外部,每 250ms 循环执行一次。简单的技巧,避免了影响用户体验。
现如今,有一些稍微高端的方式处理事件。我来结合用例介绍下 Debounce,Throttle 和 requestAnimationFrame 吧。
防抖技术可以把多个顺序地调用合并成一次。
假想一下,你在电梯中,门快要关了,突然有人准备上来。电梯并没有改变楼层,而是再次打开梯门。电梯延迟了改变楼层的功能,但是优化了资源。
在顶部按钮上点击或移动鼠标试一下:
See the Pen Debounce. Trailing by Corbacho (@dcorb) on CodePen.
你可以看到连续快速的事件是如何被一个 debounce 事件替代的。但是如果事件触发的时间间隔过长,debounce 则不会生效。
你会发现,直到事件停止快速执行以后,debounce 事件才会触发相应功能。为何不立即触发呢?那样的话就跟原本的非 debounce 处理无异了。
直到两次快速调用之间的停顿结束,事件才会再次触发。
这是带 leading
标记的例子:
前缘 debounce 的例子
在 underscore.js 中,选项叫 immediate
,而不是 leading
:
See the Pen Debounce. Leading by Corbacho (@dcorb) on CodePen.
我首次看到 debounce 的 JavaScript 实现是在 2009 年的 John Hann 的博文。
不久后,Ben Alman 做了个 jQuery 插件(不再维护),一年后 Jeremy Ashkenas 把它加入了 underscore.js。而后加入了 Lodash 。
See the Pen New example by Corbacho (@dcorb) on CodePen.
Lodash 给 _.debounce
和 _.throttle
添加了不少特性。之前的 immediate
被 leading
(最前面) 和 trailing
(最后面) 选项取代。你可以选一种,或者都选,默认只有 trailing
启用。
新的 maxWait
选项(仅 Lodash 有)本文未提及,但是也很有用。事实上,throttle 方法是用 _.debounce
加 maxWait
实现的,你可以看 lodash 源码。
调整大小的例子
调整桌面浏览器窗口大小的时候,会触发很多次 resize
事件。
看下面 demo:
See the Pen Debounce Resize Event Example by Corbacho (@dcorb) on CodePen.
如你所见,我们为 resize 事件使用了默认的 trailing
选项,因为我们只关心用户停止调整大小后的最终值。
基于 AJAX 请求的自动完成功能,通过 keypress 触发
为什么用户还在输入的时候,每隔50ms就向服务器发送一次 AJAX 请求?_.debounce
可以帮忙,当用户停止输入的时候,再发送请求。
此处也不需要 leading
标记,我们想等最后一个字符输完。
See the Pen Debouncing keystrokes Example by Corbacho (@dcorb) on CodePen.
相似的使用场景还有,直到用户输完,才验证输入的正确性,显示错误信息。
自己造一个 debounce / throttle 的轮子看起来多么诱人,或者随便找个博文复制过来。我是建议直接使用 underscore 或 Lodash 。如果仅需要 _.debounce
和 _.throttle
方法,可以使用 Lodash 的自定义构建工具,生成一个 2KB 的压缩库。使用以下的简单命令即可:
|
|
常见的坑是,不止一次地调用 _.debounce
方法:
|
|
debounce 方法保存到一个变量以后,就可以用它的私有方法 debounced_version.cancel()
,lodash 和 underscore.js 都有效。
|
|
使用 _.throttle 的时候,只允许一个函数在 X 毫秒内执行一次。
跟 debounce 主要的不同在于,throttle 保证 X 毫秒内至少执行一次。
无限滚动
用户向下滚动无限滚动页面,需要检查滚动位置距底部多远,如果邻近底部了,我们可以发 AJAX 请求获取更多的数据插入到页面中。
我们心爱的 _.debounce
就不适用了,只有当用户停止滚动的时候它才会触发。只要用户滚动至邻近底部时,我们就想获取内容。
使用 _.throttle
可以保证我们不断检查距离底部有多远。
See the Pen Infinite scrolling throttled by Corbacho (@dcorb) on CodePen.
requestAnimationFrame
是另一种限速执行的方式。
跟 _.throttle(dosomething, 16)
等价。它是高保真的,如果追求更好的精确度的话,可以用浏览器原生的 API 。
可以使用 rAF API 替换 throttle 方法,考虑一下优缺点:
根据经验,如果 JavaScript 方法需要绘制或者直接改变属性,我会选择 requestAnimationFrame
,只要涉及到重新计算元素位置,就可以使用它。
涉及到 AJAX 请求,添加/移除 class (可以触发 CSS 动画),我会选择 _.debounce
或者 _.throttle
,可以设置更低的执行频率(例子中的200ms 换成16ms)。
灵感来自于 Paul Lewis 的文章,我将用 requestAnimationFrame 控制 scroll 。
16ms 的 _.throttle
拿来做对比,性能相仿,用于更复杂的场景时,rAF 可能效果更佳。
See the Pen Scroll comparison requestAnimationFrame vs throttle by Corbacho (@dcorb) on CodePen.
headroom.js 是个更高级的例子。
使用 debounce,throttle 和 requestAnimationFrame
都可以优化事件处理,三者各不相同,又相辅相成。
总之:
debounce:把触发非常频繁的事件(比如按键)合并成一次执行。
throttle:保证每 X 毫秒恒定的执行次数,比如每200ms检查下滚动位置,并触发 CSS 动画。
requestAnimationFrame:可替代 throttle ,函数需要重新计算和渲染屏幕上的元素时,想保证动画或变化的平滑性,可以用它。注意:IE9 不支持。
原文:Debouncing and Throttling Explained Through Examples
笔记:涂鸦码龙
防抖(Debounce)和节流(throttle)都是用来控制某个函数在一定时间内执行多少次的技巧,两者相似而又不同。
当我们给 DOM 绑定事件的时候,加了防抖和节流的函数变得特别有用。为什么呢?因为我们在事件和函数执行之间加了一个控制层。记住,我们是无法控制 DOM 事件触发频率的。
看下滚动事件的例子:
See the Pen Scroll events counter by Corbacho (@dcorb) on CodePen.
当使用触控板,滚动滚轮,或者拖拽滚动条的时候,一秒可以轻松触发30次事件。经我的测试,在智能手机上,慢慢滚动一下,一秒可以触发事件100次之多。这么高的执行频率,你的滚动回调函数压力大吗?
早在2011年,Twitter 网站抛出了一个问题:向下滚动 Twitter 信息流的时候,变得很慢,很迟钝。John Resig 发表了一篇博客解释这个问题,文中解释到直接给 scroll
事件关联昂贵的函数,是多么糟糕的主意。
John(5年前)建议的解决方案是,在 onScroll
事件外部,每 250ms 循环执行一次。简单的技巧,避免了影响用户体验。
现如今,有一些稍微高端的方式处理事件。我来结合用例介绍下 Debounce,Throttle 和 requestAnimationFrame 吧。
]]>原文: Say Hello To ES2015
笔记:涂鸦码龙
ES2015 是新版的 JavaScript,Node.js 已经完全支持,浏览器端可以用 Babel 库编译。
运行本文的示例代码,可以用 JSBin 环境,也可以结合原文中的测试题检测学习效果。
ES2015 可以用 const
或 let
替换 var
,它们定义了块级作用域变量。
示例代码:
|
|
变量 value
只能在 for
循环中使用。
const
跟 let
很像,但是它定义的变量值无法改变。
示例代码:
|
|
改变的是变量 user
内部的属性,并没有改变 user
本身。
许多人更喜欢用 const
代替 let
。
熟悉的方式:
|
|
使用箭头函数以后:
|
|
如果方法接受不止1个参数,可以这么写:
|
|
箭头函数返回一个对象的话,需要加圆括号:
|
|
以前的代码:
|
|
可以用箭头函数代替 self=this
:
|
|
setInterval
里面用了箭头函数,它携带了 start
方法的上下文(this)
。
使用箭头函数要多加小心,并不是随处可用的,箭头函数会携带函数定义时的上下文。
正如这个例子:https://jsbin.com/zuseqap/edit?js,console
在对象里面定义一个方法,可以这么写:
|
|
不必每次都写 function
关键字。
这是最酷的特性,你会喜欢的:
|
|
瞅瞅多简单,并不用这么啰嗦:
|
|
很容易地提取 user
对象的 name
和 age
字段:
|
|
对于函数相当有用,上代码:
|
|
不仅简化了代码,而且可以自描述。看到函数第一行时,我们便会明白使用传入对象的哪个字段。
可以定义传入对象的默认值。
|
|
像传入对象一样,同样可以从传入的数组中解构值:
|
|
以前的代码:
|
|
ES2015 代码:
|
|
在 sumAndLog
方法中使用 spread
操作符(...
),可以很简单地把所有参数存入 args
变量,然后再用 spread
操作符把 args
传入 sum
方法。
再看以下例子:
|
|
以往都是用 underscore 或者 lodash,克隆、合并对象:
|
|
ES2015 不需要任何工具库,轻松实现以上功能。
|
|
看以下例子:
|
|
尽管我们克隆了对象,但不是深度克隆,只克隆了顶层字段,emails 数组字段使用的仍是同一个。
往数组里添加元素
跟对象类似,我们同样可以克隆数组:
|
|
这些日子,JavaScript 也兴起函数式编程的概念。因此,我们可以尝试写写纯函数。
纯函数:一个函数接收一些值,并且返回一些值,但是通过参数接收到的值不会被改变。 同样的输入总是返回同样的值。
random() 就不是一个纯函数,任何可以修改全局状态的函数都不能称之为纯。
用 spread
操作符可以轻松实现。
用于对象:
|
|
用于数组:
|
|
合并字符串通常很烦,可以用 + 操作符,或者类似 underscore
的模板。
ES2015 的模板字符串,非常简单,看例子:
|
|
注意 “`” 的使用。
既然支持模板字符串,多行字符串也不在话下:
|
|
没有模板字符串的话,是这个样子的:
|
|
JavaScript 并不是真正的面向对象语言,但是可以用函数和原型模拟类。ES2015 可以写真正原生的类了。
|
|
继承一个类:
|
|
小汽车继承了车辆:
super constructor
(Vehicle 的 constructor)。super.display()
。这里展示了子类如何继承方法。
|
|
Car 类没有实现 constructor 的话,它会用 Vehicle 的 constructor 。
ES2015 的模块系统很像 CommonJS 模块系统(或者 Node.js 的模块系统),但是有一点主要的区别:
所有的模块导入应该是静态的,无法在运行时导入模块。编译时间应该完成导入(或者最好在解释 JavaScript 期间完成)。
以下代码在 ES2015 模块里无法使用:
|
|
定义一个简单的导出函数 sum
:
|
|
然后导入它:
|
|
导入多个函数:
|
|
像函数一样,可以导出任何类型的变量,包括类。
|
|
有时需要导出一个独立的模块,叫做默认导出。
|
|
可以这么导入:
|
|
如果再导出一个 print
函数,这么写:
|
|
这便是我们如何在同一模块导入 “默认导出” 和 “命名导出” 的方式。
重命名导入,一次导入所有命名导出,以及更多知识,参见 MDN 文档。
至今还没有浏览器完全实现 ES2015 的所有规范。因此,无法直接在浏览器里使用 ES2015 。
那么我们该怎么做?
欢迎来到 transpiling 的世界。
现在可以按 ES2015 写代码,然后使用一个工具把它转换成 ES5,最有名的一个 transpiler 便是 Babel。
设置 Babel 并没那么简单,需要一定的经验,这是一些新手包,拿去用吧。
此外,可以使用 Meteor,默认支持 ES2015 了。
深入研究,可以参考以下链接:
]]>原文: Say Hello To ES2015
笔记:涂鸦码龙
ES2015 是新版的 JavaScript,Node.js 已经完全支持,浏览器端可以用 Babel 库编译。
运行本文的示例代码,可以用 JSBin 环境,也可以结合原文中的测试题检测学习效果。
ES2015 可以用 const
或 let
替换 var
,它们定义了块级作用域变量。
示例代码:
|
|
变量 value
只能在 for
循环中使用。
const
跟 let
很像,但是它定义的变量值无法改变。
示例代码:
|
|
改变的是变量 user
内部的属性,并没有改变 user
本身。
许多人更喜欢用 const
代替 let
。
熟悉的方式:
|
|
使用箭头函数以后:
|
|
如果方法接受不止1个参数,可以这么写:
|
|
箭头函数返回一个对象的话,需要加圆括号:
|
|
原文:‘flex-grow’ is weird. Or is it?
翻译:涂鸦码龙
当我刚接触 flex-grow
时,为了探寻它的工作原理,做了一个简单的例子。
我以为理解的挺透彻了,但是当我把它应用到同事的网站上时,效果跟我想象的完全不同。无论怎么改,布局都无法像我的demo那样展示。这时我才意识到,我并没有完全掌握 flex-grow
。
在我深入剖析 flex-grow
的功能之前,我想解释一下我起初犯了什么错。
我认为所有 flex 元素的 flex-grow
如果设置为 1
,它们将一样宽。如果某一项的 flex-grow
设置为 2
,它将是其它元素的二倍宽。
一切听起来顺理成章。我上面的例子 貌似也印证了这点。父元素是900px宽,flex-grow: 2
的 section 元素计算后是600px宽,flex-grow: 1
的 aside 元素计算后是300px宽。
如你所见,它在这个例子中展现的近乎完美,可是在真实的例子中却不尽人意,即使我们用了完全相同的 CSS。事实证明,问题不在 CSS,而在于内容(或者说缺乏内容)。我的测试用例只用了两个空元素,无法展示这个属性最重要的细节。
啰嗦了半天,我终于要解释 flex-grow
没有尽如人意的原因了。
为了阐明原因,我又搞了个栗子 ,所有的设置跟第一个栗子 完全一致,只不过 section 和 aside 元素不再是空的。看吧,两个元素的比例不再是 2 : 1,flex-grow 为 1 的元素的确比 flex-grow 为 2 的元素宽不少呐。
如果给父元素设置了 display: flex;
,子元素仅仅是水平排列,没有其它效果了。如果没有足够的空间,它们会收缩一些尺寸。另一方面,如果有足够的空间,它们也不会扩展,因为 Flexbox 希望我们自己定义扩展多少。flex-grow 恰恰用来定义剩余空间如何分配,每一项分享多大宽度。
换言之:
flex 容器为它的子元素分配剩余空间(它们的扩展系数是成比例的),从而填满整个容器,或者收缩元素(它们的收缩系数也是成比例的),从而阻止溢出。
https://drafts.csswg.org/css-flexbox/#flexibility
如果我们可视化一把,概念就很清晰明了了。
首先,我们给父元素设置了 display: flex
,然后它的子元素成了 flex 元素,一个挨一个的横向排列。
下一步,我们要决定每个元素能获得多少剩余空间。先前的例子中,第一个元素获得了 2/3 的剩余空间(flex-grow: 2
),第二个元素获得了 1/3 的剩余空间(flex-grow: 1
)。想知道 flex-grow 总共的值是多少,看看剩余空间被分成了几份吧。
最终我们得出了可分配的块数,根据 flex-grow
的值,每个元素可以获得适当的块数。
理论和视觉展示都不错,让我们为上面例子 做点算术吧。
我们需要4组数字:父容器宽度, section 和 aside 元素的初始宽度,以及 flex-grow 的总值。
父容器宽:900px
section 宽:99px
aside 宽:623px
flex-grow 总值:3
1. 首先计算剩余空间
父容器宽减去每一个子元素的初始宽度。
|
|
父容器宽 - section 宽 - aside 宽 = 剩余空间
2. 然后计算 flex-grow 的1份是多宽
既然有了剩余空间,我们还需要确定把它切成几份。重要的是,我们不按元素的个数切分剩余空间,而是按 flex-grow 总值,所以这里是 3 (flex-grow: 2
+ flex-grow: 1
)
|
|
剩余空间 / flex-grow 总值 = “1份 flex-grow 的宽”
3. 最终所有的元素瓜分剩余空间
依据它们的 flex-grow 值,section 需要 2 份(2 59.33),aside 需要 1 份(1 59.33)。这个数字再与每个元素的初始宽度相加。
|
|
初始 section 宽度 + (section 的 flex-grow 值 * “1 份 flex-grow 的宽”) = 新的宽度
|
|
初始 aside 宽度 + (aside 的 flex-grow 值 * “1 份 flex-grow 的宽”) = 新的宽度
so easy,不是吗?
我们按已有的公式,算下第一个例子 。
父容器宽:900px
section 宽:0px
aside 宽:0px
flex-grow 总值:3
1. 计算剩余空间
|
|
2. 计算 flex-grow 的1份是多宽
|
|
3. 分配剩余空间
|
|
如果每个元素的宽是 0,剩余空间等于父容器的宽度,因此,看起来像是 flex-grow
按比例划分了父容器的宽度。
快速回顾一下:剩余空间被 flex-grow
的总值划分,由此产生的商,乘以各自的 flex-grow
值,结果再加上每个元素的初始宽度。
但是如果没有剩余空间或者不想依赖元素的初始宽度,我们可以设置它吗?还能用 flex-grow
吗?
当然可以。有个 flex-basis
属性,它可以定义元素的初始宽度。如果 flex-basis
和 flex-grow
一起设置,宽度的计算方式得变一下。
<‘flex-basis’>
:按 flex 因子分配剩余空间之前,每个 flex 元素的最初主要尺寸。
https://drafts.csswg.org/css-flexbox/#valdef-flex-flex-basis
如果给某个元素设置了 flex-basis
属性,我们计算的时候就不能再用元素本身的初始宽度了,而要用 flex-basis
属性的值。
我调整了一下先前的例子 ,给每个元素加了 flex-basis
属性。
父容器宽:900px
section 宽:400px(flex-basis 值)
aside 宽:200px(flex-basis 值)
flex-grow 总值:3
1. 计算剩余空间
|
|
2. 计算 flex-grow 的1份是多宽
|
|
3. 分配剩余空间
|
|
仅仅是为了完整性,我才用到 px 值,用百分比的话当然也没问题 。
为了覆盖所有情况,如果我们加上 padding 和 margin 看看会发生什么,啥也没发生 ,第一步计算的时候,只要减去 margin 就好了。
唯一需要注意的是,使用 box-sizing
的话, flex-basis
跟 width
属性表现相似。如果 box-sizing 属性改变,计算结果也会变化。如果 box-sizing
设置为 border-box
,计算时只需用到 flex-basis
和 margin
值,因为 padding
已经包含到宽度里面了。
好吧,算术是玩够了。我再展示一些项目中合理使用 flex-grow
的例子吧。
实际应用中,剩余空间是自动分配的,如果想让子元素填满父容器的话,我们没必要再考虑宽度值。
See the Pen flex-grow by Manuel Matuzovic (@matuzo) on CodePen.
固定加自适应宽度的混合布局,可以用浮动实现,但是既不简单直观,又不灵活。Flexbox 实现的话,加点儿 flex-grow 和 flex-basis 魔法,简直小菜一碟。
See the Pen Layout using fluid and fixed widths by Manuel Matuzovic (@matuzo) on CodePen.
比如,一个 label 元素后面紧跟着输入框,想让输入框填满剩余空间,不再需要丑陋的 hacks 。
See the Pen Filling up the remaining space in a form by Manuel Matuzovic (@matuzo) on CodePen.
Philip Waltons 的 Solved by Flexbox 一文可以找到更多示例。
根据标准,使用 flex
的简写,比直接用 flex-grow
更好。
Authors are encouraged to control flexibility using the flex shorthand rather than flex-grow directly, as the shorthand correctly resets any unspecified components to accommodate common uses.
https://drafts.csswg.org/css-flexbox/#flex-grow-property
但是小心!如果仅仅使用 flex: 1;
,以上某些例子可能无法正常展示。
我们的例子要用 flex
的话,应该这么定义:
|
|
如果你想深入学习 Flexbox,可以看看这些不错的资源:
flex-grow
不易理解吗?也不全是。我们只需理解它如何工作,它做了什么。如果一个元素设置 flex-grow
为 3
,并不代表它是 flex-grow
为 1
的元素的3倍大,准确含义是:它的初始宽度可以增加的像素值是另一个元素的3倍。
我通过两个空元素 测试 flex-grow ,得到的结论跟真实情况 完全不符。应该在尽可能真实的环境中验证新事物,这样才能得到最切合实际的结论。
]]>原文:‘flex-grow’ is weird. Or is it?
翻译:涂鸦码龙
当我刚接触 flex-grow
时,为了探寻它的工作原理,做了一个简单的例子。
我以为理解的挺透彻了,但是当我把它应用到同事的网站上时,效果跟我想象的完全不同。无论怎么改,布局都无法像我的demo那样展示。这时我才意识到,我并没有完全掌握 flex-grow
。
在我深入剖析 flex-grow
的功能之前,我想解释一下我起初犯了什么错。
我认为所有 flex 元素的 flex-grow
如果设置为 1
,它们将一样宽。如果某一项的 flex-grow
设置为 2
,它将是其它元素的二倍宽。
一切听起来顺理成章。我上面的例子 貌似也印证了这点。父元素是900px宽,flex-grow: 2
的 section 元素计算后是600px宽,flex-grow: 1
的 aside 元素计算后是300px宽。
如你所见,它在这个例子中展现的近乎完美,可是在真实的例子中却不尽人意,即使我们用了完全相同的 CSS。事实证明,问题不在 CSS,而在于内容(或者说缺乏内容)。我的测试用例只用了两个空元素,无法展示这个属性最重要的细节。
啰嗦了半天,我终于要解释 flex-grow
没有尽如人意的原因了。
为了阐明原因,我又搞了个栗子 ,所有的设置跟第一个栗子 完全一致,只不过 section 和 aside 元素不再是空的。看吧,两个元素的比例不再是 2 : 1,flex-grow 为 1 的元素的确比 flex-grow 为 2 的元素宽不少呐。
]]>原文:Moving along a curved path in CSS with layered animation
翻译:涂鸦码龙
译者注:部分代码示例在原文中可以看效果(作者写在博文里面了…),我偷懒把它做成Gif图了。
CSS 的 animations (动画) 和 transitions(变换)擅于实现从点 A 到点 B 的直线运动,运动轨迹是直线路径。给一个元素添加了 animation
或者 transition
以后,无论你如何调整贝塞尔曲线,都无法让它沿着弧形路径运动。你可以通过自定义 timing function 属性,做出弹动的效果,但是它沿着 X 和 Y 轴相对移动的值永远是相同的。
与其使用 JavaScript 实现外观自然的运动,不如尝试用这种简单的方式:分层动画,绕过已有的限制。通过使用两个或多个元素实现动画效果,我们可以更加细粒度地控制某个元素的路径,沿着 X 轴运动使用一种 timing function ,沿着 Y 轴运动使用另一种 timing function 。
当我们深入探讨解决方案之前,看看到底问题在哪。CSS animations
和 transitions
限制我们只能沿直线路径运动。元素总是沿着点 A 到点 B 的最短路径运动,如果我们另辟蹊径,告诉 CSS 沿着“更好的路径”,而不是“最短路径”运动呢?
用 CSS (开启硬件加速)实现两点之间的运动,最直截了当的方式是使用 transform
的 translate
在一定时间内移动某个元素。这就产生了直线运动。在 @keyframes
中,我们打算在 (0,0) 和 (100,-100) 间来回运动,见上图例子:
|
|
这些看起来并不难懂,但我们稍等片刻,思考一下我们需要的解决方案,拆分开来的动画,视觉上长什么样子呢。
0%
时,元素从 (0,0) 出发,50%
时,我们用了 translate3D(100px, -100px, 0)
把它移动到 (100,-100),然后原路返回。换句话说,我们把元素向右移动了 100px
,向上移动了 100px
,两个方向联合作用使元素沿着一个角度运动。
那么,原先展示的例子中我们如何实现的弧形路径呢?为了让创建的路径不是直线,我们想让元素沿 X 轴和 Y 轴的运动速度不同步。
先前例子中都用到了 linear
线性运动函数,如果我们给运动的元素包裹一个容器,我们可以为 X 轴应用一种动画函数,Y 轴应用另一种动画函数。以下例子,我们在 X 轴使用 ease-in
,Y 轴使用 ease-out
。
不幸的是,我们不能只把 transform
动画简单叠加:因为只有最后声明的动画会执行。那么我们如何把两个动画效果联合起来呢?可以把一个元素放入另一个元素内部,给容器元素加一种动画,给里面的子元素添加另一种动画。
在以上例子中,你已经看到一个点沿着弧形路径运动,看到两个独立的元素一起做动画,只不过容器元素是完全透明的。为了清晰地看到两个元素沿着弧形路径是如何相互作用的,我们给容器元素加个边框看看呗:
那个点藏在带边框的盒子内部,跟随盒子一起沿 X 轴远动,同时它自己又在 Y 轴方向上下运动。去掉容器盒子的边框,我们就得到了弧形路径。与其在 HTML 中用两个元素,还不如用伪元素实现嘞。如果 HTML 是这样:
|
|
我们可以添加伪元素:
|
|
然后,我们需要两块独立的动画代码:X 轴,Y 轴各一块。注意一处用了 ease-in
,另一处用了 ease-out
:
|
|
加上 WebKit 前缀,用一些自定义的贝塞尔曲线代替 ease-in
和 ease-out
,我们就可以实现文章最开头展示的效果:
|
|
以下是文章起始处的例子:
你可能注意到我们在所有例子中都用了 @keyframes
,这纯粹是因为我们想展示黑点儿往返的两种状态。如果只想实现点 A 至点 B 的运动,使用 transition
属性做分层动画同样好用。
如果有个绝对定位的元素,通过给 left
和 bottom
属性加特效,就可以实现弧形路径运动,单个元素就可以,不需要容器元素。为什么不这么做呢:它性能稍差一些,动画的每一帧都会引起重绘。使用带伪元素的分层动画,translate
属性又开了硬件加速,动画效果更好,性能也更高。
]]>译者自己搞了个绝对定位的例子:
原文:Moving along a curved path in CSS with layered animation
翻译:涂鸦码龙
译者注:部分代码示例在原文中可以看效果(作者写在博文里面了…),我偷懒把它做成Gif图了。
CSS 的 animations (动画) 和 transitions(变换)擅于实现从点 A 到点 B 的直线运动,运动轨迹是直线路径。给一个元素添加了 animation
或者 transition
以后,无论你如何调整贝塞尔曲线,都无法让它沿着弧形路径运动。你可以通过自定义 timing function 属性,做出弹动的效果,但是它沿着 X 和 Y 轴相对移动的值永远是相同的。
与其使用 JavaScript 实现外观自然的运动,不如尝试用这种简单的方式:分层动画,绕过已有的限制。通过使用两个或多个元素实现动画效果,我们可以更加细粒度地控制某个元素的路径,沿着 X 轴运动使用一种 timing function ,沿着 Y 轴运动使用另一种 timing function 。
当我们深入探讨解决方案之前,看看到底问题在哪。CSS animations
和 transitions
限制我们只能沿直线路径运动。元素总是沿着点 A 到点 B 的最短路径运动,如果我们另辟蹊径,告诉 CSS 沿着“更好的路径”,而不是“最短路径”运动呢?
用 CSS (开启硬件加速)实现两点之间的运动,最直截了当的方式是使用 transform
的 translate
在一定时间内移动某个元素。这就产生了直线运动。在 @keyframes
中,我们打算在 (0,0) 和 (100,-100) 间来回运动,见上图例子:
|
|
这些看起来并不难懂,但我们稍等片刻,思考一下我们需要的解决方案,拆分开来的动画,视觉上长什么样子呢。
0%
时,元素从 (0,0) 出发,50%
时,我们用了 translate3D(100px, -100px, 0)
把它移动到 (100,-100),然后原路返回。换句话说,我们把元素向右移动了 100px
,向上移动了 100px
,两个方向联合作用使元素沿着一个角度运动。
原文:Checkbox Trickery with CSS
翻译:涂鸦码龙
Checkbox 复选框相当好用,加对 CSS 魔法有奇效。此文旨在展示一些利用 checkbox 实现的有创意的东西,并且文中的例子没用 JavaScript 哟。
从 HTML 着手。
|
|
此处无技巧可言。<label>
的 for
属性匹配 <input>
的 id
属性,因此点击 <label>
可以控制 <input>
复选框。这点尤其重要,因为下一步将隐藏 <input>
。
|
|
为什么不用 display: none
?因为屏幕阅读机和键盘 Tab 会忽略它。此方法让 <input>
保持在文档流中,但是让它离屏隐藏(超出屏幕可见范围达到隐藏)。
隐藏 <input>
以后,我们更容易大展身手。我们仍需传达选中/未选两种状态,但是可以通过 <label>
完成。真正的派对开始啦。
|
|
我们使用 :checked
伪类, 和相邻兄弟元素选择器(+
)的组合达到目的,当复选框选中时,找到紧随其后的 <label>
元素,加上想要的样式。还可以利用 <label>
中的伪元素( ::before
和 ::after
)实现更有创意的想法。
|
|
来,看看实际效果吧。例子用到了以上提及的基本配方,把一个普普通通的复选框改造得当人眼前一亮。
See the Pen Checkbox Trickery: Simple Toggle by Will Boyd (@lonekorean) on CodePen.
最大的好处是,包含在 <form>
中的复选框的值仍然可以被提交。我们只改变了外观,并没有影响功能。
目前为止,我们都是给 <label>
加样式,我们可以更进一步。这个例子会根据用户的选择,动态地隐藏/显示表单的部分内容。
See the Pen Checkbox Trickery: Form Disclosure by Will Boyd (@lonekorean) on CodePen.
:checked
伪类对单选按钮同样奏效,考虑到这一点,“How did you hear about us?”这块的 HTML 用到了单选按钮。
|
|
单选按钮指示器利用 <label>
内的 ::before
伪元素(外部的圆环)和 ::after
伪元素(内部的绿点)实现,当单选按钮选中 / 未选时显示 / 隐藏 ::after
伪元素相当简单。
|
|
<div>
一直隐藏,直到“Other…”单选按钮选中时才显示。我使用了 display: none
隐藏 <div>
,因为这次我确实想让屏幕阅读器和键盘 Tab 在内容隐藏时忽略它。当单选按钮选中时,利用 CSS 显示 <div>
。
|
|
之前我们一直使用相邻兄弟选择器(+
),不过这次得用一般兄弟选择器(~
)。它俩很相似,但是可以找到非相邻的兄弟元素,比如我们的 <div>
。
我们可以活用之前例子中的技巧,实现一个树状文件夹组件,同样具备隐藏 / 显示两种功能。
See the Pen Checkbox Trickery: Folder Tree by Will Boyd (@lonekorean) on CodePen.
单个文件夹的 HTML 如下:<label>
是文件夹,两个 <a>
元素是文件夹里的文件。
|
|
Font Awesome 图标用于表示选中(打开)和未选(关闭)状态。
|
|
文件夹里的内容通过一般兄弟选择器(~
)实现显示 / 隐藏。这就是 HTML 外面包裹额外 <div>
的原因,为了确保选择器能够选到元素,打开兄弟文件夹。
|
|
理所当然,文件夹可以嵌套。只需把另一个文件夹的 HTML 放入 <div class="sub">
即可。点击 “Multicolor”看看效果吧。
最后,我们聊一下重置按钮。
|
|
表单的重置按钮很少有人用,不过这里巧用了一下。点击重置按钮,所有复选框恢复成初始的未选中状态,关闭了所有文件夹。有意思吧。
这个例子依据已做或未做,把列表选项分成两部分。
See the Pen Checkbox Trickery: To-Do List by Will Boyd (@lonekorean) on CodePen.
HTML 像这样:
|
|
分隔列表是由 CSS flexbox 实现的,这是关键的 CSS。
|
|
CSS flexbox 可以使用 order
属性重排元素。当复选框选中的时候,<label>
的 order 值由 4
变为 2
,列表选项就从 “Not Done” <h2>
下面移到了 “Done” <h2>
下面。
不幸的是,键盘导航和许多屏幕阅读器 会遵循元素在 DOM 中的顺序,即使它们被 CSS flexbox 做了视觉上的重排。这就导致 “Done”和“Not Done”的标题对于屏幕阅读器无用,这便是我给它们加了 aria-hidden="true"
的原因 —— 被忽略总比引起混淆强。此外,通过键盘和屏幕阅读器完全可以控制分隔列表,正确显示列表项的状态(选中 / 未选)。
如果你对“Done”和“Not Done”其后的计数实现感到好奇,它们用到了 CSS counters。想深入学习的话,可以看这篇文章。
压轴的例子展示了如何根据筛选条件,高亮显示交叉区域的数据。
See the Pen Checkbox Trickery: Group Filter by Will Boyd (@lonekorean) on CodePen.
这是简短的 HTML 。注意一下 data-teams
属性是由空格分隔的列表,它们每一项恰恰与单选按钮的 id
属性一致。我就是这么把角色跟队伍匹配起来的。
|
|
关于可访问性,我使用了空的 alt
属性,因为角色的名字已显示在 <h2>
标签中了 —— 每个名字读两次也没有必要。此外,我没有真正的隐藏 <img>
元素(只是收缩和渐隐),屏幕阅读器可以轻易的跳过未高亮的角色。我仅仅需要隐藏 <h2>
。
当选择团队的时候,高亮对应的角色,CSS 是这么实现的。
|
|
我清楚这些选择器看起来有点吓人,不过也不赖。我们来仔细分析下例子的第一行(译者注:以 CSS 中最长的选择器开始算)。当 id
为 ‘original’ 的元素被选中时,找到角色元素里面 data-teams
属性包含 ‘original’ 的元素,然后找到它里面的 <h2>
。2-4行的 ‘force’, ‘factor’, 和 ‘hellfire’ 重复上一步。8-11行继续重复上一步,只不过把 <h2>
换成了 <img>
。
我希望你跟我一样从这些例子中找到了一些乐趣。通过复选框去实现一些好玩的东西,对我而言非常有趣。我并没有排斥在适当的时候使用 JavaScript ,但是没用它也可以实现这么多东西,也是蛮开心的。谢谢阅读!
]]>原文:Checkbox Trickery with CSS
翻译:涂鸦码龙
Checkbox 复选框相当好用,加对 CSS 魔法有奇效。此文旨在展示一些利用 checkbox 实现的有创意的东西,并且文中的例子没用 JavaScript 哟。
从 HTML 着手。
|
|
此处无技巧可言。<label>
的 for
属性匹配 <input>
的 id
属性,因此点击 <label>
可以控制 <input>
复选框。这点尤其重要,因为下一步将隐藏 <input>
。
|
|
为什么不用 display: none
?因为屏幕阅读机和键盘 Tab 会忽略它。此方法让 <input>
保持在文档流中,但是让它离屏隐藏(超出屏幕可见范围达到隐藏)。
隐藏 <input>
以后,我们更容易大展身手。我们仍需传达选中/未选两种状态,但是可以通过 <label>
完成。真正的派对开始啦。
|
|
我们使用 :checked
伪类, 和相邻兄弟元素选择器(+
)的组合达到目的,当复选框选中时,找到紧随其后的 <label>
元素,加上想要的样式。还可以利用 <label>
中的伪元素( ::before
和 ::after
)实现更有创意的想法。
|
|
来,看看实际效果吧。例子用到了以上提及的基本配方,把一个普普通通的复选框改造得当人眼前一亮。
]]>
原文:How to Schedule Background Tasks in JavaScript
翻译:涂鸦码龙
即使忘了 JavaScript 的一切知识,也不会忘记:它是阻塞的。
想象一下,你的浏览器里住着一个魔法小精灵,负责浏览器的正常运转。不论渲染 HTML,响应菜单命令,屏幕渲染,处理鼠标点击,或者执行 JavaScript 函数,所有事情都归一个小精灵处理。它哪忙得过来,一次只能处理一件事情。如果同时丢给它一堆任务,它会列一个长长的待办列表,按顺序完成它们。
人们常常希望初始化组件和事件处理的 JavaScript 可以尽快被执行。可是,有些不太重要的后台任务不会直接影响用户体验,比如:
他们对时序要求不严格,但是为了让页面仍然响应,直到用户滚动页面或者与内容交互时才被执行。
选择之一是 Web Workers ,它可以在独立的线程同时执行代码。用于预加载和预处理再好不过,但是你没有权限直接访问或更新 DOM。你可以在自己的代码中避开这点,但是无法保证第三方脚本比如 Google Analytics 永远不需要这个。
另一个选择是 setTimeout
,比如 setTimeout(doSomething, 1);
。一旦其它的立即执行任务执行完毕,浏览器将执行 doSomething()
函数。实际上,它被放到了待办列表的底部。不幸的是,函数将被调用,而不顾处理需求。
requestIdleCallback 是新API,当浏览器稍作喘息的时候,用来执行不太重要的后台计划任务。 难免让人想起 requestAnimationFrame,在下次重绘之前,执行函数更新动画。 想了解更多戳这里:使用 requestAnimationFrame 做简单的动画。
requestIdleCallback
特性监测:
|
|
也可以指定配置参数对象,比如 timeout,
|
|
确保函数在3秒之内调用,不管浏览器是否空闲。
deadline
对象传入以下参数时,requestIdleCallback
仅执行一次回调:
didTimeout
—— 如果可选的 timeout 触发,则设置为 truetimeRemaining()
—— 函数返回执行任务剩余的毫秒数timeRemaining()
最多分配50ms用于任务的执行,超过这个限制,也不会停止任务,但是,最好重新调用 requestIdleCallback
安排进一步的处理。我们来创建一个简单的例子,让几个任务按序执行。任务的函数引用储存在数组中:
|
|
Paul Lewis 在他的文章中提到,一次 requestIdleCallback 执行的任务应该切成小块。它不适用于不可预知时间的情况(比如操作 DOM,使用 requestAnimationFrame 回调更好些)。resolving(或者 rejecting)Promises 时也要谨慎,即使没有更多的剩余时间,空闲回调完成之后,回调函数也将立即执行。
requestIdleCallback
是试验性特性,规范仍不稳定,碰到 API 变更时不足为奇。Chrome 47 已支持… 2015年结束前应该可用了。Opera 应该会紧跟其后。Microsoft 和 Mozilla 都在考虑 API 是否应该支持 Promises 。Apple 像往常一样不鸟。
Paul Lewis(上文提到的)写了一个简单的 requestIdleCallback shim ,它可以模拟浏览器的空闲监测行为,但不是一个 polyfill(shim 和 polyfill 的区别)。
requestIdleCallback shim代码如下:
|
|
原文:How to Schedule Background Tasks in JavaScript
翻译:涂鸦码龙
即使忘了 JavaScript 的一切知识,也不会忘记:它是阻塞的。
想象一下,你的浏览器里住着一个魔法小精灵,负责浏览器的正常运转。不论渲染 HTML,响应菜单命令,屏幕渲染,处理鼠标点击,或者执行 JavaScript 函数,所有事情都归一个小精灵处理。它哪忙得过来,一次只能处理一件事情。如果同时丢给它一堆任务,它会列一个长长的待办列表,按顺序完成它们。
人们常常希望初始化组件和事件处理的 JavaScript 可以尽快被执行。可是,有些不太重要的后台任务不会直接影响用户体验,比如:
他们对时序要求不严格,但是为了让页面仍然响应,直到用户滚动页面或者与内容交互时才被执行。
选择之一是 Web Workers ,它可以在独立的线程同时执行代码。用于预加载和预处理再好不过,但是你没有权限直接访问或更新 DOM。你可以在自己的代码中避开这点,但是无法保证第三方脚本比如 Google Analytics 永远不需要这个。
]]>
原文:Negating Predicate Functions in JavaScript
翻译:涂鸦码龙
如果你从没听过这个概念,那么至少用过谓语函数。谓语本质上是一个函数,根据它的参数,可以判断返回结果是 true
或者 false
。一般都以“isX”命名,比如 isEven
或者 isNumber
。
假如我们有个程序,需要处理漫画书里的英雄和坏蛋,以下面的简单对象为例:
|
|
程序中难免会用到一些谓语,像这些:
|
|
如你所见,代码有些冗余。问题不是代码太长,而是核心逻辑定义了两次(每一对谓语,“is”和“isNot”)。逻辑重复意味着逻辑改变时,可能只更新了其中一处谓语,导致错误发生。
如何解决呢?我们首先想到的是这样改:
|
|
的确有进步,但是仍有冗余。“isNot”谓语只不过颠倒了“is”谓语的功能。
我们何不抽象成一个更清晰,更易维护的函数呢?
|
|
negate
函数接收一个谓语函数作为参数,返回一个函数用来对之前谓语的功能取反。
(如果对 apply
不了解,可以读读这篇《Invoking JavaScript Functions With ‘call’ and ‘apply’》)
使用 negate
函数解决我们先前的问题吧。
|
|
程序如期运行,但是我们把核心逻辑放到了一处,从“is”谓语也很容易推导出“isNot”谓语。
这点小小的重构看起来微不足道,但是应用于复杂系统的许多分散函数时,negate
函数可以使程序更易维护。
Underscore 和 Lo-Dash 都已经填加了 negate
函数。
原文:Negating Predicate Functions in JavaScript
翻译:涂鸦码龙
如果你从没听过这个概念,那么至少用过谓语函数。谓语本质上是一个函数,根据它的参数,可以判断返回结果是 true
或者 false
。一般都以“isX”命名,比如 isEven
或者 isNumber
。
假如我们有个程序,需要处理漫画书里的英雄和坏蛋,以下面的简单对象为例:
|
|
程序中难免会用到一些谓语,像这些:
|
|
两种方式:
提示:跟上游仓库同步代码之前,必须配置过 remote,指向上游仓库 。
|
|
从上游仓库获取到分支,及相关的提交信息,它们将被保存在本地的 upstream/master
分支
|
|
切换到本地的 master
分支
|
|
把 upstream/master
分支合并到本地的 master
分支,本地的 master
分支便跟上游仓库保持同步了,并且没有丢失你本地的修改。
|
|
提示:同步后的代码仅仅是保存在本地仓库,记得 push
到 Github 哟。
盗几张知乎的图,见图知意。
这一页往下面拉:
两种方式:
原文:What forces layout / reflow
笔记:涂鸦码龙
注:本文只摘取了自己认为重要的知识点,并没有逐字逐句翻译
当在 JavaScript 中调用(requested/called)以下所有属性或方法时,浏览器将会同步地计算样式和布局。重排(也有叫 reflow 或 layout thrashing 的),通常是性能瓶颈。
window.getComputedStyle()
通常会引起样式重新计算(源码)
如果以下任何一种情况存在,window.getComputedStyle()
将会引起重排:
相当多;没有详尽的清单,但是 Tony Gentilcore’s 2011 Layout Triggering List 指出了一些。
数不胜数,…包含复制图片到剪切板(源码)
CSS Triggers 是非常棒的参考资源。设置/改变给出的 CSS 值,在浏览器的生命周期会出现什么结果,会触发哪些操作都有展示。
原文:What forces layout / reflow
笔记:涂鸦码龙
注:本文只摘取了自己认为重要的知识点,并没有逐字逐句翻译
当在 JavaScript 中调用(requested/called)以下所有属性或方法时,浏览器将会同步地计算样式和布局。重排(也有叫 reflow 或 layout thrashing 的),通常是性能瓶颈。
好的 API 设计:在自描述的同时,达到抽象的目标。
设计良好的 API ,开发者可以快速上手,没必要经常抱着手册和文档,也没必要频繁光顾技术支持社区。
方法链:流畅易读,更易理解
|
|
设置和获取操作,可以合二为一;方法越多,文档可能越难写
|
|
相关的接口保持一致的风格,一整套 API 如果传递一种熟悉和舒适的感觉,会大大减轻开发者对新工具的适应性。
命名这点事:既要短,又要自描述,最重要的是保持一致性
“There are only two hard problems in computer science: cache-invalidation and naming things.”
“在计算机科学界只有两件头疼的事:缓存失效和命名问题”
— Phil Karlton
选择一个你喜欢的措辞,然后持续使用。选择一种风格,然后保持这种风格。
需要考虑大家如何使用你提供的方法,是否会重复调用?为何会重复调用?你的 API 如何帮助开发者减少重复的调用?
接收map映射参数,回调或者序列化的属性名,不仅让你的 API 更干净,而且使用起来更舒服、高效。
jQuery 的 css()
方法可以给 DOM 元素设置样式:
|
|
这个方法可以接受一个 JSON 对象:
|
|
定义方法的时候,需要决定它可以接收什么样的参数。我们不清楚人们如何使用我们的代码,但可以更有远见,考虑支持哪些参数类型。
|
|
加了短短的6行代码,我们的方法强大到可以接收 Date
对象,数字的时间戳,甚至像 Sat Sep 08 2012 15:34:35 GMT+0200 (CEST)
这样的字符串
如果你需要确保传入的参数类型(字符串,数字,布尔),可以这样转换:
|
|
为了使你的 API 更健壮,需要鉴别是否真正的 undefined
值被传递进来,可以检查 arguments
对象:
|
|
|
|
Event.initMouseEvent 这个方法简直丧心病狂,不看文档的话,谁能说出每个参数是什么意思?
给每个参数起个名字,赋个默认值,可好
|
|
ES6, 或者 Harmony 就有 默认参数值 和 rest 参数 了。
与其接收一堆参数,不如接收一个 JSON 对象:
|
|
调用起来也更简单了:
|
|
参数最好有默认值,通过 jQuery.extend() http://underscorejs.org/#extend) 和 Protoype 的 Object.extend ,可以覆盖预设的默认值。
|
|
通过回调, API 用户可以覆盖你的某一部分代码。把一些需要自定义的功能开放成可配置的回调函数,允许 API 用户轻松覆盖你的默认代码。
API 接口一旦接收回调,确保在文档中加以说明,并提供代码示例。
事件接口最好见名知意,可以自由选择事件名字,避免与原生事件 重名。
不是所有的错误都对开发者调试代码有用:
|
|
这样的错误调试起来很痛苦,不要浪费开发者的时间,直接告诉他们犯了什么错:
|
|
备注:
typeof callback === "function"
在老的浏览器上会有问题,object
会当成个function
。
好的 API 具有可预测性,开发者可以根据例子推断它的用法。
Modernizr’s 特性检测 是个例子:
a) 它使用的属性名完全与 HTML5、CSS 概念和 API 相匹配
b) 每一个单独的检测一致地返回 true 或 false 值
|
|
依赖于开发者已熟悉的概念也可以达到可预测的目的。
jQuery’s 选择器语法 就是一个显著的例子,CSS1-CSS3 的选择器可直接用于它的 DOM 选择器引擎。
|
|
好的 API 并不一定是小的 API,API 的体积大小要跟它的功能相称。
比如 Moment.js ,著名的日期解析和格式化的库,可以称之为均衡,它的 API 既简洁又功能明确。
像 Moment.js 这样特定功能的库,确保 API 的专注和小巧非常重要。
软件开发最艰难的任务之一是写文档,实际上每个人都恨写文档,怨声载道的是没有一个好用的文档工具。
以下是一些文档自动生成工具:
最重要的是:确保文档跟代码同步更新。
]]>参考资料:
好的 API 设计:在自描述的同时,达到抽象的目标。
设计良好的 API ,开发者可以快速上手,没必要经常抱着手册和文档,也没必要频繁光顾技术支持社区。
方法链:流畅易读,更易理解
|
|
前些日子总被人问起 iOS Retina 屏,设置 1px 边框,实际显示 2px,如何解决?
原来一直没在意,源于自己根本不是像素眼……
今天仔细瞅了瞅原生实现的边框和CSS设置的边框,确实差距不小……
上图是原生实现,下图是 CSS 边框,手机上对比更加明显
然后,如何解决呢?搜遍整个谷歌,发现好多人早已开始研究解决方案了。到底有哪些方案,到底好不好用呢?试过才知道,把我试过的结论记录一下。
在2014年的 WWDC,“设计响应的Web体验” 一讲中,Ted O’Connor 讲到关于“retina
hairlines”(retina 极细的线):在retina屏上仅仅显示1物理像素的边框,开发者应该如何处理呢。
他们曾介绍到 iOS 8 和 OS X Yosemite 即将支持 0.5px 的边框:
额的神呐!so easy! 果真如此吗?
这样还不够(WWDC幻灯片通常是“唬人”的),但是相差不多。
问题是 retina 屏的浏览器可能不认识0.5px的边框,将会把它解释成0px,没有边框。包括 iOS 7 和 之前版本,OS X Mavericks 及以前版本,还有 Android 设备。
解决方案
解决方案是通过 JavaScript 检测浏览器能否处理0.5px的边框,如果可以,给<html>
元素添加个class
。
|
|
然后,极细的边框样式就容易了:
|
|
看起来是个解决方案,可是我要兼容安卓设备,和 iOS 8 以下设备怎么办?这个思路行不通。
6x6 的 一张图片
可以用 gif,png,或 base64 图片
|
|
缺点是改边框颜色时要改图片,不是很方便。
设置1px的渐变背景,50%有颜色,50%透明
|
|
多写了不少代码,圆角也没法实现,也是不太好用。
|
|
试了下不太好用,颜色不好处理,有阴影出现。
在devicePixelRatio = 2
时,输出viewport
|
|
在devicePixelRatio = 3
时,输出viewport
|
|
同时通过设置对应viewport的rem基准值,这种方式就可以像以前一样轻松愉快的写1px了。
个人感觉为了做个 border 多准备两套样式,还得动态改变 viewport ,有点费劲。
原理是把原先元素的 border 去掉,然后利用 :before
或者 :after
重做 border ,并 transform 的 scale 缩小一半,原先的元素相对定位,新做的 border 绝对定位
|
|
|
|
样式使用的时候,也要结合 JS 代码,判断是否 Retina 屏
|
|
可以支持圆角,唯一的一点小缺陷是 <td>
用不了。
]]>参考资料:
前些日子总被人问起 iOS Retina 屏,设置 1px 边框,实际显示 2px,如何解决?
原来一直没在意,源于自己根本不是像素眼……
今天仔细瞅了瞅原生实现的边框和CSS设置的边框,确实差距不小……
上图是原生实现,下图是 CSS 边框,手机上对比更加明显
然后,如何解决呢?搜遍整个谷歌,发现好多人早已开始研究解决方案了。到底有哪些方案,到底好不好用呢?试过才知道,把我试过的结论记录一下。
]]>原文:《ELIMINATE JAVASCRIPT CODE SMELLS》
作者:@elijahmanor
笔记:涂鸦码农
|
|
Lint 规则
|
|
结果
|
|
|
|
重构后统计
jshint - http://jshint.com/
eslint - http://eslint.org/
jscomplexity - http://jscomplexity.org/
escomplex - https://github.com/philbooth/escomplex
jasmine - http://jasmine.github.io/
已有功能…
已有代码,BOX.js
|
|
那么,现在想要这个功能
于是,Duang! CIRCLE.JS 就出现了…
|
|
为毛是这个味?因为我们复制粘贴了!
检查复制粘贴和结构相似的代码
一行命令:
|
|
程序源码的复制/粘贴检查器
(JavaScript, TypeScript, C#, Ruby, CSS, SCSS, HTML, 等等都适用…)
|
|
把随机颜色部分丢出去…
|
|
再重构
再把怪异的 [].forEach.call 部分丢出去…
|
|
再再重构
|
|
|
|
增加个新形状
|
|
加点设计模式
|
|
再增加新形状时
|
|
|
|
神奇的字符串重构为对象类型
|
|
神奇字符重构为 CONST & SYMBOLS
|
|
木有 :(
ESLINT-PLUGIN-SMELLS
用于 JavaScript Smells(味道) 的 ESLint 规则
规则
|
|
替换方案
1) bind
|
|
替换方案
2) forEach 的第二个参数
|
|
替换方案
3) ECMAScript 2015 (ES6)
|
|
4a) 函数式编程
|
|
4b) 函数式编程
|
|
ESLint
|
|
替换方案
@thomasfuchs 推文上的 JavaScript 模板引擎
|
|
替换方案
2) ECMAScript 2015 (ES6) 模板字符串
|
|
替换方案
3) ECMAScript 2015 (ES6) 模板字符串 (多行)
替换方案
4) 其它小型库或大型库/框架
ESLINT-PLUGIN-SMELLS
no-complex-string-concat
Tweet Sized JavaScript Templating Engine by @thomasfuchs
Learn ECMAScript 2015 (ES6) - http://babeljs.io/docs/learn-es6/
|
|
愉快地重构吧
|
|
最终 Demo
See the Pen pvQQZw by Elijah Manor (@elijahmanor) on CodePen.
ESLINT-PLUGIN-SMELLS
|
|
Demo: setInterval
See the Pen bNQmzP by Elijah Manor (@elijahmanor) on CodePen.
用 setTimeout 保证顺序
|
|
Demo: setTimeout
See the Pen raQQay by Elijah Manor (@elijahmanor) on CodePen.
|
|
替换方案
1) 嵌套调用函数
|
|
2) forEach
3) reduce
4) flow
ESLINT-PLUGIN-SMELLS
|
|
如何改善!?!
消息订阅
依赖注入
|
|
消息订阅
|
|
资源
|
|
Demo: 根本停不下来
See the Pen LEXBdX by Elijah Manor (@elijahmanor) on CodePen.
解决方案:节流阀
Demo:
See the Pen azQjGj by Elijah Manor (@elijahmanor) on CodePen.
解决方案:DEBOUNCE
Demo
See the Pen bNQjje by Elijah Manor (@elijahmanor) on CodePen.
资源
|
|
给函数命名的原因:
代码复用
堆栈追踪
|
|
修改后
单次事件绑定
Demo
See the Pen PwxBxP by Elijah Manor (@elijahmanor) on CodePen.
|
|
更多的 ESLint 规则
NPM 搜索 eslint-plugin
]]>原文:《ELIMINATE JAVASCRIPT CODE SMELLS》
作者:@elijahmanor
笔记:涂鸦码农
|
|
原文《JavaScript Errors and How to Fix Them》
作者:Jani Hartikainen
翻译:涂鸦码农
JavaScript 调试是一场噩梦:首先给出的错误非常难以理解,其次给出的行号不总有帮助。有个查找错误含义,及修复措施的列表,是不是很有用?
以下是奇怪的 JavaScript 错误列表。同样的错误,不同的浏览器会给出不同的消息,因此有一些不同的例子。
首先,让我们快速看下错误信息的结构。理解结构有助于理解错误,如果遇到列表之外的错误会减少麻烦。
Chrome 中典型的错误像这样:
|
|
错误的结构如下:
Uncaught TypeError:这部分信息通常不是很有用。Uncaught
表示错误没有被 catch
语句捕获,TypeError
是错误的名字。
undefined is not a function: 这部分信息,你必须逐字阅读。比如这里表示代码尝试使用 undefined
,把它当做一个函数。
其它基于 webkit 的浏览器,比如 Safari ,给出的错误格式跟 Chrome 很类似。Firefox 也类似,但是不总包含第一部分,最新版本的 IE 也给出比 Chrome 简单的错误 - 但是在这里,简单并不总代表好。
以下是真正的错误。
相关错误:number is not a function, object is not a function, string is not a function, Unhandled Error: ‘foo’ is not a function, Function Expected
当尝试调用一个像方法的值时,这个值并不是一个方法。比如:
|
|
如果你尝试调用一个对象的方法时,你输错了名字,这个典型的错误很容易发生。
|
|
由于对象的属性不存在,默认是 undefined
,以上代码将导致这个错误。
尝试调用一个像方法的数字,“number is not a function” 错误出现。
如何修复错误:确保方法名正确。这个错误的行号将指出正确的位置。
相关错误:Uncaught exception: ReferenceError: Cannot assign to ‘functionCall()’, Uncaught exception: ReferenceError: Cannot assign to ‘this’
尝试给不能赋值的东西赋值,引起这个错误。
这个错误最常见的例子出现在 if 语句使用:
|
|
此例中,程序员意外地使用了单个等号,而不是双等号。“left-hand side in assignment” 提及了等号左手边的部分,因此你可以看到以上例子,左手边包含不能赋值的东西,导致这个错误。
如何修复错误:确保没有给函数结果赋值,或者给 this
关键字赋值。
相关错误:Uncaught exception: TypeError: JSON.stringify: Not an acyclic Object, TypeError: cyclic object value, Circular reference in value argument not supported
把循环引用的对象,传给 JSON.stringify
总会引起错误。
|
|
由于以上的 a
和 b
循环引用彼此,结果对象无法转换成 JSON。
如何修复错误:移除任何想转换成 JSON 的对象中的循环引用。
相关错误:Expected ), missing ) after argument list
JavaScript 解释器预期的东西没有被包含。不匹配的圆括号或方括号通常引起这个错误。
错误信息可能有所不同 - “Unexpected token ]” 或者 “Expected {” 等。
如何修复错误:有时错误出现的行号并不准确,因此很难修复。
相关错误:Unterminated String Literal, Invalid Line Terminator
一个字符串字面量少了结尾的引号。
如何修复错误:确保所有的字符串都有结束的引号。
相关错误:TypeError: someVal is null, Unable to get property ‘foo’ of undefined or null reference
尝试读取 null
或者 undefined
,把它当成了对象。例如:
|
|
如何修复错误:通常由于拼写错误导致。检查错误指出的行号附近使用的变量名是否正确。
相关错误:TypeError: someVal is undefined, Unable to set property ‘foo’ of undefined or null reference
尝试写入 null 或者 undefined ,把它当成了一个对象。例如:
|
|
如何修复错误:也是由于拼写错误所致。检查错误指出的行号附近的变量名。
相关错误:Related errors: Uncaught exception: RangeError: Maximum recursion depth exceeded, too much recursion, Stack overflow
通常由程序逻辑 bug 引起,导致函数的无限递归调用。
如何修复错误:检查递归函数中可能导致无限循环 的 bug 。
相关错误:URIError: malformed URI sequence
无效的 decodeURIComponent 调用所致。
如何修复错误:按照错误指出的行号,检查 decodeURIComponent
调用,它是正确的。
相关错误:Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at
http://some/url/
错误肯定是使用 XMLHttpRequest 引起的。
如何修复:确保请求的 URL 是正确的,它遵循同源策略 。最好的方法是从代码中找到错误信息指出的 URL 。
相关错误:InvalidStateError, DOMException code 11
代码调用的方法在当前状态无法调用。通常由 XMLHttpRequest
引起,在方法准备完毕之前调用它会引起错误。
|
|
这时就会出错,因为 setRequestHeader
方法只能在 xhr.open
方法之后调用。
如何修复:查看错误指出的行号,确保代码运行的时机正确,或者在它(例如 xhr.open
)之前添加了不必要的调用
我看过不少无用的 JavaScript 错误,比如 PHP 中声名狼藉的异常 Expected T_PAAMAYIM_NEKUDOTAYIM
。抛出更熟悉的错误才更有意义。现代浏览器不再抛出完全无用的错误,才会更有帮助。
你看到的最令人困惑的错误是什么?在评论中分享一下呗。
]]>原文《JavaScript Errors and How to Fix Them》
作者:Jani Hartikainen
翻译:涂鸦码农
JavaScript 调试是一场噩梦:首先给出的错误非常难以理解,其次给出的行号不总有帮助。有个查找错误含义,及修复措施的列表,是不是很有用?
以下是奇怪的 JavaScript 错误列表。同样的错误,不同的浏览器会给出不同的消息,因此有一些不同的例子。
首先,让我们快速看下错误信息的结构。理解结构有助于理解错误,如果遇到列表之外的错误会减少麻烦。
Chrome 中典型的错误像这样:
|
|
错误的结构如下:
Uncaught TypeError:这部分信息通常不是很有用。Uncaught
表示错误没有被 catch
语句捕获,TypeError
是错误的名字。
undefined is not a function: 这部分信息,你必须逐字阅读。比如这里表示代码尝试使用 undefined
,把它当做一个函数。
其它基于 webkit 的浏览器,比如 Safari ,给出的错误格式跟 Chrome 很类似。Firefox 也类似,但是不总包含第一部分,最新版本的 IE 也给出比 Chrome 简单的错误 - 但是在这里,简单并不总代表好。
以下是真正的错误。
相关错误:number is not a function, object is not a function, string is not a function, Unhandled Error: ‘foo’ is not a function, Function Expected
当尝试调用一个像方法的值时,这个值并不是一个方法。比如:
|
|
JSHint,发现错误和潜在问题的社区驱动的工具
JSLint 错误解析
|
|
|
|
Ctrl+Shift+P
或 Cmd+Shift+P
输入 install
,选择 Package Control: Install Package
输入 js gutter
,选择 JSHint Gutter
Tools -> Command Palette (Ctrl+Shift+P
或者 Cmd+Shift+P
) 然后输入 jshint
– 或者 –
Ctrl+Shift+J
(或者 Mac 使用 Cmd+Shift+J
)
– 或者 –
当前文件右键选择 JSHint -> Lint Code
– 或者 –
打开 JavaScript 文件,菜单 View -> Show Console,然后输入 view.run_command("jshint"
)
右键 -> JSHint -> Set Plugin Options
三项设置为 true
通过 --config
标记手动配置
使用 .jshintrc 文件
配置放到项目的 package.json 文件里面, jshintConfig 下面
禁用位运算符,位运算符在 JavaScript 中使用较少,经常是把 && 错输成 &
循环或者条件语句必须使用花括号包围
强制使用三等号
兼容低级浏览器 IE 6/7/8/9
禁止重写原生对象的原型,比如 Array
,Date
代码缩进
禁止定义之前使用变量,忽略 function
函数声明
构造器函数首字母大写
禁止使用 arguments.caller
和 arguments.callee
,未来会被弃用, ECMAScript 5 禁止使用 arguments.callee
为 true
时,禁止单引号和双引号混用
变量未定义
变量未使用
严格模式
最多参数个数
最大嵌套深度
复杂度检测
最大行数
控制“缺少分号”的警告
忽略 debugger
控制 eval
使用警告
检查一行代码最后声明后面的分号是否遗漏
检查不安全的折行,忽略逗号在最前面的编程风格
检查循环内嵌套 function
检查多行字符串
检查无效的 typeof
操作符值
person['name']
vs. person.name
new function () { ... }
和 new Object
;
在非构造器函数中使用 this
预定义一些全局变量
预定义全局变量 document
,navigator
,FileReader
等
定义用于调试的全局变量:console
,alert
定义全局变量
JSHint,发现错误和潜在问题的社区驱动的工具
JSLint 错误解析
|
|
|
|
Ctrl+Shift+P
或 Cmd+Shift+P
输入 install
,选择 Package Control: Install Package
输入 js gutter
,选择 JSHint Gutter
Tools -> Command Palette (Ctrl+Shift+P
或者 Cmd+Shift+P
) 然后输入 jshint
– 或者 –
Ctrl+Shift+J
(或者 Mac 使用 Cmd+Shift+J
)
– 或者 –
当前文件右键选择 JSHint -> Lint Code
– 或者 –
打开 JavaScript 文件,菜单 View -> Show Console,然后输入 view.run_command("jshint"
)
官方文档:http://handlebarsjs.com/
笔记:涂鸦码龙
Handlebars 兼容 Mustache 模板。
对比了几个 Node.js 常用模板,什么 EJS 、Jade 等等,还是感觉 Handlebars 比较顺手,模板只做数据展示,前端逻辑的东西通过 helper 实现,HTML 中没有掺杂太多 JS 的东西,看起来整洁一些。
Express 中引入 Handlebars 模板的话,需要引入hbs 模块
handlebars 表达式
|
|
在上下文中找 title 属性,获取它的值
点分割表达式
|
|
当前上下文找 article 属性,再找它的 title 属性
标识符可以是除了以下字符以外的 unicode 字符
Whitespace ! “ # % & ‘ ( ) * + , . / ; < = > @ [ \ ] ^ ` { | } ~
不合法的标识符用 “[]” 包装
|
|
不转义
|
|
Helpers
0或多个参数,用空格分割,每个参数是个 handlebars 表达式
|
|
link 是 helper 名字,story 是 helper 参数。
注册 helper
|
|
helper 返回 HTML ,不想被转义,用 Handlebars.SafeString()
。
helper 把接收的上下文作为 this
上下文
|
|
上下文和 helper:
|
|
|
|
输出结果:
|
|
也可以直接传字符串参数
|
|
等价于
|
|
|
|
helper 最后一个参数也可以接收可选的键值对序列(文档提到的 hash 参数)
|
|
hash 参数的 key
必须是简单的标识符,value
是 Handlebars 表达式, value
可以是简单的标识符,路径,或者字符串。
|
|
|
|
|
|
noop helper 实际跟没有 helper 类似,只是传递上下文,返回字符串。Handlebars 把当前的上下文作为 this
。
根据模板传递的上下文解析模板
|
|
当 JSON 对象包含嵌套属性时,不必再三重复父属性的名字。比如以下数据:
|
|
helper 接收参数,参数为 JSON 属性的 上下文。
|
|
Handlebars 内建了 each 迭代器
|
|
实现原理如下: 把 comments 数组的每一个元素作为上下文解析模板
|
|
可以用 this
引用迭代元素
|
|
上下文:
|
|
结果:
|
|
当某一项为空时,可以用
表达式
|
|
通过
可以引用当前的循环索引
|
|
用
数组迭代的第一步和最后一步用 @first
和 @last
变量表示, 对象迭代时仅 @first
可用。
如果条件参数返回 false
, undefined
, null
, ""
或 []
(非真的值)时,Handlebars 将不渲染该块
Handlebars 内建了 if
和 unless
语句
|
|
实现原理:根据传入的条件参数,判断是否解析模板
|
|
Handlebars 还提供了 else
语句
|
|
unless
跟 if
正好相反,如果表达式返回 false ,模板将被渲染。
|
|
当 license 返回 false
,Handlebars 将渲染 warning 。
记录上下文状态
|
|
模板可以包含在特殊的 <script>
里:
|
|
然后用 Handlebars.compile
编译模板
|
|
获取编译后的 HTML 模板,用 JSON 数据填充模板
|
|
最终结果:
|
|
不想转义用
|
|
模板:
|
|
上下文数据:
|
|
最终结果:
|
|
Handlebars.SafeString
方法不做转义,通常返回 new Handlebars.SafeString(result)
。此种情形,你可能想手动转义参数:
|
|
模板注释
|
|
模板注释不会输出,HTML 注释会输出
|
|
用
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|