原文链接 : Reverse Engineering One Line of JavaScript
原文作者 : Alex
译文出自 : 众成翻译
译者 : yangzj1992
首发于: 众成翻译
不久前,我看到有人提了个问题,能否有人将下面这行 JS 代码进行逆向分析。
1
| <pre id=p><script>n=setInterval("for(n+=7,i=k,P='p.\\n';i-=1/k;P+=P[i%2?(i%2*j-j+n/k^j)&1:2])j=k/i;p.innerHTML=P",k=64)</script>
|
这行代码的效果如下所示,它是一个大小仅为 128b 的光线追踪挡板 Demo 。你也可以访问这里查看效果。这行代码的作者是p01, 发布于Pouet.net。你可以访问他的网站看到更多有趣的 Demo。
下面我们试着对这行代码进行一下逆向分析。
首先,我们将 HTML 和 JS 代码分离。这里我们保留相关的 id 指向。
1 2 | <script src="code.js"></script> <pre id="p"></pre> |
这里我们注意到有个变量 k
。我们将它置顶申明,并重命名为 delay
。
1 2 3 | var delay = 64; var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P"; var n = setInterval(draw, delay); |
var draw
由于是一个字符串,它会被 setInterval 用 eval() 方法执行(setInterval 可以接受一个 function 或是 string 来执行)。所以这里我们将它重写成一个真实的 function。
另外这里还对元素 p 进行了直接的 DOM 操作,这里我们用 JS 获取这个 id 来重新书写,让它更加易懂。
1 2 3 4 5 6 7 8 9 | var delay = 64; var p = document.getElementById("p"); // < --------------- // var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P"; var draw = function() { for (n += 7, i = delay, P = 'p.\n'; i -= 1 / delay; P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2]) { j = delay / i; p.innerHTML = P; } }; var n = setInterval(draw, delay); |
接下来,我们将变量 i
,p
, j
也作提前声明。
1 2 3 4 5 6 7 8 9 10 11 12 13 | var delay = 64; var p = document.getElementById("p"); // var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P"; var draw = function() { var i = delay; // < --------------- var P ='p.\n'; var j; for (n += 7; i > 0 ;P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2]) { j = delay / i; p.innerHTML = P; i -= 1 / delay; } }; var n = setInterval(draw, delay); |
然后,我们将 for 循环转为 while 循环,将 for 循环中间的条件语句作为条件,其他的语句放到 while 循环的内外部。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | var delay = 64; var p = document.getElementById("p"); // var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P"; var draw = function() { var i = delay; var P ='p.\n'; var j; n += 7; while (i > 0) { // <---------------------- //Update HTML p.innerHTML = P; j = delay / i; i -= 1 / delay; P += P[i % 2 ? (i % 2 * j - j + n / delay ^ j) & 1 : 2]; } }; var n = setInterval(draw, delay); |
接下来我们展开三元表达式。
这里 i % 2
的作用是判断 i 是否为偶数,如果是偶数,那么将会返回 2,否则返回 (i % 2 * j - j + n / delay ^ j) & 1
。
而这一堆式子也将会作为 P
的 index 进行处理。 即 P += P[index];
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | var delay = 64; var p = document.getElementById("p"); // var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P"; var draw = function() { var i = delay; var P ='p.\n'; var j; n += 7; while (i > 0) { //Update HTML p.innerHTML = P; j = delay / i; i -= 1 / delay; let index; let iIsOdd = (i % 2 != 0); // <--------------- if (iIsOdd) { // <--------------- index = (i % 2 * j - j + n / delay ^ j) & 1; } else { index = 2; } P += P[index]; } }; var n = setInterval(draw, delay); |
这里我们接下来注意到与运算符 & 1
。
(i % 2 * j - j + n / delay ^ j) & 1
,这段代码可以巧妙地去比较一个数是否为奇偶,它的原理其实是数值转换为二进制进行与运算返回的十进制结果。
1 2 3 4 5 6 | 0 & 1 // 0 1 & 1 // 1 2 & 1 // 0 3 & 1 // 1 3 & 2 // 2 8 & 8 // 8 |
然后我们再次对变量进行重命名整合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | var delay = 64; var p = document.getElementById("p"); // var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P"; var draw = function() { var i = delay; var P ='p.\n'; var j; n += 7; while (i > 0) { //Update HTML p.innerHTML = P; j = delay / i; i -= 1 / delay; let index; let iIsOdd = (i % 2 != 0); if (iIsOdd) { let magic = (i % 2 * j - j + n / delay ^ j); let magicIsOdd = (magic % 2 != 0); // &1 < -------------------------- if (magicIsOdd) { // &1 <-------------------------- index = 1; } else { index = 0; } } else { index = 2; } P += P[index]; } }; var n = setInterval(draw, delay); |
由于这里 P ='p.\n'; 而我们的 index 的值为:0, 1, 2。对应即有:
1 2 3 | P[0] = 'p' P[1] = '.' P[2] = '\n' |
所以这里我们还可以用 switch 重写代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | var delay = 64; var p = document.getElementById("p"); // var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P"; var draw = function() { var i = delay; var P ='p.\n'; var j; n += 7; while (i > 0) { //Update HTML p.innerHTML = P; j = delay / i; i -= 1 / delay; let index; let iIsOdd = (i % 2 != 0); if (iIsOdd) { let magic = (i % 2 * j - j + n / delay ^ j); let magicIsOdd = (magic % 2 != 0); // &1 if (magicIsOdd) { // &1 index = 1; } else { index = 0; } } else { index = 2; } switch (index) { // P += P[index]; <----------------------- case 0: P += "p"; // aka P[0] break; case 1: P += "."; // aka P[1] break; case 2: P += "\n"; // aka P[2] } } }; var n = setInterval(draw, delay); |
现在我们来梳理 var n = setInterval(draw, delay);
。因为 setInterval 返回一个从 1 开始的整数 ID 。并在每次 setInterval 方法被调用时依次递增。(这个 ID 可以被用于 clearInterval 等方法。)在我们的例子中,setInterval 只被调用了一次,所以 n 被设置为 1。
此外,我们把 delay
重命名为 DELAY
以作为常量。
最后,我们对 i % 2 * j - j + n / DELAY ^ j
进行排序,由于 ^
位异或运算符的优先级较低。所以加上括号后整理如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | const DELAY = 64; // approximately 15 frames per second var n = 1; var p = document.getElementById("p"); // var draw = "for(n+=7,i=delay,P='p.\\n';i-=1/delay;P+=P[i%2?(i%2*j-j+n/delay^j)&1:2])j=delay/i;p.innerHTML=P"; /** * Draws a picture * 128 chars by 32 chars = total 4096 chars */ var draw = function() { var i = DELAY; // 64 var P ='p.\n'; // First line, reference for chars to use var j; n += 7; while (i > 0) { j = DELAY / i; i -= 1 / DELAY; let index; let iIsOdd = (i % 2 != 0); if (iIsOdd) { let magic = ((i % 2 * j - j + n / DELAY) ^ j); // < ------------------ let magicIsOdd = (magic % 2 != 0); // &1 if (magicIsOdd) { // &1 index = 1; } else { index = 0; } } else { index = 2; } switch (index) { // P += P[index]; case 0: P += "p"; break; case 1: P += "."; break; case 2: P += "\n"; } } //Update HTML p.innerHTML = P; }; setInterval(draw, 64); |
你可以在这里看到最后的代码。
在 draw()
函数第一次执行时 i 被 var i = DELAY;
初始化为 64,并在每次循环中被 i -= 1 / DELAY;
进行逐次 1/64 的递减。直到 i > 0
时结束(循环 64 * 64 次)
而我们的图像则是由 32 行组成,每行包含了 128 个字符。这里我们可以注意到我们会对 i 进行奇偶判断:let iIsOdd = (i % 2 != 0)
当 i 为偶数时,总计会发生 32 次。(即 64、62、60...等)。此时 index 将被赋值为 2。此时通过 P += "\n";
来添加新的一行。剩下的 127 次循环产生的字符即为 p
或 .
。
由代码可知,当 ((i % 2 * j - j + n / DELAY) ^ j);
为奇数时。我们会添加 .
,反之会添加 p
。
这里问题的关键来了,这串式子什么时候分别为奇偶呢?在我们研究前,我们可以做一个实验,让我们从 let magic = ((i % 2 * j - j + n / DELAY) ^ j);
中移除 + n/DELAY
。刷新页面后,我们获得了一份静止的图像输出。
那么,我们就先在移除 + n/DELAY
的情况下进行探讨。对于 (i % 2 * j - j) ^ j
因为在每次循环中有: j = DELAY / i;
所以我们可以将式子简化为一元式:(i % 2 * 64/i - 64/i) ^ 64/i
。
这里我们借助一个在线的图表生成工具来帮忙绘制函数。
例如,首先我们要绘制的 i % 2
,它的展示为下图所示的重复一次函数片段,y 值的范围在 0 到 2 之间。
如果我们绘制 64/i
,所对应的图表展示如下:
现在我们将式子结合起来,绘制图如下:
将两个函数绘制在一张图上,绘制图如下:
现在让我们着力观察图表的前16行,即 i
值的范围是从 64 到 32。
通过 JS 的 XOR (位异或)运算符的计算规则,当你的位运算两端都为 0 或 1 时,将返回 0 ,两端不同时为 1。同时如果你的数是小数的话,将会抛弃小数部分进行计算。
1 2 3 4 5 | First Second Result 1 1 1 1 0 1 0 1 1 0 0 0 |
异或运算的窍门:对两个数进行三次异或运算,可以互换他们的值,不需要引入临时变量。
1 2 3 var a=12; var b=23; a^=b,b^=a,a^=b;//a=23,b=12
异或运算也可以用来取整
1.23^0 //1
,3.5^0 //3
当负数与整数进行异或运算时。负数先进行补码,取反再加一。正数的原码和补码一致。如,对于 -2:
1 2 3 源码:1000 0000 0000 0010 (负数,最高为是 1) 反码:1111 1111 1111 1101 (按位取反) 补码:1111 1111 1111 1110 (加一)
1 2 3 4 -2 ^ 3 = 1111 1111 1111 1110 ^ 0000 0000 0000 0011 = 1111 1111 1111 1101
再转回原码(负数最高位不变,其他位取反,+1,正数不变) = - 3
在 i
值的这个范围内 j
将从 1 开始慢慢的走向 2(即基本为 1.XXX)。这样在另一端也为 1 时,我们将会得到异或计算结果为 0(偶数),最后获得 p
字符输出。
换句话说,每条蓝色的对角线代表着我们 Demo 图表中的一行。因为 j
在这 16 行里总是大于 1 而小于 2。我们得到奇数的唯一办法就是使式子 (i % 2 * j - j)^ j
即 i % 2 * i/64 - i/64
即蓝色的对角线应该处于大于 1 或小于 -1的范围。
通过图我们可以看到,在最右侧的对角线上很少有到大于 1 和小于 -1 的地方。随着对角线往左的描绘,对角线逐渐开始变长。到第 16 行位置对角线到达 -2 到 2 的位置。在 16 行以后,我们从静态 Demo 图上也可以看到图的展示规律变成了另一种模式。
在第 16 行后,j
的值开始大于 2 。这时我们的式子期望也发生了反转,在蓝色对角线大于 2 和小于 -2 时或是 -1 到 1 的范围时式子才能为偶数。这就是为什么在 17 行以后我们能看到更多组 p
的展示。此外如果你仔细观察 Demo 的底部几行,你会注意到它们也并没有遵循同样的展示规则,因为在后面图的波动也越来越大了。
让我们回到 + n/DELAY
,通过代码我们可以知道 n 是从 8 开始(从 1 开始并在每次执行 setInterval 时加 7)。
当 n 变成 64 时,此时绘图如下:
现在 j
的值趋近于 1,x 轴在 62 - 63 上的值为 0.x , 63 - 64 的值为 1.x。可推得在 63 - 64 时对角线的值是 (1 ^ 1 = 0 // even) 添加一串 p
,62 - 63 时对角线的值是(1^0 = 1 // odd) 添加一串 .
。
此时呈现的 Demo 静态图像如下所示(在 codepen 的 demo 里你可以自行修改 n
值进行测试)。它的第一行正如我们所推测的那样。
当 n 在下一次执行 setInterval,图表产生了如下的轻微变化。
注意,第一行对角线此时增长了 7/64 ≈ 0.1 ,由于 Demo 中 1 行有 128 个字符(对应图表值范围为 2)。相应影响的字符数应该为 0.1 * (128 / 2) = 6.4。我们看一下对应静态图修改后的展示,在第一行中实际移动了 7 个字符,这与我们的猜想也吻合。
再来最后一个例子,这是当 setInterval 被调用 7 次时,n = 64 + 9 * 7。
此时第一行 j
依然等于 1。而 63 -- 64 值为 2.x,62 -- 63 值为 1.x。由于 1^2 = 3 // odd - .
,1 ^ 1 = 0 //even - p
。所以展示效果为一串 .
后面跟随了一串 p
。如图所示:
之后 Demo 将会按类似的规则反复进行渲染。
代码的原理大致如此,尽管亲手进行正向压缩简化代码到如此程度确实很难,但是去尝试着理解它也是很有趣的。希望这篇文章能对你有所帮助。
]]>一般是至少三轮的技术面 + 一轮 HR 面,三轮技术一定程度上可能是你未来的同事 + 架构 + 上级。
原理利用了JS 事件的事件冒泡机制,在 document(或事件源的父层)进行监听,冒泡到监听点后,判断事件源是否自己设定的元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | function delegate(parent, eventType, selector, fn){ //参数处理 if(typeof parent === 'string'){ var parent = document.getElementById(parent); !parent && alert('parent not found'); } if(typeof selector !== 'string'){ alert('selector is not string'); } if(typeof fn !== 'function'){ alert('fn is not function'); } function handle(e){ // 获取 event 对象 // 标准 DOM 方法事件处理函数第一个参数是 event 对象 // IE可以使用全局变量 window.event var evt = window.event ? window.event : e; // 获取触发事件的原始事件源 // 标准 DOM 方法是用 target 获取当前事件源 // IE 使用 evt.srcElement 获取事件源 var target = evt.target || evt.srcElement; // 获取当前正在处理的事件源 // 标准 DOM 方法是用 currentTarget 获取当前事件源 // IE 中的 this 指向当前处理的事件源 var currentTarget= e ? e.currentTarget : this; if(target.id === selector || target.className.indexOf(selector) != -1){ fn.call(target); } } parent[eventType]=handle; } delegate('container', 'onclick', 'listener', function(){ alert(this); }); |
一个完整的 drag and drop 流程通常包含以下几个步骤:
设置可拖拽目标. 设置属性 draggable="true" 实现元素的可拖拽.
监听 dragstart 设置拖拽数据
为拖拽操作设置反馈图标 (可选)
设置拖放效果, 如 copy, move, link
设置拖放目标, 默认情况下浏览器阻止所有的拖放操作, 所以需要监听 dragenter 或者 dragover 取消浏览器默认行为使元素可拖放.
监听 drop 事件执行所需操作
实现 drag
onmousedown + onmousemove → startDrag()
onmouseup → stopDrag()
偏移值,两次事件的鼠标位置记录: e.pageX || e.clientX + scrollX; e.pageY || e.clientY + scrollY;
1
| window.location.search.match(/(([^?&=]+)(=([^?&=]*))*)/g)
|
transition
animation
transition 主要设置四个过渡属性:
transition-property // 规定设置过渡效果的 CSS 属性的名称。
transition-duration // 规定完成过渡效果需要多少秒或毫秒。
transition-timing-function // 规定速度效果的速度曲线。
transition-delay // 定义过渡效果何时开始。
1 2 3 4 5 6 7 8 9 | div{ width:100px; background:blue; transition:width 2s; } div:hover{ width:300px; } |
注释:请始终设置 transition-duration 属性,否则时长为 0,就不会产生过渡效果。
优化具体参考即为如下几点:
由于渲染三阶段分为:Layout—>Paint—>Composite,可针对此三者分别进行优化。 优化目标:15FPS 不流畅 ,30FPS+感觉流畅,60FPS舒适完美
触发Layout的方式有:
要点:
触发 Paint 的方式:
当修改 border-radius, box-shadow, color 等展示相关属性时,会触发 paint 要点:
Composite 小结:
GPU 是有限度的,不要滥用 GPU 资源生成不必要的 Layer 留意意外生成的 Layer
可结合贝塞尔曲线(cubic-bezier),开启硬件加速。will-change 属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | div{ width:100px; height:100px; background:red; position:relative; animation:mymove 5s infinite; -webkit-animation:mymove 5s infinite; /*Safari and Chrome*/ } @keyframes mymove{ from {left:0px;} to {left:200px;} } @-webkit-keyframes mymove{ from {left:0px;} to {left:200px;} } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | function add() { var sum = 0, i, len; for (i = 0, len = arguments.length; i < len; i++) { sum += arguments[i]; } return sum; } var currying = function(fn) { console.log(arguments) var _args = []; return function cb() { if (arguments.length === 0) { return fn.apply(this, _args); } Array.prototype.push.apply(_args, arguments); return cb; } } var curryingAdd = currying(add); curryingAdd(1)(2)(3)(4)(); // 10 var add321 = curryingAdd(3)(2, 1); add321(4)(); // 10 |
static / relative / absolute / fixed / sticky(新特性)
粘性定位是相对定位和固定定位的混合。元素在跨越特定阈值前为相对定位,之后为固定定位。
其盒位置根据正常流计算,然后相对于该元素在流中的 flow root(BFC)和 containing block(最近的块级祖先元素)定位。在所有情况下,该元素定位均不对后续元素造成影响。当元素 B 被粘性定位时,后续元素的位置仍按照 B 未定位时的位置来确定。position: sticky 对 table 元素的效果与 position: relative 相同。
1
| #one { position: sticky; top: 10px; }
|
中间件是一种独立的系统软件或服务程序,分布式应用软件借助这种软件在不同的技术之间共享资源。中间件位于客户机 / 服务器的操作系统之上,管理计算机资源和网络通讯。是连接两个独立应用程序或独立系统的软件。相连接的系统即使它们具有不同的接口,但通过中间件相互之间仍能交换信息。执行中间件的一个关键途径是信息传递。通过中间件,应用程序可以工作于多平台或 OS 环境。
插件是一种遵循一定规范的应用程序接口编写出来的程序。
浮动,宽度瓜分,媒体查询,响应式布局。
原型链继承(对象间的继承) 类式继承(构造函数间的继承) ES6 Class
Canvas 适用场景:
Canvas 提供的功能更原始,适合像素处理,动态渲染和大数据量绘制; Canvas 是使用 JavaScript 程序绘图,提供画布标签和绘制 API(动态生成); Canvas 是基于位图的图像,它不能够改变大小,只能缩放显示;
SVG 适用场景:
SVG 功能更完善,适合静态图片展示,高保真文档查看和打印的应用场景 SVG 是使用 XML 文档描述来绘图,是一整套独立的矢量图形语言; SVG 更适合用来做动态交互,而且 SVG 绘图很容易编辑,只需要增加或移除相应的元素就可以了。 SVG 是基于矢量的,所有它能够很好的处理图形大小的改变。
二者是有不同用途的,作为一个开发者,你应该做的是理解应用程序的具体需求并选择正确的技术来实现它。
1 2 3 4 5 | var a = '2014-04-23'; var date1 = new Date(a); var b = '2014-04-24'; var date2 = new Date(b); var days = Math.floor((date2 - date1) / 1000 / 60 / 60 / 24) |
详见JavaScript 变量的生命周期:为什么 let 不存在变量提升
原生方法监听 DOM 结构改变事件 https://developer.mozilla.org/en-US/docs/XUL/Events#Mutation_DOM_events
1 2 3 | document.addEventListener('DOMNodeInserted',function(){alert(1)},false); document.addEventListener('DOMAttrModified',function(){alert(1)},false); document.addEventListener('DOMNodeRemoved',function(){alert(1)},false); |
变动事件包括以下不同事件类型:
DOMSubtreeModified: 在 DOM 结构中发生任何变化时触发
DOMNodeInserted: 在一个节点作为子节点被插入到另一个节点中时触发
DOMNodeRemoved: 在节点从其父节点中被移除时触发
DOMNodeRemovedFromDocument: 在一个节点被直接从文档中移除或通过子树间接从文档中移除之前触发
DOMNodeInsertedIntoDocument: 在一个节点被直接插入文档或通过子树间接插入文档之后触发
DOMAttrModified: 在属性被修改之后触发
DOMCharacterDataModified: 在文本节点的值发生变化时触发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // Firefox和Chrome早期版本中带有前缀 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver // 选择目标节点 var target = document.querySelector('#some-id'); // 创建观察者对象 var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { console.log(mutation.type); }); }); // 配置观察选项: var config = { attributes: true, childList: true, characterData: true } // 传入目标节点和观察选项 observer.observe(target, config); // 随后,你还可以停止观察 observer.disconnect(); |
WebSocket 协议基于 TCP,解决过去为了实现即时通讯而采用的轮询方案,解决了大量交换 HTTP header,信息交换效率低的问题。在浏览器和服务器之间建立双向连接。
这方面被问到的比较多的:观察者模式,工厂模式,职责链模式等等
主要是经常涉及应用于 js 开发组件。比如如何去设计一个前端UI组件,应该公开出哪些方法,应该提供哪些接口,应该提供哪些事件。哪部分逻辑流程应该开放出去让用户自行编写,如何实现组件与组件之间的通信,如何实现高内聚低耦合,如何实现组件的高复用等等。
结合框架原理,社区环境,技术选型(技术、业务、人)等
]]>babel-preset-es2015 : 6.18.0
,babel-core : 6.21.0
版本进行。
Babel 是 JavaScript 编译器,更确切地说是源码到源码的编译器,通常也叫做“转换编译器(transpiler)”,它的工作流可以用下面一张图来表示,代码首先经由 babylon 解析成抽象语法树(AST),后经一些遍历和分析转换(主要过程),最后根据转换后的 AST 生成新的常规代码。
在这其中,理解 AST 十分重要,为了让计算机能够更好地理解,所以需要将代码转换为 AST 。我们可以来看看下面这段代码被解析成 AST 后对应的结构图:
1 2 3 4 5 6 7 | function abs(number) { if (number >= 0) { // test return number; // consequent } else { return -number; // alternate } } |
在 Babylon 的 AST 规范文档中对每个节点类型都做了详细的说明,你可以对照各个节点类型在这查找到所需要的信息。在这个例子中,我们主要关注函数声明里的内容, IfStatement
对应代码中的 if...else
区块的内容,我们先对条件(test)进行判断,这里是个简单的二元表达式,我们的分支也会从这个条件继续进行下去,consequent 代表条件值为 true 的分支,alternate 代表条件值为 false 的分支,最后两条分支各自在 ReturnStatement 节点进行返回。
了解 AST 各个节点的类型是理解 Babel 、编写插件的关键,AST 通常情况下都是比较复杂的,上述一段简单的函数定义也生成了比较大的 AST,对于一些复杂的程序,我们可以借助 astexplorer 来帮我们分析 AST 的结构。 这里是上述代码的一个示例链接。
Babel 的三个主要处理步骤分别是: 解析(parse)
,转换(transform)
,生成(generate)
。
解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:词法分析(Lexical Analysis)(把字符串形式的代码转换为 令牌(tokens) 流) 和 语法分析(Syntactic Analysis)(令牌流转换成 AST 的形式)。
程序转换(Program transformation)步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel 或是其他编译器中最复杂的过程 同时也是插件将要介入工作的部分。
代码生成(Code generation)步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。.
代码生成其实很简单:深度优先遍历(DFS) 整个 AST,然后构建可以表示转换后代码的字符串。
想要转换 AST 你需要进行递归的树形遍历。在进行节点遍历前还需要先了解 visitor 和 path 的概念,前者相当于从众多节点类型中选择开发者所需要的节点,后者相当于对节点之间的关系的访问。
Babel 使用 babel-traverse
进行树状的遍历,基本的树的遍历分为 DFS 和 BFS。AST 树的遍历使用 DFS,这里 Babel 提供了一个 visitor 对象来供我们获取 AST 里的具体节点,比如在我只想访问 if...else 生成的节点,我们可以在 visitor 里指定获取它所对应的节点:
1 2 3 4 5 | const visitor = { IfStatement() { console.log('get if'); } }; |
之所以使用这样的术语是因为有一个访问者模式(visitor)的概念。对于每个结点,有向下遍历进入结点(enter)和向上退出结点(exit)两个时刻(对应递归调用的入栈、出栈),我们可以在此时来访问结点。
1 2 3 4 5 6 | const visitor = { IfStatement: { enter() {}, exit() {} } } |
visitor 模式中我们对节点的访问实际上是对节点路径的访问,在这个模式中我们一般把path 当作参数传入节点选择器中。path 表示两个节点之间的连接,通过这个对象我们可以访问到节点、父节点以及进行一系列跟节点操作相关的方法(类似 DOM 的操作)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | var babel = require('babel-core'); var t = require('babel-types'); const code = `d = a + b + c`; const visitor = { Identifier(path) { console.log(path.node.name); // d a b c } }; const result = babel.transform(code, { plugins: [{ visitor: visitor }] }); |
以上面的例子,我们有一个 FunctionDeclaration
类型如下。它有几个属性:id
,params
,和 body
,每一个都有一些内嵌节点。我们依次遍历每个节点即可。Babel 的转换步骤就是循环这样的遍历过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | { type: "FunctionDeclaration", id: { type: "Identifier", name: "abs" }, params: [{ type: "Identifier", name: "number" }], body: { type: "BlockStatement", body: [{ type: "IfStatement", test: { type: "BinaryExpression", left: { type: "Identifier", name: "number" }, operator: ">=", right: { type: "Literal", value: "0" } }, consequent:{ type: "BlockStatement", body: [{ type: "ReturnStatement", argument: { type: "Identifier", name: "number" } }] }, alternate: { type: "BlockStatement", body: [{ type: "ReturnStatement", argument: { type: "UnaryExpression", opertaor: "-", argument: { type: "Identifier", name: "number" }, name: "number" } }] } }] } } |
我们统称的 babel 源码实质上对应于 npm 上的多个包,具体可以参考对应的说明文件, 主要包括 核心代码(babel-core)、工具包(babel-cli 等)、Preset(babel-preset-es2015 等),另外还有一系列 helper 包。
babel 中最核心的是 babel-core 这个包,它向外暴露出 babel.transform
接口,供类似 babel.transform(code, options);
的方式调用,而核心代码位于 transformation/pipeline.js
文件中,所有信息都会挂在到 transformation/file
所暴露出来的数据结构 File
上。
File
维护的是一个文件的所有信息,包括 babel 处理所用的插件等信息。babel 的繁荣与其强大的插件管理机制是密不可分的,而插件主要由 pluginPasses
和 pluginVisitors
来维护。
为了保证在遍历路径的时候能够快速访问对应的插件处理方法,babel 对 pluginVisitors
做了一定的预处理,将所有同类型 Identifier 的处理流程合并到了一起。具体用代码的角度来看,可以简化成这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | const PluginA = { Identifier() {}, FunctionDeclaration() {} } const PluginB = { BinaryExpression() {}, FunctionDeclaration() {} } // 进行处理后得到(源码可见 babel-traverse/lib/visitors.js 中的 function merge()) let rootVisitor = { Identifier: [PluginA.Identifier], BinaryExpression: [PluginB.Identifier], FunctionDeclaration: [PluginA.FunctionDeclaration, PluginB.FunctionDeclaration] } |
再看一下后续的内部转码流程,其最外层 pipeline 中的代码很简短:
1 2 3 4 5 | file.wrap(code, function () { file.addCode(code); file.parseCode(code); // 去除顶部的 #!/usr/bin/env node 信息,之后使用 babylon 解析出 ast,使用 babel-traverse 进行遍历 return file.transform(); }); |
其过程分解用语言描述的话,对应上文步骤如下:
File.prototype.parse
)set AST
过程:利用 babel-traverse
对 AST 进行遍历,并解析出整个树的 path,通过挂载的 metadataVisitor
读取对应的元信息。transform
过程:遍历 AST 树并应用各 transformers(plugin) 生成转换后的 AST 树注:以上面的代码片断为例,为了详细了解到整个编译过程,可以使用
DEBUG=babel node main.js
运行代码,这样就可以看到整个过程中的输出日志了。
由于 Babel 默认只转换新的 JavaScript 语法,而不转换新的 API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign
)都不会转码。
Babel 默认不转码的 API 非常多,具体可以查看详细清单
这里参考 进行补充
目前版本下插件列表如下:
babel-plugin-check-es2015-constants
=> 验证 es2015 常量babel-plugin-transform-es2015-arrow-functions
=> 箭头函数babel-plugin-transform-es2015-block-scoped-functions
=> 函数块级作用域babel-plugin-transform-es2015-block-scoping
=> let 和 const 块级作用域babel-plugin-transform-es2015-classes
=> class类babel-plugin-transform-es2015-computed-properties
=> 动态计算属性,如 var x = 1;var obj = {[x]: 3};
babel-plugin-transform-es2015-destructuring
=> 解构赋值babel-plugin-transform-es2015-duplicate-keys
=> 对象中重复的 key 转换成计算属性babel-plugin-transform-es2015-for-of
=> 对象 for ... of
遍历babel-plugin-transform-es2015-function-name
=> 函数 name 属性babel-plugin-transform-es2015-literals
=> unicode 字符串和数字字面值babel-plugin-transform-es2015-modules-amd
=> amd 模块转换babel-plugin-transform-es2015-modules-commonjs
=> commonjs 模块转换babel-plugin-transform-es2015-modules-systemjs
=> systemjs 模块转换babel-plugin-transform-es2015-modules-umd
=> umd 模块转换babel-plugin-transform-es2015-object-super
=> super 方法调用 prototypebabel-plugin-transform-es2015-parameters
=> 函数参数默认值及扩展运算符babel-plugin-transform-es2015-shorthand-properties
=> 对象属性的快捷定义,如obj = { x, y }babel-plugin-transform-es2015-spread
=> 对象扩展运算符属性,如 ...foobar
babel-plugin-transform-es2015-sticky-regex
=> 粘连修饰符 y.babel-plugin-transform-es2015-template-literals
=> es2015 模板babel-plugin-transform-es2015-typeof-symbol
=> symbol 特性babel-plugin-transform-es2015-unicode-regex
=> unicode 正则babel-plugin-transform-regenerator
=> generator特性首先基础的,转换前:
1 2 3 | var a = 1; let b = 2; const c = 3; |
转换后:
1 2 3 | var a = 1; var b = 2; var c = 3; |
let的块级作用域怎么体现呢?转换前:
1 2 3 4 5 6 7 8 9 10 11 | let a1 = 1; let a2 = 6; { let a1 = 2; let a2 = 5; { let a1 = 4; let a2 = 5; } } a1 = 3; |
转换后:
1 2 3 4 5 6 7 8 9 10 11 | var a1 = 1; var a2 = 6; { var _a = 2; var _a2 = 5; { var _a3 = 4; var _a4 = 5; } } a1 = 3; |
可见这样的例子实质就是改变一下变量名,使之与外层不同。
那么看一下经典的 let for 场景,这里大家都知道如果 let 换成 var ,那么输出将会是 10,那么这样 babel 怎么处理呢?转换前:
1 2 3 4 5 6 7 | var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 6 |
转换后:
1 2 3 4 5 6 7 8 9 10 11 12 | var a = []; var _loop = function _loop(i) { a[i] = function () { console.log(i); }; }; for (var i = 0; i < 10; i++) { _loop(i); } a[6](); // 6 |
可见这里用了闭包做了处理。经典的 for 循环闭包处理方式。
对于普通的解构赋值,转换前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | let [foo, [[bar], baz]] = [1, [[2], 3]]; let [ , , third] = ["foo", "bar", "baz"]; // third = "baz" let [x, , y] = [1, 2, 3]; // x = 1 y = 3 let [head, ...tail] = [1, 2, 3, 4]; // head = 1 tail = [2, 3, 4] let [i, j, ...k] = ['a']; // i = "a" j = undefined k = [] let [m, n] = [1]; // n = undefined let [a, [b], d] = [1, [2, 3], 4]; // a = 1 b = 2 d = 4 const [a, b, c, d, e] = 'hello'; let {length : len} = 'hello'; |
转换后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); var foo = 1, bar = 2, baz = 3; var _ref = ["foo", "bar", "baz"], third = _ref[2]; var _ref2 = [1, 2, 3], x = _ref2[0], y = _ref2[2]; var head = 1, tail = [2, 3, 4]; var _ref3 = ['a'], i = _ref3[0], j = _ref3[1], k = _ref3.slice(2); var _ref4 = [1], m = _ref4[0], n = _ref4[1]; var a = 1, _ref5 = [2, 3], b = _ref5[0], d = 4; var _hello = 'hello', _hello2 = _slicedToArray(_hello, 5), x1 = _hello2[0], x2 = _hello2[1], x3 = _hello2[2], x4 = _hello2[3], x5 = _hello2[4]; var _hello3 = 'hello', len = _hello3.length; |
可以看到,这里 babel 就是很正常的采用一一赋值的方式进行的,对于匿名数组等情况,babel 会帮你先定义一个变量存放这个数组,然后再对需要赋值的变量进行赋值。
还有一种对象深层次的解构赋值,转换前:
1 2 3 4 5 6 7 8 9 | var obj = { p: [ 'Hello', { y: 'World' } ] }; var { p: [x, { y }] } = obj; // x = "Hello" y = "World" |
转换后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | var _slicedToArray = (function() { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { // 用 Symbol.iterator 造了一个可遍历对象,然后进去遍历。 for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function(arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; })(); var obj = { p: ['Hello', { y: 'World' }] }; var _obj$p = _slicedToArray(obj.p, 2), x = _obj$p[0], y = _obj$p[1].y; // x = "Hello" y = "World" |
这里和上一个示例中对字符串进行赋值解构时 babel 都在代码顶部生产了一个公共的代码 _slicedToArray
。它的作用主要是对对象里的属性转换成数组形式,并依靠 i 变量来取出我们真正需要的值,方便解构赋值的进行。
转换前:
1 2 3 4 5 6 7 8 | function move({x, y} = { x: 0, y: 0 }) { return [x, y]; } move({x: 3, y: 8}); // [3, 8] move({x: 3}); // [3, undefined] move({}); // [undefined, undefined] move(); // [0, 0] |
转换后:
1 2 3 4 5 6 7 8 9 10 11 12 | function move() { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { x: 0, y: 0 }, x = _ref.x, y = _ref.y; return [x, y]; } move({ x: 3, y: 8 }); // [3, 8] move({ x: 3 }); // [3, undefined] move({}); // [undefined, undefined] move(); // [0, 0] |
转换前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | // default parameter values function log(x, y = 'World') { console.log(x, y); } log('Hello') // Hello World log('Hello', 'China') // Hello China log('Hello', '') // Hello // rest parameter function push(array, ...items) { items.forEach(function(item) { array.push(item); console.log(item); }); } var a = []; push(a, 1, 2, 3) // arrow function var obj = { prop: 1, func: function() { var _this = this; var innerFunc = () => { this.prop = 1; }; var innerFunc1 = function() { this.prop = 1; }; }, }; |
转换后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | // default parameter values function log(x) { var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'World'; console.log(x, y); } log('Hello'); // Hello World log('Hello', 'China'); // Hello China log('Hello', ''); // Hello // rest parameter function push(array) { for (var _len = arguments.length, items = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { items[_key - 1] = arguments[_key]; } items.forEach(function (item) { array.push(item); console.log(item); }); } var a = []; push(a, 1, 2, 3); // arrow function var obj = { prop: 1, func: function func() { var _this2 = this; var _this = this; var innerFunc = function innerFunc() { _this2.prop = 1; }; var innerFunc1 = function innerFunc1() { this.prop = 1; }; } }; |
这里通过默认参数的转换方式并结合上例中解构赋值函数的例子就看的很明白了,主要使用 arguments 来做处理。
而 rest 参数则同样依靠 arguments 来遍历处理既定参数之后的所有参数。
而箭头函数主要是省了写 function 的代码,同时能够直接用使外层的 this 而不用担心 context 切换的问题。以前我们一般都要在外层多写一个 _this/self 直向 this。babel 的转换方法跟大家平时所了解的基本一致。
转换前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | const prop2 = "PROP2"; var obj = { ['prop']: 1, ['func']: function() { console.log('func'); }, [prop2]: 3 }; var obj = { toString() { // Super calls return "d " + super.toString(); }, }; |
转换后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | var _get = function get(object, property, receiver) { // 如果 prototype 为空,则往 Function 的 prototype 上寻找 if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); // 如果在本层 prototype 找不到,再往更深层的 prototype 上找 if (parent === null) { return undefined; } else { return get(parent, property, receiver); } // 如果是属性,则直接返回 } else if ("value" in desc) { return desc.value; // 如果是方法,则用 call 来调用,receiver 是调用的对象 } else { var getter = desc.get;// getOwnPropertyDescriptor 返回的 getter 方法 if (getter === undefined) { return undefined; } return getter.call(receiver); } }; var _obj, _obj2; function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } var prop2 = "PROP2"; var obj = (_obj = {}, _defineProperty(_obj, 'prop', 1), _defineProperty(_obj, 'func', function func() { console.log('func'); }), _defineProperty(_obj, prop2, 3), _obj); var obj = _obj2 = { toString: function toString() { // Super calls return "d " + _get(_obj2.__proto__ || Object.getPrototypeOf(_obj2), 'toString', this).call(this); } }; |
对于对象中用中括号解释属性的功能,babel 新增了一个 _defineProperty
函数,给新建的 _obj = {}
进行属性定义。除此之外使用小括号包住一系列从左到右的运算使整个定义更简洁。
对于对象字面量中使用 super
去调用 prototype,babel 通过 _get
方法来在 prototype 链上寻找方法/属性。
转换前:
1 2 3 4 5 6 7 8 9 10 11 | var a = 5; var b = 10; function tag(strings, ...values) { console.log(strings[0]); // "Hello " console.log(strings[1]); // " world " console.log(values[0]); // 15 console.log(values[1]); // 50 } tag`Hello ${ a + b } world ${ a * b }`; |
转换后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | var _templateObject = _taggedTemplateLiteral(["Hello ", " world ", ""], ["Hello ", " world ", ""]); // _templateObject = ["Hello ", " world ", "" , raw: Array[3]] // 给传入的 object 定义了 strings 和 raw 两个不可变的属性。 function _taggedTemplateLiteral(strings, raw) { return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); } var a = 5; var b = 10; function tag(strings) { // strings = ["Hello ", " world ", "", raw: Array[3]] arguments = [Array[3], 15, 50] console.log(strings[0]); // "Hello " console.log(strings[1]); // " world " console.log(arguments.length <= 1 ? undefined : arguments[1]); // 15 console.log(arguments.length <= 2 ? undefined : arguments[2]); // 50 } tag(_templateObject, a + b, a * b); es6 的这种新特性给模板处理赋予更强大的功能,一改以往对模板进行各种 replace 的处理办法,用一个统一的 handler 去处理。babel 的转换主要是添加了 2 个属性,通过捕获传参和 arguments 变量来获取具体的值。 |
js 实现 oo 一直是非常热门的话题。从最原始时代的手动维护构造函数来调用父类构造函数,到后来封装好函数进行 extend 继承,再到 babel 出现之后可以像其它面向对象的语言一样直接写 class。es2015 的类方案仍然算是过渡方案,它所支持的特性仍然没有涵盖类的所有特性。目前主要支持的有:
转换前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | class Animal { constructor(name, type) { this.name = name; this.type = type; } walk() { console.log('walk'); } run() { console.log('run') } static getType() { return this.type; } get getName() { return this.name; } set setName(name) { this.name = name; } } class Dog extends Animal { constructor(name, type) { super(name, type); } get getName() { return super.getName(); } } |
转换后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | // 与上同 var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; // _creatClass 用于创建类及其对应的方法 var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; // es6 规范要求类方法为 non-enumerable descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; // 对于 setter 和 getter 方法,writable 为 false if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { // 非静态方法定义在原型链上 if (protoProps) defineProperties(Constructor.prototype, protoProps); // 静态方法直接定义在 constructor 函数上 if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); // 子类实现 constructor // babel 会强制子类在 constructor 中使用 super,否则编译会报错 function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } // 若call是函数/对象则返回 return call && (typeof call === "object" || typeof call === "function") ? call : self; } // 子类继承父类 function _inherits(subClass, superClass) { // 父类一定要是 function 类型 if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } // 使原型链 subClass.prototype.__proto__ 指向父类 superClass,同时保证 constructor 是 subClass 自己 subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); // 保证 subClass.__proto__ 指向父类 superClass if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // 检测 constructor 正确与否 function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var Animal = function () { function Animal(name, type) { // 此处是 constructor 的实现 _classCallCheck(this, Animal); this.name = name; this.type = type; } _createClass(Animal, [{ key: 'walk', value: function walk() { console.log('walk'); } }, { key: 'run', value: function run() { console.log('run'); } }, { key: 'getName', get: function get() { return this.name; } }, { key: 'setName', set: function set(name) { this.name = name; } }], [{ key: 'getType', value: function getType() { return this.type; } }]); return Animal; }(); var Dog = function (_Animal) { _inherits(Dog, _Animal); function Dog(name, type) { _classCallCheck(this, Dog); return _possibleConstructorReturn(this, (Dog.__proto__ || Object.getPrototypeOf(Dog)).call(this, name, type)); } _createClass(Dog, [{ key: 'getName', get: function get() { return _get(Dog.prototype.__proto__ || Object.getPrototypeOf(Dog.prototype), 'getName', this).call(this); } }]); return Dog; }(Animal); |
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // module.js import { Animal as Ani, catwalk } from "./t1"; import * as All from "./t2"; class Cat extends Ani { constructor() { super(); } } class Dog extends Ani { constructor() { super(); } } |
1 2 3 4 5 6 7 8 9 | // t1.js export class Animal { constructor() { } } export function catwal() { console.log('cat walk'); }; |
1 2 3 4 5 6 7 8 9 10 | // t2.js export class Person { constructor() { } } export class Plane { constructor() { } } |
转换后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | // t1.js 模块 Object.defineProperty(exports, "__esModule", { value: true }); exports.catwal = catwal; var Animal = exports.Animal = function Animal() { _classCallCheck(this, Animal); }; function catwal() { console.log('cat walk'); }; // t2.js 模块 Object.defineProperty(exports, "__esModule", { value: true }); var Person = exports.Person = function Person() { _classCallCheck(this, Person); }; var Plane = exports.Plane = function Plane() { _classCallCheck(this, Plane); }; // module.js var _t = require("./t1"); var _t2 = require("./t2"); // 返回的都是exports上返回的对象属性 var All = _interopRequireWildcard(_t2); function _interopRequireWildcard(obj) { // 发现是babel编译的, 直接返回 if (obj && obj.__esModule) { return obj; // 非 babel 编译, 猜测可能是第三方模块,为了不报错,让 default 指向它自己 } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } var Cat = function (_Ani) { _inherits(Cat, _Ani); function Cat() { _classCallCheck(this, Cat); return _possibleConstructorReturn(this, (Cat.__proto__ || Object.getPrototypeOf(Cat)).call(this)); } return Cat; }(_t.Animal); var Dog = function (_Ani2) { _inherits(Dog, _Ani2); function Dog() { _classCallCheck(this, Dog); return _possibleConstructorReturn(this, (Dog.__proto__ || Object.getPrototypeOf(Dog)).call(this)); } return Dog; }(_t.Animal); |
es6 的模块加载是属于多对象多加载,而 commonjs 则属于单对象单加载。babel 需要做一些手脚才能将 es6 的模块写法写成 commonjs 的写法。主要是通过定义 __esModule
这个属性来判断这个模块是否经过 babel 的编译。然后通过 _interopRequireWildcard
对各个模块的引用进行相应的处理。
这一年发生了不少的事,但在抬笔时却又不知从何处细谈起。这一年让我对很多事有了更深的认识。下面还是按简单的分类简谈一下吧。
这一年是我司工作环境变化巨大的一年。总体上来讲公司依然是在较好的发展,公司搬了家,让办公环境有了很大的改善。整体技术团队也扩张了许多。可以说在硬件环境方面,这一年还是提升了许多的。
然而从个人角度而言,硬件环境改善的背面却是入职以来所认同的好几名前辈和同事的相继离职。尤其是所在的商城组,可以说是流水的老大 + 组员。尽管目前所在的前端组的小气候相比来说还算稳定。但是由于大气候的原因,导致在今年的一段时间内还是很难保持一个稳定的开发环境。。
此外随着公司的发展,在我看来公司的环境和公司文化间也存在了一定的撕裂。不少公司都或多或少存在上面的撕裂现象,例如今年百度的魏则西事件,阿里的校园日记事件等等。这些事态的发展肯定是与公司所宣扬、认同的文化、价值观相悖的。但最后是如何执行成这样的?多少都是因为这样的撕裂导致的。此外,这一年里对上下级管理、加班文化、公司人文关怀等也有了更多深刻的看法。这一年里真的需要对公司里负责的同事,老大报以十分的感谢。
这一年里在商城我们从年初用 Vue 开始重构,到目前年底总算将商城主流程的大多数页面改造成了由 Vue 全家桶开发的单页面应用(期间遇到的各种业务更替、拆分和插入需求而影响的进度不表,但这里并不是说公司不重视重构,这里的确存在着公司成立尚 2 年的客观现实,可以说相比其他主流电商,我们的商城的功能和复杂度还远远不够。商城很多新的需求的优先级确实要比重构要高许多,急需增加的很多功能对解决商户的需求是很重要的,所以技术债的解决也只能是随着需求的变化来适时的同步推进。)
此外参与了许多公司项目的开发,个人觉得印象比较深或是说有难度、特色的技术项目包括:
业务需求项目主要包括:
在这些项目中除了熟悉应用相关框架、库的方法外,在代码之外还深刻认识到了如下道理,简要列举如下:
重构是一项需要慎重、深思熟虑、小心进行的活动。关于怎样进行利大于弊的重构, Martin Fowler 给出了以下提示。
- 不要试图在重构的同时增加大量工能。
- 在开始重构之前,确保你拥有良好的测试。
- 对重构任务尽可能的划分为短小、深思熟虑的步骤方案。重构常常涉及到许多局部的改动,在代码复杂度达到一定程度时,这样的改动可能产生很大的影响。如果你的重构步骤能够保持短小,并且每次改动都有良好的测试方案,你将能够避免长时间的调试和隐含的巨大错误。
此外还包括重构项目实施的若干方法,在针对不同用户群或项目情况时,重构可以采取不同的方案,来尽可能的做到科学并达到最大化的项目效果。一般公司都是由于技术债的原因导致代码需要重构来保证代码的质量、稳定、功能拓展性等。这里还可以结合我今年 11 月的总结博文里推荐的两篇讨论技术债的文章来更加深入的了解相关的内容(链接)
在职位分工明确的大环境下,工作压力不小的情况中,也要时刻警醒自己的工作计划状态。当你过多的被动工作生活时,就是你需要停下来思考一下的时候了。这里可以参考阮一峰的这篇博文你的命运不是一头骡子 、
这一年的 Alpha Go 、无人驾驶等都展示了深度学习下的计算机应用的加速推进。在愈演愈烈的 AI 浪潮和职业环境背景下,个人更需不断提升自己的核心竞争力来提高自身工作的价值。这就如工厂制造业机械化的演进一样,相信在未来十几年各行各业都将面临新一步的劳动力革命。
做事的时候经常换一个角度想想,会有更多更深刻更有意思的发现。方法可以参考这篇博文做卧底,如何不动声色的毁掉对手的产品
正常加班的原则也应当是救急不救穷。
对个人网站 qcyoung.com 的主题进行完善、增加了新的功能和样式。
一个简单的基于网易云音乐 API 的在线音乐播放器 yPlayer
一个基于 Koa 的个人题库系统 Koa Test
一个基于总结有趣题目 & 面试题 & 算法、数据结构等基础的库FE-Questions
在掘金翻译计划、众成翻译参与了十多篇文章的翻译和校对。参与了 Vue2 的中文文档翻译校对等。
这一年坚持了扇贝打卡的习惯,没有缺勤一天(Unbelievable!
)。今日办了新的扇贝新年打卡计划。希望能继续保持。
然而今年的 Keep 和吉他计划却夭折了。。这里十分不满意。。毕竟也是真想有生之年有个好身材体和会一门拿的出手的乐器。
按照传统,后面推荐下今年的一些内容
Daisy - STEREO DIVE FOUNDATION
今年很喜欢的几本书:
重构:通过多样的重构方法,来达到合适的设计模式。分辨那些具有 Bad smell 的代码,提高自己的代码质量。
代码大全:参与工作进行更多实践之后看这本书,更是大有所悟。很多经典的准则都是数代人的经历所总结的经验。
程序员修炼之道:讲的很泛,但每个方向都有一些收获。
手把手教你读财报: 讲解十分落地,面向国内市场。很适合入门级。
具体数学:很多习题,配合英文版的书籍还能参看翻译。了解并温固了很多数学技巧。
累:分镜很不错,目前关注无奈而扭曲的女主如何收场。
GrandBlue:这真的是一本以青春、潜水为主题的故事。
亞人:漫画的剧情,分镜相当高水准,人物性格塑造的也很优秀。赞
進擊的巨人: 16 年里剧情又掀起了最后的高潮,目前来看漫画剧情应该也快要完结了,第二季动画在 17 年也将上映(虽然应该会被墙)。结局应该是像《大剑》那样结束,但希望最后还能有新的别出心裁。
关于排序算法的有关文章已经很多了,然而网络上用 Javascript 语言来作为示例并详实介绍的文章貌似还是不太多。这里主要是我来尝试自己针对网上各式的排序算法进行一份详实的个人总结,从而温故知新。
a
原本在 b
前面,而 a = b
,排序之后 a
仍然在 b
的前面;a
原本在 b
的前面,而 a = b
,排序之后 a
可能会出现在 b
的后面;名词解释:
n
: 数据规模
k
: 桶的个数
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | $O(n^2)$ | $O(n)$ | $O(n^2)$ | $O(1)$ | 内排序 | 稳定 |
选择排序 | $O(n^2)$ | $O(n^2)$ | $O(n^2)$ | $O(1)$ | 内排序 | 不稳定 |
插入排序 | $O(n^2)$ | $O(n)$ | $O(n^2)$ | $O(1)$ | 内排序 | 稳定 |
希尔排序 | $O(n\log n)$ | $O(n(\log_2^2 n))$ | $O(n(\log_2^2 n))$ | $O(1)$ | 内排序 | 不稳定 |
归并排序 | $O(n\log n)$ | $O(n\log n)$ | $O(n\log n)$ | $O(n)$ | 外排序 | 稳定 |
快速排序 | $O(n\log n)$ | $O(n\log n)$ | $O(n^2)$ | $O(\log n)$ | 内排序 | 不稳定 |
堆排序 | $O(n\log n)$ | $O(n\log n)$ | $O(n\log n)$ | $O(1)$ | 内排序 | 不稳定 |
计数排序 | $O(n + k)$ | $O(n + k)$ | $O(n + k)$ | $O(k)$ | 外排序 | 稳定 |
桶排序 | $O(n + k)$ | $O(n + k)$ | $O(n^2)$ | $O(n + k)$ | 外排序 | 稳定 |
基数排序 | $O(n × k)$ | $O(n × k)$ | $O(n × k)$ | $O(n + k)$ | 外排序 | 稳定 |
从分类上来讲:
排序算法 | 交换排序 | 冒泡排序 | 快速排序 |
选择排序 | 选择排序 | 堆排序 | |
插入排序 | 插入排序 | 希尔排序 | |
归并排序 | 归并排序 | ||
分布排序 | 计数排序 | 桶排序 | 基数排序 |
冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地走访要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换时,此时该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
图解如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | const bubbleSort = (arr) => { let len = arr.length; for(let i = 0;i < len;i++){ for(let j = 0;j < len - 1 - i;j++){ if(arr[j] > arr[j+1]){ let temp = arr[j+1]; arr[j+1] = arr[j] arr[j] = temp; } } } return arr; } let arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48]; console.log(bubbleSort(arr)); //[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50] |
改进冒泡排序: 我们设置一个标志性变量
pos
,用于记录每趟排序中最后一次进行交换的位置。由于pos
位置之后的元素均已交换到位,故在进行下一趟排序时只要扫描到pos
位置即可。 这样的优化方式可以在最好情况下把复杂度降到 $O(n)$。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | const bubbleSort2 = (arr) => { let i = arr.length - 1; while(i > 0){ let pos = 0; for(let j = 0; j < i; j++){ if (arr[j] > arr[j+1]){ pos = j; //记录交换的位置 let tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp; } } i = pos; //为下一趟排序作准备 } return arr; } |
另外传统冒泡排序中每一趟排序操作只能找到一个最大值或最小值,我们可以考虑利用在每趟排序中进行正向和反向两遍冒泡的方法来一次得到两个最终值(最大者和最小者),从而继续优化使排序趟数几乎减少一半。(这就是鸡尾酒排序)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | const cooktailSort = (arr) => { let min = 0; let max = arr.length - 1; while(min < max){ for(let j = min;j < max;j++){ if(arr[j] > arr[j+1]){ let tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp; } } -- max; for(let j = max;j > min;j--){ if(arr[j] < arr[j-1]){ let tmp = arr[j] arr[j] = arr[j-1]; arr[j-1] = tmp; } } ++ min; } return arr; } |
冒泡排序对有 $n$ 个元素的项目平均需要 $O(n^2)$ 次比较次数,它可以原地排序,并且是能简单实现的几种排序算法之一,但是它对于少数元素之外的数列排序是很没有效率的。
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
图解如下:
$n$ 个记录的直接选择排序可经过 $n - 1$ 趟直接选择排序得到有序结果。具体算法描述如下:
R[1 ... n]
,有序区为空;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | const SelectionSort = (arr) => { let len = arr.length; let minIndex, tmp; console.time('选择排序耗时'); for (let i = 0;i < len - 1; i++){ minIndex = i; for(let j = i + 1;j < len;j++){ if(arr[minIndex] > arr[j]){ minIndex = j; } } tmp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = tmp; } console.timeEnd('选择排序耗时'); return arr; } |
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对 $n$ 个元素的表进行排序总共进行至多 $n - 1$ 次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。 但原地操作几乎是选择排序的唯一优点,当空间复杂度要求较高时,可以考虑选择排序;实际适用的场合非常罕见。
插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前s扫描,找到相应位置并插入。插入排序在实现上,通常采用 in-place 排序(即只需用到 $O(1)$ 的额外空间的排序),因而在从后向前的扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
图解如下:
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目。该算法可以认为是插入排序的一个变种,称为二分查找插入排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | const insertionSort = (arr) => { let len = arr.length; console.time('插入排序耗时'); for (let i = 1;i < len; i++){ let key = arr[i]; let j = i - 1; while(j >= 0 && arr[j] > key){ arr[j+1] = arr[j]; j--; } arr[j+1] = key; } console.timeEnd('插入排序耗时'); return arr; } |
改进插入排序:查找插入位置时使用二分查找的方式。
具体思路如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | const binaryInsertionSort = (arr) => { console.time('二分插入排序耗时:'); for (let i = 1; i < arr.length; i++){ let key = arr[i], left = 0, right = i - 1; while (left <= right){ let middle = parseInt((left + right) / 2); if (key < arr[middle]){ right = middle - 1; }else{ left = middle + 1; } } for (let j = i - 1; j >= left; j--){ arr[j + 1] = arr[j]; } arr[left] = key; } console.timeEnd('二分插入排序耗时:'); return arr; } |
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。它是非稳定排序算法。
希尔排序基于插入排序的以下两点性质提出了改进方法:
它与插入排序的不同之处在于,它会优先比较距离较远的元素。因此希尔排序又叫缩小增量排序。
图解如下:
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | const shellSort = (arr) => { console.time('希尔排序耗时:'); let len = arr.length,temp,gap = 1; while(gap < len / 5) { // 动态定义间隔序列步长为 5 gap = gap * 5 + 1; } for (gap; gap > 0; gap = Math.floor(gap / 5)) { for (let i = gap; i < len; i++) { temp = arr[i]; let j; for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) { arr[j + gap] = arr[j]; } arr[j + gap] = temp; } } console.timeEnd('希尔排序耗时:'); return arr; } |
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
图解如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | const mergeSort = (arr) =>{ //采用自上而下的递归方法 let len = arr.length; if(len < 2) { return arr; } let middle = Math.floor(len / 2), left = arr.slice(0, middle), right = arr.slice(middle); return merge(mergeSort(left), mergeSort(right)); } const merge = (left, right) => { let result = []; while (left.length && right.length) { if (left[0] <= right[0]) { result.push(left.shift()); } else { result.push(right.shift()); } } while (left.length) result.push(left.shift()); while (right.length) result.push(right.shift()); return result; } let arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48]; console.time('归并排序耗时'); console.log(mergeSort(arr)); console.timeEnd('归并排序耗时'); |
和选择排序一样,归并排序的性能不受输入数据的影响,但它的表现比选择排序要好,因为它始终都是 $O_(n\log n)$ 的时间复杂度。但代价是需要额外的内存空间。
通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | const quickSort = (arr) => { if (arr.length <= 1) { return arr; } let pivotIndex = Math.floor(arr.length / 2); let pivot = arr.splice(pivotIndex, 1)[0]; let left = []; let right = []; for (let i = 0; i < arr.length; i++){ if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return quickSort(left).concat([pivot], quickSort(right)); }; let arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48]; console.time('快速排序耗时'); console.log(quickSort(arr)); console.timeEnd('快速排序耗时'); |
堆排序可以说是一种利用堆的概念来排序的选择排序。它利用堆这种数据结构所设计。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | const heapSort = (array) => { console.time('堆排序耗时'); //建堆 let heapSize = array.length, temp; for (let i = Math.floor(heapSize / 2) - 1; i >= 0; i--) { heapify(array, i, heapSize); } //堆排序 for (let j = heapSize - 1; j >= 1; j--) { temp = array[0]; array[0] = array[j]; array[j] = temp; heapify(array, 0, --heapSize); } console.timeEnd('堆排序耗时'); return array; } /*方法说明:维护堆的性质 @param arr 数组 @param x 数组下标 @param len 堆大小*/ const heapify = (arr, x, len) => { let l = 2 * x + 1, r = 2 * x + 2, largest = x, temp; if (l < len && arr[l] > arr[largest]) { largest = l; } if (r < len && arr[r] > arr[largest]) { largest = r; } if (largest != x) { temp = arr[x]; arr[x] = arr[largest]; arr[largest] = temp; heapify(arr, largest, len); } } let arr=[91,60,96,13,35,65,46,65,10,30,20,31,77,81,22]; console.log(heapSort(arr)); |
计数排序(Counting sort)是一种稳定的排序算法。计数排序使用一个额外的数组 C,其中第 i 个元素是待排序数组 A 中值等于 i 的元素的个数。然后根据数组 C 来将 A 中的元素排到正确的位置。它只能对整数进行排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | const countingSort = (array) => { let len = array.length, result = [], C = [], min,max; min = max = array[0]; console.time('计数排序耗时'); for (let i = 0; i < len; i++) { min = min <= array[i] ? min : array[i]; max = max >= array[i] ? max : array[i]; C[array[i]] = C[array[i]] ? C[array[i]] + 1 : 1; } for (let j = min; j < max; j++) { C[j + 1] = (C[j + 1] || 0) + (C[j] || 0); } for (let k = len - 1; k >= 0; k--) { result[C[array[k]] - 1] = array[k]; C[array[k]]--; } console.timeEnd('计数排序耗时'); return result; } let arr = [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2]; console.log(countingSort(arr)); |
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组 C 的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上 1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。
工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | const bucketSort = (array, num) => { if (array.length <= 1) { return array; } var len = array.length, buckets = [], result = [], min = max = array[0], regex = '/^[1-9]+[0-9]*$/', space, n = 0; num = num || ((num > 1 && regex.test(num)) ? num : 10); console.time('桶排序耗时'); for (var i = 1; i < len; i++) { min = min <= array[i] ? min : array[i]; max = max >= array[i] ? max : array[i]; } space = (max - min + 1) / num; for (var j = 0; j < len; j++) { var index = Math.floor((array[j] - min) / space); if (buckets[index]) { // 非空桶,插入排序 var k = buckets[index].length - 1; while (k >= 0 && buckets[index][k] > array[j]) { buckets[index][k + 1] = buckets[index][k]; k--; } buckets[index][k + 1] = array[j]; } else { //空桶,初始化 buckets[index] = []; buckets[index].push(array[j]); } } while (n < num) { result = result.concat(buckets[n]); n++; } console.timeEnd('桶排序耗时'); return result; } var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48]; console.log(bucketSort(arr,4)); |
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。 桶排序最好情况下使用线性时间 $O(n)$,桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为 $O(n)$。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | const radixSort = (arr, maxDigit) => { var mod = 10; var dev = 1; var counter = []; console.time('基数排序耗时'); for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) { for(var j = 0; j < arr.length; j++) { var bucket = parseInt((arr[j] % mod) / dev); if(counter[bucket]== null) { counter[bucket] = []; } counter[bucket].push(arr[j]); } var pos = 0; for(var j = 0; j < counter.length; j++) { var value = null; if(counter[j]!=null) { while ((value = counter[j].shift()) != null) { arr[pos++] = value; } } } } console.timeEnd('基数排序耗时'); return arr; } var arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]; console.log(radixSort(arr,2)); |
基数排序有两种方法:
MSD 从高位开始进行排序 LSD 从低位开始进行排序
基数排序 vs 计数排序 vs 桶排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
基数排序:根据键值的每位数字来分配桶 计数排序:每个桶只存储单一键值 桶排序:每个桶存储一定范围的数值
以上代码可访问 GitHub 具体查看
说到前端排序,自然首先会想到 JavaScript 的原生接口 Array.prototype.sort
这个接口自 ECMAScript 1st Edition
起就被设计存在。而在规范中关于它的描述是这样的:[11]
Array.prototype.sort(compareFn)
The elements of this array are sorted. The sort is not necessarily stable (that is, elements that compare equal do not necessarily remain in their original order). If comparefn is not undefined, it should be a function that accepts two arguments x and y and returns a negative value if x < y, zero if x = y, or a positive value if x > y.
显然,规范里并没有限定 sort
内部需要用什么排序算法。在这样的背景下,前端排序这件事完全取决于各家浏览器内核的具体实现。
Chrome 的 JavaScript 引擎是 v8。由于它是开源的,所以可以直接看源代码。
整个 array.js 都是用 JavaScript 语言实现的。排序方法部分很明显比通常看到的快速排序要复杂得多,但显然核心算法还是快速排序的思想。算法复杂的原因在于 v8 出于性能考虑进行了很多优化。(后续会展开说)
暂时无法确定 Firefox 的 JavaScript 引擎即将使用的数组排序算法会是什么。
按照现有的信息,SpiderMoney 内部实现了归并排序。(这里不多做叙述)
Microsoft Edge 的 JavaScript 引擎 Chakra 的核心部分代码已经于 2016 年初在 Github 开源。
通过源代码可以发现,Chakra 的数组排序算法也主要是以快速排序为主。并针对其他具体特殊情况进行具体优化。
如上所述,快速排序是一种不稳定的排序算法,而归并排序是一种稳定的排序算法。由于不同引擎在算法选择上可能存在差异,导致前端如果依赖 Array.prototype.sort
接口实现的 JavaScript 代码,在浏览器中实际执行的排序效果是不一致的。
而排序稳定性的差异其实也只是在特定的场景才会体现出问题;在很多情况下,不稳定的排序也并不会造成什么影响。所以假如实际项目开发中,对于数组的排序没有稳定性的需求,那么看到这里就可以了。
但是若项目要求排序必须是稳定的,那么这些差异的存在将无法满足需求。我们需要为此进行一些额外的工作。
举个例子:
某市的机动车牌照拍卖系统,最终中标的规则为:
1 2 | 1. 按价格进行倒排序; 2. 相同价格则按照竞标顺位(即价格提交时间)进行正排序。 |
排序若是在前端进行,那么采用快速排序的浏览器中显示的中标者很可能是不符合预期的。
此外例如:
MySQL 5.6 的 limit M,N
语句实现
等情况都是不稳定排序的特征。
其实这个情况从一开始便存在。
Chrome测试版于2008年9月2日发布 (这里附上当时随版本发布的漫画,还是很有意思的Google on Google Chrome - comic book),然而发布后不久,就有开发者向 Chromium 开发组提交 #90 Bug V8 doesn't stable sort 反馈 v8 的数组排序实现不是稳定排序的。
这个 Bug ISSUE 讨论的时间跨度很大。时至今日,仍然有开发者对 v8 的数组排序的实现提出评论。
同时我们还注意到,该 ISSUE 曾经已被关闭。但是于 2013 年 6 月被开发组成员重新打开,作为 ECMAScript Next 规范讨论的参考。
而 es-discuss 的最后结论是这样的
It does not change. Stable is a subset of unstable. And vice versa, every unstable algorithm returns a stable result for some inputs. Mark’s point is that requiring “always unstable” has no meaning, no matter what language you chose.
这也正如本文前段所引用的已定稿 ECMAScript 2015
规范中的描述一样。
IMHO,Chrome 发布之初即被报告出这个问题可能是有其特殊的时代特点。
上文已经说到,Chrome 第一版是 2008 年 9 月发布的。根据 statcounter 的统计数据,那个时期市场占有率最高的两款浏览器分别是 IE(那时候只有 IE6 和 IE7) 和 Firefox,市场占有率分别达到了 67.16% 和 25.77%。也就是说,两个浏览器加起来的市场占有率超过了 90%。
而根据另一份浏览器排序算法稳定性的统计数据显示,这两款超过了 90% 市场占有率的浏览器都采用了稳定的数组排序。所以 Chrome 发布之初被开发者质疑也是合情合理的。
我们从 ISSUE 讨论的过程中,可以大概理解开发组成员对于引擎实现采用快速排序的一些考量。他们认为引擎必须遵守 ECMAScript 规范。由于规范不要求稳定排序的描述,故他们认为 v8 的实现也是完全符合规范的。
另外,他们认为 v8 设计的一个重要考量在于引擎的性能。
快速排序相比较于归并排序,在整体性能上表现更好:
既然说 v8 非常看中引擎的性能,那么在数组排序中它做了哪些事呢?
通过阅读源代码,还是粗浅地学习了一些皮毛。
目前 v8 的实现是设定一个阈值,对最下层的 10 个及以下长度的小数组使用插入排序。
根据代码注释以及 Wikipedia 中的描述,虽然插入排序的平均时间复杂度为 $O(n^2)$ 差于快速排序的 $O(nlogn)$。但是在运行环境,小数组使用插入排序的效率反而比快速排序会更高,这里不再展开。
1 2 3 4 5 6 7 8 9 10 11 12 | var QuickSort = function QuickSort(a, from, to) { ...... while (true) { // Insertion sort is faster for short arrays. if (to - from <= 10) { InsertionSort(a, from, to); return; } ...... } ...... }; |
正如已知的,快速排序的阿克琉斯之踵在于,最差数组组合情况下会算法退化。
快速排序的算法核心在于选择一个基准(pivot),将经过比较交换的数组按基准分解为两个数区进行后续递归。试想如果对一个已经有序的数组,每次选择基准元素时总是选择第一个或者最后一个元素,那么每次都会有一个数区是空的,递归的层数将达到 n,最后导致算法的时间复杂度退化为 $O(n^2)$。因此 pivot 的选择非常重要。
v8采用的是**三数取中(median-of-three)**的优化:除了头尾两个元素再额外选择一个元素参与基准元素的竞争。
第三个元素的选取策略大致为:
最后取三个元素的中位值作为 pivot。
v8 代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | var GetThirdIndex = function(a, from, to) { var t_array = new InternalArray(); // Use both 'from' and 'to' to determine the pivot candidates. var increment = 200 + ((to - from) & 15); var j = 0; from += 1; to -= 1; for (var i = from; i < to; i += increment) { t_array[j] = [i, a[i]]; j++; } t_array.sort(function(a, b) { return comparefn(a[1], b[1]); }); var third_index = t_array[t_array.length >> 1][0]; return third_index; }; var QuickSort = function QuickSort(a, from, to) { ...... while (true) { ...... if (to - from > 1000) { third_index = GetThirdIndex(a, from, to); } else { third_index = from + ((to - from) >> 1); } } ...... }; |
目前,大多数快速排序算法中大部分的代码实现如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | var quickSort = function(arr) { if (arr.length <= 1) { return arr; } var pivotIndex = Math.floor(arr.length / 2); var pivot = arr.splice(pivotIndex, 1)[0]; var left = []; var right = []; for (var i = 0; i < arr.length; i++){ if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return quickSort(left).concat([pivot], quickSort(right)); }; |
以上代码存在一个问题在于:利用 left 和 right 两个数区存储递归的子数组,因此它需要 $O(n)$ 的额外存储空间。这与理论上的平均空间复杂度 $O(logn)$ 相比差距较大。
额外的空间开销,同样会影响实际运行时的整体速度。(这也是快速排序在实际运行时的表现可以超过同等时间复杂度级别的其他排序算法的其中一个原因。)所以一般来说,性能较好的快速排序会采用原地(in-place)排序的方式。
v8 源代码中的实现是对原数组进行元素交换。
它的背后也是有故事的。
Firefox 其实在一开始发布的时候对于数组排序的实现并不是采用稳定的排序算法,这块有据可考。
Firefox(Firebird) 最初版本实现的数组排序算法是堆排序,这也是一种不稳定的排序算法。因此,后来有人对此提交了一个 Bug。
Mozilla开发组内部针对这个问题进行了一系列讨论。
从讨论的过程我们能够得出几点
基于开发组成员倾向于实现稳定的排序算法为主要前提,Firefox3 将归并排序作为了数组排序的新实现。
以上说了这么多,主要是为了讲述各个浏览器对于排序实现的差异,以及解释为什么存在这些差异的一些比较表层的原因。
但是读到这里,读者可能还是会有疑问:如果我的项目就是需要依赖稳定排序,那该怎么办呢?
其实解决这个问题的思路比较简单。
浏览器出于不同考虑选择不同排序算法。可能某些偏向于追求极致的性能,某些偏向于提供良好的开发体验,但是有规律可循。
从目前已知的情况来看,所有主流浏览器(包括IE6,7,8)对于数组排序算法的实现基本可以枚举:
所以,我们将快速排序经过定制改造,变成稳定排序的是不是就可以了?
一般来说,针对对象数组使用不稳定排序会影响结果。而其他类型数组本身使用稳定排序或不稳定排序的结果是相等的。因此方案大致如下:
面对归并排序这类实现时由于算法本身就是稳定的,额外增加的自然序比较并不会改变排序结果,所以方案兼容性比较好。
但是涉及修改待排序数组,而且需要开辟额外空间用于存储自然序属性,可想而知v8这类引擎应该不会采用类似手段。不过作为开发者自行定制的排序方案是可行的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | ;const INDEX = Symbol('index'); function getComparer(compare) { return function (left, right) { let result = compare(left, right); return result === 0 ? left[INDEX] - right[INDEX] : result; }; } function sort(array, compare) { array = array.map( (item, index) => { if (typeof item === 'object') { item[INDEX] = index; } return item; } ); return array.sort(getComparer(compare)); } |
以上只是一个简单的满足稳定排序的算法改造示例。
之所以说简单,是因为实际生产环境中作为数组输入的数据结构冗杂,需要根据实际情况判断是否需要进行更多样的排序前类型检测。
影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:
待排序的记录数目 n 的大小;
记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
关键字的结构及其分布情况;
对排序稳定性的要求。
设待排序元素的个数为 n。选择的大致方案如下:
当 n 较大,内存空间允许,且要求稳定性则选择归并排序
当 n 较小,可采用直接插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
直接选择排序:元素分布有序,如果不要求稳定性,选择直接选择排序
一般不使用或不直接使用传统的冒泡排序。
基数排序: 它是一种稳定的排序算法,但有一定的局限性,最好满足:
Array.prototype.sort 随机排列数组的错误实现
复杂度:O(WTF)
1 2 3 4 5 | var numbers = [8, 32, 49, 4, 111, 999]; numbers.forEach(item => { setTimeout(() => {console.log(item)},item); }) |
关于排序算法的有关文章已经很多了,然而网络上用 Javascript 语言来作为示例并详实介绍的文章貌似还是不太多。这里主要是我来尝试自己针对网上各式的排序算法进行一份详实的个人总结,从而温故知新。
a
原本在 b
前面,而 a = b
,排序之后 a
仍然在 b
的前面;a
原本在 b
的前面,而 a = b
,排序之后 a
可能会出现在 b
的后面;名词解释:
n
: 数据规模
k
: 桶的个数
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | $O(n^2)$ | $O(n)$ | $O(n^2)$ | $O(1)$ | 内排序 | 稳定 |
选择排序 | $O(n^2)$ | $O(n^2)$ | $O(n^2)$ | $O(1)$ | 内排序 | 不稳定 |
插入排序 | $O(n^2)$ | $O(n)$ | $O(n^2)$ | $O(1)$ | 内排序 | 稳定 |
希尔排序 | $O(n\log n)$ | $O(n(\log_2^2 n))$ | $O(n(\log_2^2 n))$ | $O(1)$ | 内排序 | 不稳定 |
归并排序 | $O(n\log n)$ | $O(n\log n)$ | $O(n\log n)$ | $O(n)$ | 外排序 | 稳定 |
快速排序 | $O(n\log n)$ | $O(n\log n)$ | $O(n^2)$ | $O(\log n)$ | 内排序 | 不稳定 |
堆排序 | $O(n\log n)$ | $O(n\log n)$ | $O(n\log n)$ | $O(1)$ | 内排序 | 不稳定 |
计数排序 | $O(n + k)$ | $O(n + k)$ | $O(n + k)$ | $O(k)$ | 外排序 | 稳定 |
桶排序 | $O(n + k)$ | $O(n + k)$ | $O(n^2)$ | $O(n + k)$ | 外排序 | 稳定 |
基数排序 | $O(n × k)$ | $O(n × k)$ | $O(n × k)$ | $O(n + k)$ | 外排序 | 稳定 |
从分类上来讲:
排序算法 | 交换排序 | 冒泡排序 | 快速排序 |
选择排序 | 选择排序 | 堆排序 | |
插入排序 | 插入排序 | 希尔排序 | |
归并排序 | 归并排序 | ||
分布排序 | 计数排序 | 桶排序 | 基数排序 |
冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地走访要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换时,此时该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
图解如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | const bubbleSort = (arr) => { let len = arr.length; for(let i = 0;i < len;i++){ for(let j = 0;j < len - 1 - i;j++){ if(arr[j] > arr[j+1]){ let temp = arr[j+1]; arr[j+1] = arr[j] arr[j] = temp; } } } return arr; } let arr = [3,44,38,5,47,15,36,26,27,2,46,4,19,50,48]; console.log(bubbleSort(arr)); //[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50] |
改进冒泡排序: 我们设置一个标志性变量
pos
,用于记录每趟排序中最后一次进行交换的位置。由于pos
位置之后的元素均已交换到位,故在进行下一趟排序时只要扫描到pos
位置即可。 这样的优化方式可以在最好情况下把复杂度降到 $O(n)$。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | const bubbleSort2 = (arr) => { let i = arr.length - 1; while(i > 0){ let pos = 0; for(let j = 0; j < i; j++){ if (arr[j] > arr[j+1]){ pos = j; //记录交换的位置 let tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp; } } i = pos; //为下一趟排序作准备 } return arr; } |
另外传统冒泡排序中每一趟排序操作只能找到一个最大值或最小值,我们可以考虑利用在每趟排序中进行正向和反向两遍冒泡的方法来一次得到两个最终值(最大者和最小者),从而继续优化使排序趟数几乎减少一半。(这就是鸡尾酒排序)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | const cooktailSort = (arr) => { let min = 0; let max = arr.length - 1; while(min < max){ for(let j = min;j < max;j++){ if(arr[j] > arr[j+1]){ let tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp; } } -- max; for(let j = max;j > min;j--){ if(arr[j] < arr[j-1]){ let tmp = arr[j] arr[j] = arr[j-1]; arr[j-1] = tmp; } } ++ min; } return arr; } |
冒泡排序对有 $n$ 个元素的项目平均需要 $O(n^2)$ 次比较次数,它可以原地排序,并且是能简单实现的几种排序算法之一,但是它对于少数元素之外的数列排序是很没有效率的。
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
图解如下:
$n$ 个记录的直接选择排序可经过 $n - 1$ 趟直接选择排序得到有序结果。具体算法描述如下:
R[1 ... n]
,有序区为空;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | const SelectionSort = (arr) => { let len = arr.length; let minIndex, tmp; console.time('选择排序耗时'); for (let i = 0;i < len - 1; i++){ minIndex = i; for(let j = i + 1;j < len;j++){ if(arr[minIndex] > arr[j]){ minIndex = j; } } tmp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = tmp; } console.timeEnd('选择排序耗时'); return arr; } |
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对 $n$ 个元素的表进行排序总共进行至多 $n - 1$ 次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。 但原地操作几乎是选择排序的唯一优点,当空间复杂度要求较高时,可以考虑选择排序;实际适用的场合非常罕见。
插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前s扫描,找到相应位置并插入。插入排序在实现上,通常采用 in-place 排序(即只需用到 $O(1)$ 的额外空间的排序),因而在从后向前的扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
图解如下:
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
如果比较操作的代价比交换操作大的话,可以采用二分查找法来减少比较操作的数目。该算法可以认为是插入排序的一个变种,称为二分查找插入排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | const insertionSort = (arr) => { let len = arr.length; console.time('插入排序耗时'); for (let i = 1;i < len; i++){ let key = arr[i]; let j = i - 1; while(j >= 0 && arr[j] > key){ arr[j+1] = arr[j]; j--; } arr[j+1] = key; } console.timeEnd('插入排序耗时'); return arr; } |
改进插入排序:查找插入位置时使用二分查找的方式。
具体思路如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | const binaryInsertionSort = (arr) => { console.time('二分插入排序耗时:'); for (let i = 1; i < arr.length; i++){ let key = arr[i], left = 0, right = i - 1; while (left <= right){ let middle = parseInt((left + right) / 2); if (key < arr[middle]){ right = middle - 1; }else{ left = middle + 1; } } for (let j = i - 1; j >= left; j--){ arr[j + 1] = arr[j]; } arr[left] = key; } console.timeEnd('二分插入排序耗时:'); return arr; } |
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。它是非稳定排序算法。
希尔排序基于插入排序的以下两点性质提出了改进方法:
它与插入排序的不同之处在于,它会优先比较距离较远的元素。因此希尔排序又叫缩小增量排序。
图解如下:
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | const shellSort = (arr) => { console.time('希尔排序耗时:'); let len = arr.length,temp,gap = 1; while(gap < len / 5) { // 动态定义间隔序列步长为 5 gap = gap * 5 + 1; } for (gap; gap > 0; gap = Math.floor(gap / 5)) { for (let i = gap; i < len; i++) { temp = arr[i]; let j; for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) { arr[j + gap] = arr[j]; } arr[j + gap] = temp; } } console.timeEnd('希尔排序耗时:'); return arr; } |
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
图解如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | const mergeSort = (arr) =>{ //采用自上而下的递归方法 let len = arr.length; if(len < 2) { return arr; } let middle = Math.floor(len / 2), left = arr.slice(0, middle), right = arr.slice(middle); return merge(mergeSort(left), mergeSort(right)); } const merge = (left, right) => { let result = []; while (left.length && right.length) { if (left[0] <= right[0]) { result.push(left.shift()); } else { result.push(right.shift()); } } while (left.length) result.push(left.shift()); while (right.length) result.push(right.shift()); return result; } let arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48]; console.time('归并排序耗时'); console.log(mergeSort(arr)); console.timeEnd('归并排序耗时'); |
和选择排序一样,归并排序的性能不受输入数据的影响,但它的表现比选择排序要好,因为它始终都是 $O_(n\log n)$ 的时间复杂度。但代价是需要额外的内存空间。
通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | const quickSort = (arr) => { if (arr.length <= 1) { return arr; } let pivotIndex = Math.floor(arr.length / 2); let pivot = arr.splice(pivotIndex, 1)[0]; let left = []; let right = []; for (let i = 0; i < arr.length; i++){ if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return quickSort(left).concat([pivot], quickSort(right)); }; let arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48]; console.time('快速排序耗时'); console.log(quickSort(arr)); console.timeEnd('快速排序耗时'); |
堆排序可以说是一种利用堆的概念来排序的选择排序。它利用堆这种数据结构所设计。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | const heapSort = (array) => { console.time('堆排序耗时'); //建堆 let heapSize = array.length, temp; for (let i = Math.floor(heapSize / 2) - 1; i >= 0; i--) { heapify(array, i, heapSize); } //堆排序 for (let j = heapSize - 1; j >= 1; j--) { temp = array[0]; array[0] = array[j]; array[j] = temp; heapify(array, 0, --heapSize); } console.timeEnd('堆排序耗时'); return array; } /*方法说明:维护堆的性质 @param arr 数组 @param x 数组下标 @param len 堆大小*/ const heapify = (arr, x, len) => { let l = 2 * x + 1, r = 2 * x + 2, largest = x, temp; if (l < len && arr[l] > arr[largest]) { largest = l; } if (r < len && arr[r] > arr[largest]) { largest = r; } if (largest != x) { temp = arr[x]; arr[x] = arr[largest]; arr[largest] = temp; heapify(arr, largest, len); } } let arr=[91,60,96,13,35,65,46,65,10,30,20,31,77,81,22]; console.log(heapSort(arr)); |
计数排序(Counting sort)是一种稳定的排序算法。计数排序使用一个额外的数组 C,其中第 i 个元素是待排序数组 A 中值等于 i 的元素的个数。然后根据数组 C 来将 A 中的元素排到正确的位置。它只能对整数进行排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | const countingSort = (array) => { let len = array.length, result = [], C = [], min,max; min = max = array[0]; console.time('计数排序耗时'); for (let i = 0; i < len; i++) { min = min <= array[i] ? min : array[i]; max = max >= array[i] ? max : array[i]; C[array[i]] = C[array[i]] ? C[array[i]] + 1 : 1; } for (let j = min; j < max; j++) { C[j + 1] = (C[j + 1] || 0) + (C[j] || 0); } for (let k = len - 1; k >= 0; k--) { result[C[array[k]] - 1] = array[k]; C[array[k]]--; } console.timeEnd('计数排序耗时'); return result; } let arr = [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2]; console.log(countingSort(arr)); |
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 O(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组 C 的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上 1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。
工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | const bucketSort = (array, num) => { if (array.length <= 1) { return array; } var len = array.length, buckets = [], result = [], min = max = array[0], regex = '/^[1-9]+[0-9]*$/', space, n = 0; num = num || ((num > 1 && regex.test(num)) ? num : 10); console.time('桶排序耗时'); for (var i = 1; i < len; i++) { min = min <= array[i] ? min : array[i]; max = max >= array[i] ? max : array[i]; } space = (max - min + 1) / num; for (var j = 0; j < len; j++) { var index = Math.floor((array[j] - min) / space); if (buckets[index]) { // 非空桶,插入排序 var k = buckets[index].length - 1; while (k >= 0 && buckets[index][k] > array[j]) { buckets[index][k + 1] = buckets[index][k]; k--; } buckets[index][k + 1] = array[j]; } else { //空桶,初始化 buckets[index] = []; buckets[index].push(array[j]); } } while (n < num) { result = result.concat(buckets[n]); n++; } console.timeEnd('桶排序耗时'); return result; } var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48]; console.log(bucketSort(arr,4)); |
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。 桶排序最好情况下使用线性时间 $O(n)$,桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为 $O(n)$。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | const radixSort = (arr, maxDigit) => { var mod = 10; var dev = 1; var counter = []; console.time('基数排序耗时'); for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) { for(var j = 0; j < arr.length; j++) { var bucket = parseInt((arr[j] % mod) / dev); if(counter[bucket]== null) { counter[bucket] = []; } counter[bucket].push(arr[j]); } var pos = 0; for(var j = 0; j < counter.length; j++) { var value = null; if(counter[j]!=null) { while ((value = counter[j].shift()) != null) { arr[pos++] = value; } } } } console.timeEnd('基数排序耗时'); return arr; } var arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]; console.log(radixSort(arr,2)); |
基数排序有两种方法:
MSD 从高位开始进行排序 LSD 从低位开始进行排序
基数排序 vs 计数排序 vs 桶排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
基数排序:根据键值的每位数字来分配桶 计数排序:每个桶只存储单一键值 桶排序:每个桶存储一定范围的数值
以上代码可访问 GitHub 具体查看
说到前端排序,自然首先会想到 JavaScript 的原生接口 Array.prototype.sort
这个接口自 ECMAScript 1st Edition
起就被设计存在。而在规范中关于它的描述是这样的:[11]
Array.prototype.sort(compareFn)
The elements of this array are sorted. The sort is not necessarily stable (that is, elements that compare equal do not necessarily remain in their original order). If comparefn is not undefined, it should be a function that accepts two arguments x and y and returns a negative value if x < y, zero if x = y, or a positive value if x > y.
显然,规范里并没有限定 sort
内部需要用什么排序算法。在这样的背景下,前端排序这件事完全取决于各家浏览器内核的具体实现。
Chrome 的 JavaScript 引擎是 v8。由于它是开源的,所以可以直接看源代码。
整个 array.js 都是用 JavaScript 语言实现的。排序方法部分很明显比通常看到的快速排序要复杂得多,但显然核心算法还是快速排序的思想。算法复杂的原因在于 v8 出于性能考虑进行了很多优化。(后续会展开说)
暂时无法确定 Firefox 的 JavaScript 引擎即将使用的数组排序算法会是什么。
按照现有的信息,SpiderMoney 内部实现了归并排序。(这里不多做叙述)
Microsoft Edge 的 JavaScript 引擎 Chakra 的核心部分代码已经于 2016 年初在 Github 开源。
通过源代码可以发现,Chakra 的数组排序算法也主要是以快速排序为主。并针对其他具体特殊情况进行具体优化。
如上所述,快速排序是一种不稳定的排序算法,而归并排序是一种稳定的排序算法。由于不同引擎在算法选择上可能存在差异,导致前端如果依赖 Array.prototype.sort
接口实现的 JavaScript 代码,在浏览器中实际执行的排序效果是不一致的。
而排序稳定性的差异其实也只是在特定的场景才会体现出问题;在很多情况下,不稳定的排序也并不会造成什么影响。所以假如实际项目开发中,对于数组的排序没有稳定性的需求,那么看到这里就可以了。
但是若项目要求排序必须是稳定的,那么这些差异的存在将无法满足需求。我们需要为此进行一些额外的工作。
举个例子:
某市的机动车牌照拍卖系统,最终中标的规则为:
1 2 | 1. 按价格进行倒排序; 2. 相同价格则按照竞标顺位(即价格提交时间)进行正排序。 |
排序若是在前端进行,那么采用快速排序的浏览器中显示的中标者很可能是不符合预期的。
此外例如:
MySQL 5.6 的 limit M,N
语句实现
等情况都是不稳定排序的特征。
其实这个情况从一开始便存在。
Chrome测试版于2008年9月2日发布 (这里附上当时随版本发布的漫画,还是很有意思的Google on Google Chrome - comic book),然而发布后不久,就有开发者向 Chromium 开发组提交 #90 Bug V8 doesn't stable sort 反馈 v8 的数组排序实现不是稳定排序的。
这个 Bug ISSUE 讨论的时间跨度很大。时至今日,仍然有开发者对 v8 的数组排序的实现提出评论。
同时我们还注意到,该 ISSUE 曾经已被关闭。但是于 2013 年 6 月被开发组成员重新打开,作为 ECMAScript Next 规范讨论的参考。
而 es-discuss 的最后结论是这样的
It does not change. Stable is a subset of unstable. And vice versa, every unstable algorithm returns a stable result for some inputs. Mark’s point is that requiring “always unstable” has no meaning, no matter what language you chose.
这也正如本文前段所引用的已定稿 ECMAScript 2015
规范中的描述一样。
IMHO,Chrome 发布之初即被报告出这个问题可能是有其特殊的时代特点。
上文已经说到,Chrome 第一版是 2008 年 9 月发布的。根据 statcounter 的统计数据,那个时期市场占有率最高的两款浏览器分别是 IE(那时候只有 IE6 和 IE7) 和 Firefox,市场占有率分别达到了 67.16% 和 25.77%。也就是说,两个浏览器加起来的市场占有率超过了 90%。
而根据另一份浏览器排序算法稳定性的统计数据显示,这两款超过了 90% 市场占有率的浏览器都采用了稳定的数组排序。所以 Chrome 发布之初被开发者质疑也是合情合理的。
我们从 ISSUE 讨论的过程中,可以大概理解开发组成员对于引擎实现采用快速排序的一些考量。他们认为引擎必须遵守 ECMAScript 规范。由于规范不要求稳定排序的描述,故他们认为 v8 的实现也是完全符合规范的。
另外,他们认为 v8 设计的一个重要考量在于引擎的性能。
快速排序相比较于归并排序,在整体性能上表现更好:
既然说 v8 非常看中引擎的性能,那么在数组排序中它做了哪些事呢?
通过阅读源代码,还是粗浅地学习了一些皮毛。
目前 v8 的实现是设定一个阈值,对最下层的 10 个及以下长度的小数组使用插入排序。
根据代码注释以及 Wikipedia 中的描述,虽然插入排序的平均时间复杂度为 $O(n^2)$ 差于快速排序的 $O(nlogn)$。但是在运行环境,小数组使用插入排序的效率反而比快速排序会更高,这里不再展开。
1 2 3 4 5 6 7 8 9 10 11 12 | var QuickSort = function QuickSort(a, from, to) { ...... while (true) { // Insertion sort is faster for short arrays. if (to - from <= 10) { InsertionSort(a, from, to); return; } ...... } ...... }; |
正如已知的,快速排序的阿克琉斯之踵在于,最差数组组合情况下会算法退化。
快速排序的算法核心在于选择一个基准(pivot),将经过比较交换的数组按基准分解为两个数区进行后续递归。试想如果对一个已经有序的数组,每次选择基准元素时总是选择第一个或者最后一个元素,那么每次都会有一个数区是空的,递归的层数将达到 n,最后导致算法的时间复杂度退化为 $O(n^2)$。因此 pivot 的选择非常重要。
v8采用的是**三数取中(median-of-three)**的优化:除了头尾两个元素再额外选择一个元素参与基准元素的竞争。
第三个元素的选取策略大致为:
最后取三个元素的中位值作为 pivot。
v8 代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | var GetThirdIndex = function(a, from, to) { var t_array = new InternalArray(); // Use both 'from' and 'to' to determine the pivot candidates. var increment = 200 + ((to - from) & 15); var j = 0; from += 1; to -= 1; for (var i = from; i < to; i += increment) { t_array[j] = [i, a[i]]; j++; } t_array.sort(function(a, b) { return comparefn(a[1], b[1]); }); var third_index = t_array[t_array.length >> 1][0]; return third_index; }; var QuickSort = function QuickSort(a, from, to) { ...... while (true) { ...... if (to - from > 1000) { third_index = GetThirdIndex(a, from, to); } else { third_index = from + ((to - from) >> 1); } } ...... }; |
目前,大多数快速排序算法中大部分的代码实现如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | var quickSort = function(arr) { if (arr.length <= 1) { return arr; } var pivotIndex = Math.floor(arr.length / 2); var pivot = arr.splice(pivotIndex, 1)[0]; var left = []; var right = []; for (var i = 0; i < arr.length; i++){ if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return quickSort(left).concat([pivot], quickSort(right)); }; |
以上代码存在一个问题在于:利用 left 和 right 两个数区存储递归的子数组,因此它需要 $O(n)$ 的额外存储空间。这与理论上的平均空间复杂度 $O(logn)$ 相比差距较大。
额外的空间开销,同样会影响实际运行时的整体速度。(这也是快速排序在实际运行时的表现可以超过同等时间复杂度级别的其他排序算法的其中一个原因。)所以一般来说,性能较好的快速排序会采用原地(in-place)排序的方式。
v8 源代码中的实现是对原数组进行元素交换。
它的背后也是有故事的。
Firefox 其实在一开始发布的时候对于数组排序的实现并不是采用稳定的排序算法,这块有据可考。
Firefox(Firebird) 最初版本实现的数组排序算法是堆排序,这也是一种不稳定的排序算法。因此,后来有人对此提交了一个 Bug。
Mozilla开发组内部针对这个问题进行了一系列讨论。
从讨论的过程我们能够得出几点
基于开发组成员倾向于实现稳定的排序算法为主要前提,Firefox3 将归并排序作为了数组排序的新实现。
以上说了这么多,主要是为了讲述各个浏览器对于排序实现的差异,以及解释为什么存在这些差异的一些比较表层的原因。
但是读到这里,读者可能还是会有疑问:如果我的项目就是需要依赖稳定排序,那该怎么办呢?
其实解决这个问题的思路比较简单。
浏览器出于不同考虑选择不同排序算法。可能某些偏向于追求极致的性能,某些偏向于提供良好的开发体验,但是有规律可循。
从目前已知的情况来看,所有主流浏览器(包括IE6,7,8)对于数组排序算法的实现基本可以枚举:
所以,我们将快速排序经过定制改造,变成稳定排序的是不是就可以了?
一般来说,针对对象数组使用不稳定排序会影响结果。而其他类型数组本身使用稳定排序或不稳定排序的结果是相等的。因此方案大致如下:
面对归并排序这类实现时由于算法本身就是稳定的,额外增加的自然序比较并不会改变排序结果,所以方案兼容性比较好。
但是涉及修改待排序数组,而且需要开辟额外空间用于存储自然序属性,可想而知v8这类引擎应该不会采用类似手段。不过作为开发者自行定制的排序方案是可行的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | ;const INDEX = Symbol('index'); function getComparer(compare) { return function (left, right) { let result = compare(left, right); return result === 0 ? left[INDEX] - right[INDEX] : result; }; } function sort(array, compare) { array = array.map( (item, index) => { if (typeof item === 'object') { item[INDEX] = index; } return item; } ); return array.sort(getComparer(compare)); } |
以上只是一个简单的满足稳定排序的算法改造示例。
之所以说简单,是因为实际生产环境中作为数组输入的数据结构冗杂,需要根据实际情况判断是否需要进行更多样的排序前类型检测。
影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:
待排序的记录数目 n 的大小;
记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
关键字的结构及其分布情况;
对排序稳定性的要求。
设待排序元素的个数为 n。选择的大致方案如下:
当 n 较大,内存空间允许,且要求稳定性则选择归并排序
当 n 较小,可采用直接插入或直接选择排序。
直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
直接选择排序:元素分布有序,如果不要求稳定性,选择直接选择排序
一般不使用或不直接使用传统的冒泡排序。
基数排序: 它是一种稳定的排序算法,但有一定的局限性,最好满足:
Array.prototype.sort 随机排列数组的错误实现
这个月主要是继续收尾完善 CMS 并做了一个感恩节主题的 H5 项目,同时由于 CMS 这个项目在前期规划上的交流问题,公司在 C 端方向也开发了 CMS 项目导致在开发资源上一定程度存在重复开发现象,于是上面想把这两个项目合并,合并开发一套 CMS 后台来共同服务我们 B 端和 C 端方向的前台。
而这也算是毕业以来第一次遇到这种算的上是『公司 + 部门』级别的项目合并(一定意义上的跨公司,但是在公司的技术体系上却又并不是分隔的很独立)。中间各种过程不表了。由于两边前端的技术栈比较统一,所以前端合并的开发量主要也就在接口的联调、组件展示逻辑的兼容、以及埋点逻辑等方向上的处理。相比于这个项目的开发来说,项目的复杂点更多的是管理决策的 PK 和方案的确定吧。。
(就像美团和点评的合并吧,这种项目合并中间必定会或多或少伤害其中一个团队或是Team,站在公司的角度上肯定希望最大程度的节约开发资源。所以不太会容忍多个 Team 分别开发各自功能却近似的项目。但是在两边的项目都起到一定规模的时候才发觉。。。确实很伤团队的士气,而作为个人最好得便是在开发层面上自然地尽可能做好自己负责的项目,这样即便当你遇到这种项目资源重复,需要合并时。只要做到比别人的好。这样合并时更多的就会以你的开发方案和代码为主,让别人基于你的代码架构上来开发。你的工作成果也就会得到肯定并且在一定程度上不会浪费)
另外感恩节的 H5 项目主要就是一个针对每个商户的总结性的商户数据 H5 统计展示并最后分享领券的一个互动活动页。除了工期比较紧外,技术上难点并不多,于是也就比较快速的用 jq 来实现具体的移动端的展示和动画效果了。在一些具体的图形数据展示上用了 d3 作了雷达图的分类数据展示等。第一次用 d3 在移动端做项目,发现 d3 还是在移动端上有一些兼容性的问题(例如 SVG 的 dy , dx 等)。还是需要在应用时多注意一下,在平时也更熟悉一下 d3 的一些应用。
今年双 11 又看到了去年双 11 天猫的狂欢城的技术方案总结,有些细节挺有意思的,也了解了这些实时活动的相关设计,容灾机制等方案。天猫双 11 晚会和狂欢城的互动技术方案
由于一直在做移动端的商城,所以这些手势的相关 API 还是比较了解的,但是对于 pinch rotate 这些操作相应的实践就并不多了,通过 AlloyFinger 熟悉了一下。超小 Web 手势库 AlloyFinger 原理
对应到编程领域也很贴切:形成主见 -> 发现不能解释的事情 -> 融汇贯通 -> 以简御繁 -> 运用自如 -> 一览众山小 -> 通透。梁漱溟:思考问题的八层境界
HoloLens 初代到底有多牛,这是一篇关于 HoloLens 硬件、技术原理、细节的 PPT 介绍,内容比较详实。期待 HoloLens 能早日普及。(然而有生之年是否能见到类似 SAO 的产品呢..)
看了月影个人的英语学习方法经验,有一些经验还是很有参考价值的。 例如:翻译技术文章的时候可以根据作者的 GitHub 地址进行深入了解背景甚至交流等等。 实录|月影谈循序渐进的英语学习方法
这个月到了 24 岁,感觉良好。
这个月业余时间没有写太多新的代码,主要因为有一些其他烦心的事,看到 Github 上灰溜溜的四列还是挺惭愧的。需要调整一下心态了。
(本文的题目是本月扇贝某日打卡的每日一句,『Do whatever you do intensely -- 罗伯特亨利』,说的是无论做什么,都要满怀热情。而对于我来讲如何保证热情呢?就是去践行新的计划与目标。)
目前这个月一直在搞公司的 CMS 系统。其实公司这块起步相对也算挺晚的了。这个项目也算是又一次从零开始参与搭建一个项目..
理论上来讲这是公司一个很大的项目,公司的双 11 活动页面很多入口都会走由此系统搭建的各级页面。从 9 月开始从 0 起步进行开发到这个月月底完成两期版本上线。但是在公司里..由于每个项目都是最高优先级..所以这个项目中途都还被插了好几个紧急需求..真的要吐槽下..
这个项目中我主要负责了 CMS 后台设计器部分的页面展示和交互,以及前台部分上微信和 APP 的展示对接。两边的技术栈上我都统一用了 Vue + Vuex + Vuerouter 全家桶,撸起来也还是挺快的。
在开发过程中主要是在后台设计器的制作中回顾了下 drag event 这一事件。用了一个基于 Vue 的拖拽插件 vue-drag-and-drop 来协助实现了设计器里的拖拽控件交互。写了一个点击展示调色板的小插件,准备回头整理一下开源出来。同时后台的设计器由于想回头交给后端方便点,所以也加了 Bootstrap 的样式。并用了 titatoggle 来优化展示 toggle 形式的一个小插件。
最后在双 11 前完成了目前的 CMS 系统,他主要具有以下特性:
架构上来讲,CMS及其周边系统大概为如下的关系:
其中 Weblog 是我们公司自己的埋点分析处理工具。在前台会自动将数据埋到各个组件模块上。auth 系统是权限系统用来审核页面发布流程。GIS 负责来提供城市区域维度的信息,它与商户智能服务共同进行判断使每个用户根据自己的位置信息和用户标签来浏览差异化的页面展示信息。而商品中心提供了基础的商品信息服务。而在后台方案管理模块中我们可以进入设计器和方案预览页去编辑、查看搭建效果。
在前端我们会首次请求拿到此页面的方案信息(包含页面背景,页面名称,模块序列等页面方案层级的信息)在模块序列中会包含这个方案页面的各模块ID,根据各模块ID,我们来反查各模块的内容并填充到页面上,在首次加载时只会加载首屏的模块内容从而也起到一个懒加载的效果。
另外后端会在每个方案生成后产生一个版本ID字段,在复用次数较多的页面,前端会存储这个页面模块的信息内容和布局以及版本号,在请求后端接口时会携带这一版本ID,如果能够匹配,或是请求失败,则会直接调用前端存储的上次内容。这也是降级和性能优化的一个方案。
这里简单说一下这个月看到的技术和其他相关的有意思的东西:
首先分享的还是在做 CMS 项目时刚好看到 JD 他们的 CMS 架构演进分享,毕竟我所在的公司在体量上还相差甚远,从他们的架构中可以去了解其他公司目前的 CMS 系统是如何实现发展的。京东上千页面搭建基石——CMS前后端分离演进史
这个月在忙 CMS 的同时,还参与了 Vue2 中文文档的翻译校对,也算是为一直在使用的 Vue 做出一些贡献..不过在这个翻译校对的过程中也发现,以社区形式贡献的内容输出。质量管理真的是一个需要重点把握的内容。例如某人初次翻译后我认为应该对其进行初次校对后视能力和态度才能再让他参与翻译,这样就很容易发现一些其他人翻译时留下的不负责任的坑。(不要问我为什么有这样的领悟 = =。。)
此外这个月接触到了 BEM 这一 CSS 命名方案,发现这也算是很老的一个概念了。。尽管目前类似 webpack css-loader 使 CSS 已经变得模块化并解决了很多作用域冲突的问题,但在语义化和设计规范的推进下还是准备尝试在个人项目中先试着使用来体验一下感受。 相关资料:
使用案例:
另外看到一个有趣的校招题:用 HTML 和 CSS 画一个笑脸。如下是预期效果和最终实现参考。 demo
这个月还看到了『绿色地球』这一个很有意义的项目,创建人居然选择在我大成都开始创业运行,这就更得关注并支持一下这样的项目,希望它能走的长远,早日把业务拓展到帝都来。感兴趣的朋友可以看看下面的文章来具体了解。
下面是他在一席的演讲,挺值得看看的:
这个月国庆回了次绵阳,在城区逛了很多地方,市中心的公园、警钟街等地方基本和小时候一样还是保留着原来的样子,甚至几年前吐槽的车多了路还是跟九几年一样窄..但是新的变化也是有的 ——
在涪江畔越王楼,曾经的三江广场被彻底翻修,由以前长满野草的河岸变成了沿河散步休憩的步行广场,修的十分的漂亮,两岸江边的建筑墙上也做了光幕,到了晚上在建筑物上可以展现十分靓丽的灯光秀,今年国庆恰逢绵阳主题灯光展,在越王楼上看三江河畔的灯光秀还是很吸引人的。
另外马家巷的小吃 + 平日早餐的(油茶 + 米粉)真的是绵阳一绝。唉,帝都实在是难找如此这般正统风味的米粉和油茶...(所幸找到一家淘宝店卖的油茶很不错..可解相思之苦= =..)
下面是正宗的四川米粉和油茶的样子(我就不说淘宝直接搜油茶,默认搜出来的其他省的冲泡型油茶有多难喝了..)
另外也第一次去了北川地震遗址,感受了生命、生活的无常,珍惜现有的日子和幸福时光吧。
另外 WOW 更新 7.0 后,作为一个单纯的剧情党和战场爱好者,深深地感受到了暴雪对我们这种休闲玩家的恶意,加上网易月卡的补刀。游戏成本一下子陡增(虽然架不过脸好,居然莫名的掉了小德核心橙护腕。。居然战场啥的还能混下去。)然而在 7.1 打了一把 4 个小时才通关的 KLZ 之后。。确实发现 WOW 真的越来越有点肝不动了。。实在是有些可惜吧。。
最后,在这十月的一个月里,我的衣着从:四川的短袖 + 短裤 => 北京的毛衣 + 秋裤。。不禁感慨我国真是幅员辽阔。。(相反帝都的秋天也真是越来越短暂了。。以前这个月里我还总会沉浸在伤秋的氛围里过上好几天日子,这一年感觉都没啥反应。。就已经开始来暖气准备过冬了 = =。。)
原文链接 : Tutorial: How to Bundle JavaScript With Rollup
原文作者 : jlengstorf
译文出自 : 众成翻译
译者 : yangzj1992
校对者: lisa
首发于: 众成翻译
本文将通过一步步的系列教学来学习如何使用更小,更高效的工具 Rollup 来代替 Webpack 和 Browserify 打包 JavaScript 文件。
这周,我们将第一次用 Rollup 来构建我们的项目,它是一款用来打包 Javascript 代码的构建工具(它同样支持样式表,但我们将在下周单独介绍这一点)
译者注:原文系列文章如下(本文为第一篇)
Part I: How to Use Rollup to Process and Bundle JavaScript Files
Part II: How to Use Rollup to Process and Bundle Stylesheets
Part III: How to Use Rollup to Watch and Live Reload Files During Development
通过本教程,我们会了解到以下 Rollup 的相关配置方式:
npm
(还没有安装? 在这安装 Node.js)Rollup 是下一代的 JavaScript 模块打包工具。当你使用 ES2015 模块来编写你的应用或者库时,它可以对它们有效的打包来成为一个单独文件供浏览器和 Node.js 使用。
这与 Browserify 和 webpack 很相似。
你也可以把 Rollup 称为一个构建工具,类似于 Grunt 和 Gulp。然而,你需要重点注意的是当你使用 Grunt 和 Gulp 去处理类似创建 JavaScript bundles 的任务时,这些工具在底层也会像 Rollup, Browserify 或是 webpack 一样去使用一些相同的方式来处理。
Rollup 如此令人兴奋的原因在于它能够保持文件体积更小。这听上去很傻瓜,所以 tl;dr 版本在这:相比于其他工具创建的 JavaScript bundles,Rollup 总是会创建相比之更小,更快的 bundle。
之所以会这样是因为 Rollup 是基于 ES2015 模块的,它相比于 webpack 和 Browserify 所使用的 CommonJS 模块更加具有效率,另外 Rollup 也会使用一种叫 tree-shaking 的特性来更容易的移除模块中未使用的代码,这意味着在最终的 bundle 中只有我们实际需要的代码。
Tree-shaking 在我们引用了包含很多可用的函数或方法的第三方工具或框架时就会变得十分重要。例如我们只使用它们中的一两个方法时 —— 像 lodash 或 jQuery 这样的库就会在加载时产生许多额外的开销。
目前 Browserify 和 webpack 在最终生成时仍会包含大量未使用的代码(译者注:在 webpack2 中也引入了 tree-shaking 特性)。但 Rollup 并不如此 —— 它只会生成我们最终实际使用的代码。
(2016 年 8 月 22 日更新) 澄清一下, Rollup 只会在 ES 模块中支持 tree-shaking 特性。目前依照 CommonJS 模块所编写的 lodash 和 jQuery 不能被支持 tree-shaken。然而 tree-shaking 并不是 Rollup 唯一的速度和性能上的优势。可以看这些文章了解更多信息Rich Harris’s explanation、Nolan Lawson’s added info
为了展示 Rollup 是如何运作的,让我们一起针对一个十分简单的项目用 Rollup 处理打包 JavaScript。
开始之前,我们需要一个项目来进行工作。在此教学中,我们将会根据此 Github 项目来进行展示。
目录结构是这样的:
1 2 3 4 5 6 7 8 9 10 | learn-rollup/ ├── src/ │ ├── scripts/ │ │ ├── modules/ │ │ │ ├── mod1.js │ │ │ └── mod2.js │ │ └── main.js │ └── styles/ │ └── main.css └── package.json |
你可以在你的终端中执行下面的命令来安装此项目,在本教学中我们将通过此项目进行展示。
1 2 3 4 5 6 7 | # Move to the folder where you keep your dev projects. cd /path/to/your/projects # Clone the starter branch of the app from GitHub. git clone -b step-0 --single-branch https://github.com/jlengstorf/learn-rollup.git # The files are downloaded to /path/to/your/projects/learn-rollup/ |
首先,通过下面的命令安装 Rollup:
1
| npm install --save-dev rollup
|
接下来,在 learn-rollup
文件夹中创建一个新文件 rollup.config.js
。之后在文件中添加下面的内容:
1 2 3 4 5 6 | export default { entry: 'src/scripts/main.js', dest: 'build/js/main.min.js', format: 'iife', sourceMap: 'inline', }; |
下面是每一个配置选项都做了些什么:
entry
—— 这是我们希望 Rollup 去执行的文件。在大多数项目中,这将是你主要的 JavaScript 文件,它负责一切的初始化工作并作为开始文件。
dest
—— 这是脚本程序执行后所存储的位置。
format
—— Rollup 支持多种输出格式。因为我们在浏览器中运行,我们希望使用立即调用的函数表达式(immediately-invoked function expression,IIFE)。[1]
sourceMap
—— 如果有 sourcemap 的话,那么在调试代码时会提供很大的帮助,这个选项会在生成文件中添加 sourcemap,来让事情变得更加简单。
注意: 关于其他的 format
选项以及什么场合你可能会需要它们,可以参考 Rollup’s wiki
一旦我们创建了配置文件,就可以在我们的终端里运行下面的代码进行测试了:
1
| ./node_modules/.bin/rollup -c
|
这样会在你的项目中创建一个新的 build
文件夹,它包含了一个 js
的子文件夹,在 js
文件夹中还包含了我们生成的 main.min.js
文件。
我们在浏览器中打开 build/index.html
可以看到 bundle 已经被正确创建了。
注意: 在这个阶段,只有现代浏览器会正常工作并不产生错误。如果要让不支持ES2015/ES6
的旧版本的浏览器也正常工作,我们需要添加一些插件。
Rollup 如此强大的原因在于它的 “tree-shaking” 特性,它会将我们引用的模块中未使用的代码剥离。例如,在 src/scripts/modules/mod1.js
中有一个名为 sayGoodbyeTo()
的函数并未在你的项目中使用 —— 既然它不会被使用,那么 Rollup 在最后的 bundle 中就不会包含它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | (function () { ;/** * Says hello. * @param {String} name a name * @return {String} a greeting for `name` */ function sayHelloTo( name ) { const toSay = `Hello, ${name}!`; return toSay; } /** * Adds all the values in an array. * @param {Array} arr an array of numbers * @return {Number} the sum of all the array values */ const addArray = arr => { const result = arr.reduce((a, b) => a + b, 0); return result; }; // Import a couple modules for testing. // Run some functions from our imported modules. const result1 = sayHelloTo('Jason'); const result2 = addArray([1, 2, 3, 4]); // Print the results on the page. const printTarget = document.getElementsByClassName('debug__output')[0]; printTarget.innerText = `sayHelloTo('Jason') => ${result1}\n\n` printTarget.innerText += `addArray([1, 2, 3, 4]) => ${result2}`; }()); //# sourceMappingURL=data:application/json;charset=utf-8;base64,... |
而在其他的构建工具中却并不会如此,因此如果我们引用了一个很大的库如 lodash 却只为了使用它一到两个方法的话,最后的 bundles 会十分的巨大。
例如,使用 webpack 的话,sayGoodbyeTo()
函数就会被引入,并且最终的 bundle 体积相比于 Rollup 所生成的 bundle 要大两倍还多。[2]
此时,我们获得了可以在现代浏览器中运行的代码包,然而如果访问的浏览器仍是旧版本的话那么就会产生错误 —— 这样不太理想。
幸好,Babel 已经提供了支持。它能够帮助我们转译 JavaScript 新特性(ES6/ES2015 等等)到 ES5 版本,这也将支持目前所有的浏览器来正常运行代码。
如果你从未使用过 Babel,那么从今以后你作为开发者的日子就要永远改变了。去尝试 JavaScript 的新特性可以让你的语言变得更简单、干净,让你的开发更加愉快。
所以让我们立刻开始 Rollup 的这一部分吧。
首先,我们需要安装 Babel Rollup 插件 和 合适的 Babel preset
1 2 3 4 5 | # Install Rollup’s Babel plugin. npm install --save-dev rollup-plugin-babel # Install the Babel preset for transpiling ES2015 using Rollup. npm install --save-dev babel-preset-es2015-rollup |
注意: Babel preset 是一个有关 Babel 插件的集合,它会告诉 Babel 我们实际上想要转译什么。
.babelrc
接下来,在你的项目根目录(learn-rollup/
)创建一个名为 .babelrc
的新文件,在它内部添加以下 JSON 内容:
1 2 3 | { "presets": ["es2015-rollup"], } |
这会告诉 Babel 它应该使用哪种 preset 来转译代码。
rollup.config.js
要让它能够真正运行,我们需要更新 rollup.config.js
。
在 rollup.config.js
中,我们需要 import
Babel 插件,将它添加到一个新的配置选项 plugins
中,它会管控一个数组形式的插件列表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // Rollup plugins import babel from 'rollup-plugin-babel'; export default { entry: 'src/scripts/main.js', dest: 'build/js/main.min.js', format: 'iife', sourceMap: 'inline', plugins: [ babel({ exclude: 'node_modules/**', }), ], }; |
为了避免转译第三方脚本,我们需要设置一个 exclude
的配置选项来忽略掉 node_modules
目录
安装和配置完成后,我们可以重新构建 bundle:
1 2 3 | ./node_modules/.bin/rollup -c // 译者注:若执行报错,运行 npm install --save-dev babel-preset-es2015 具体issue 详情见:https://github.com/jlengstorf/learn-rollup/issues/2 |
当我们观察输出时,它看上去貌似没有什么改变。然而实际上它还是有一些细微的区别的:例如, addArray()
函数:
1 2 3 4 5 6 7 | var addArray = function addArray(arr) { var result = arr.reduce(function (a, b) { return a + b; }, 0); return result; }; |
这里可以看到 Babel 如何转换 箭头表示函数 (arr.reduce((a, b) => a + b, 0)
) 到一个常规函数。
在转译运行完成后,程序执行的结果依然相同,但是代码已经支持到了 IE9 之前的浏览器。
重点: Babel 也提供了 babel-polyfill
,它可以让类似像 Array.prototype.reduce()
的代码可以在 IE8 以及更早的浏览器上能够得到顺利执行。
在你的代码中使用 linter 无疑是十分好的决定,因为它会强制执行一致的编码规范来帮助你捕捉像是漏掉了括弧这种棘手的 bug。
在这个项目中,我们将会使用 ESLint。
为了使用 ESLint,我们将要安装 ESLint Rollup plugin
1
| npm install --save-dev rollup-plugin-eslint
|
.eslintrc.json
。为了确保我们只获取我们想要的错误,我们需要首先配置 ESLint。这里可以通过下面的代码来自动生成大多数配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 | $ ./node_modules/.bin/eslint --init ? How would you like to configure ESLint? Answer questions about your style ? Are you using ECMAScript 6 features? Yes ? Are you using ES6 modules? Yes ? Where will your code run? Browser ? Do you use CommonJS? No ? Do you use JSX? No ? What style of indentation do you use? Spaces ? What quotes do you use for strings? Single ? What line endings do you use? Unix ? Do you require semicolons? Yes ? What format do you want your config file to be in? JSON Successfully created .eslintrc.json file in /Users/jlengstorf/dev/code.lengstorf.com/projects/learn-rollup |
如果你回答了上述的问题,你将会在 .eslintrc.json
中获得以下输出内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | { "env": { "browser": true, "es6": true }, "extends": "eslint:recommended", "parserOptions": { "sourceType": "module" }, "rules": { "indent": [ "error", 4 ], "linebreak-style": [ "error", "unix" ], "quotes": [ "error", "single" ], "semi": [ "error", "always" ] } } |
.eslintrc.json
。然而,我们还需要去做一些调整来避免我们的项目出现问题:
我们要用 2 缩进符来代替 4 缩进符。
我们将使用一个全局变量 ENV
,所以我们需要为它设置一个白名单。
所以我们来做以下调整 —— 在你的 .eslintrc.json
中修改 globals
属性和 indent
属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | { "env": { "browser": true, "es6": true }, "globals": { "ENV": true }, "extends": "eslint:recommended", "parserOptions": { "sourceType": "module" }, "rules": { "indent": [ "error", 2 ], "linebreak-style": [ "error", "unix" ], "quotes": [ "error", "single" ], "semi": [ "error", "always" ] } } |
rollup.config.js
。接下来,import
ESLint 插件并将它添加到 Rollup 配置中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // Rollup plugins import babel from 'rollup-plugin-babel'; import eslint from 'rollup-plugin-eslint'; export default { entry: 'src/scripts/main.js', dest: 'build/js/main.min.js', format: 'iife', sourceMap: 'inline', plugins: [ babel({ exclude: 'node_modules/**', }), eslint({ exclude: [ 'src/styles/**', ] }), ], }; |
首先,我们运行 ./node_modules/.bin/rollup -c
,然而好像并没有发生什么,这是因为在标准设置下,应用代码已经顺利通过了 linter 的检查并且没有发现任何问题。
但是如果我们引入一个问题 —— 比如移除一个分号 —— 我们则会看到 ESLint 的帮助提示:
1 2 3 4 5 6 | $ ./node_modules/.bin/rollup -c /Users/jlengstorf/dev/code.lengstorf.com/projects/learn-rollup/src/scripts/main.js 12:64 error Missing semicolon semi ✖ 1 problem (1 error, 0 warnings) |
像这样一些无意间引入的神秘 bug 就会被立刻发现,帮助信息中也包含了文件名,行数以及列数。
尽管这并不会消除我们项目中所有需要调试的问题,但对明显的拼写错误和疏忽起到了相当大的帮助。[3]
如果你的依赖项使用了 Node 模式的模块那么下面的插件是很重要的。如果没有它,你将会在 require
时产生错误。
在这个示例项目中如果没有引用第三方模块的话将会变得很麻烦,但这并不是让你将第三方模块剪切到实际项目中。所以,为了使我们的 Rollup 更方便使用,让我们为代码添加引用第三方模块的功能。
为了简单起见,我们将在代码中添加一个 debug
包来简单的记录日志,通过下面的命令安装:
1
| npm install --save debug
|
注意: 由于这将被引用到主项目中,使用 --save
参数是很重要的,这将避免在生产环境中由于 devDependencies
没有被安装而导致的错误。
然后,在 src/scripts/main.js
中,我们添加一些简单的日志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // Import a couple modules for testing. import { sayHelloTo } from './modules/mod1'; import addArray from './modules/mod2'; // Import a logger for easier debugging. import debug from 'debug'; const log = debug('app:log'); // Enable the logger. debug.enable('*'); log('Logging is enabled!'); // Run some functions from our imported modules. const result1 = sayHelloTo('Jason'); const result2 = addArray([1, 2, 3, 4]); // Print the results on the page. const printTarget = document.getElementsByClassName('debug__output')[0]; printTarget.innerText = `sayHelloTo('Jason') => ${result1}\n\n`; printTarget.innerText += `addArray([1, 2, 3, 4]) => ${result2}`; |
到目前为止一切顺利,但是当我们运行 rollup 时我们会得到一个警告:
1 2 3 | $ ./node_modules/.bin/rollup -c Treating 'debug' as external dependency No name was provided for external module 'debug' in options.globals – guessing 'debug' |
如果我们再一次检查我们的 index.html
,我们会发现 debug
会抛出一个 ReferenceError
通常情况下,第三方 Node 模块并不会被 Rollup 正确加载
这是由于 Node 模块使用的是 CommonJS,它并不被 Rollup 兼容因此不能直接使用。为了解决它,我们需要添加一些插件来处理 Node 依赖和 CommonJS 模块。
为了解决这个问题,我们准备为 Rollup 添加两个插件:
rollup-plugin-node-resolve
, 它会允许加载在 node_modules
中的第三方模块。
rollup-plugin-commonjs
, 它会将 CommonJS 模块转换为 ES6,来为 Rollup 获得兼容。
用下面的命令安装这两个插件:
1
| npm install --save-dev rollup-plugin-node-resolve rollup-plugin-commonjs
|
rollup.config.js
.接下来,在 Rollup 配置中 import
来添加插件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // Rollup plugins import babel from 'rollup-plugin-babel'; import eslint from 'rollup-plugin-eslint'; import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; export default { entry: 'src/scripts/main.js', dest: 'build/js/main.min.js', format: 'iife', sourceMap: 'inline', plugins: [ resolve({ jsnext: true, main: true, browser: true, }), commonjs(), eslint({ exclude: [ 'src/styles/**', ] }), babel({ exclude: 'node_modules/**', }), ], }; |
注意: jsnext
属性是指定将 Node 包转换为 ES2015 模块。main
和 browser
属性将使插件决定将哪些文件应用到 bundle。
用 ./node_modules/.bin/rollup -c
重新构建 bundle,然后在浏览器中再次检查输出:
好的!我们的日志现在正常展示了。
环境变量能为我们的开发流程提供很大的帮助,我们可以通过它来执行像是关闭或开启日志、注入开发环境脚本等功能。
所以让我们来确保 Rollup 能够使用这一特性。
main.js
中添加基础配置 ENV
。让我们添加一个环境变量来使我们的日志脚本只在非 production
环境下才会执行。在 src/scripts/main.js
中让我们改变 log()
初始化后的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 | // Import a logger for easier debugging. import debug from 'debug'; const log = debug('app:log'); // The logger should only be disabled if we’re not in production. if (ENV !== 'production') { // Enable the logger. debug.enable('*'); log('Logging is enabled!'); } else { debug.disable(); } |
然而,当我们重新构建我们的 bundle (./node_modules/.bin/rollup -c
) 并检查浏览器,我们可以看到 ENV
报出了 ReferenceError
的错误。
这并不奇怪,因为我们并没有在所有位置定义它,我们试着运行 ENV=production ./node_modules/.bin/rollup -c
,然而它仍然没有正常工作。这是由于用这种方式设置环境变量只会对 Rollup 生效,对 Rollup 生成的 bundle 并不起作用。
我们仍需要一个插件来将我们的环境变量作用到 bundle 中。
首先安装 rollup-plugin-replace
,它本质上是一个用来查找和替换的工具。它可以做很多事,但对我们来说只需要找到目前的环境变量并用实际值来替代就可以了。(例如:在 bundle 中出现的所有 ENV
将被 "production"
替换)
1
| npm install --save-dev rollup-plugin-replace
|
rollup.config.js
在 rollup.config.js
中, 让我们 import
插件并添加到我们的插件列表里。
配置很简单:我们可以添加一个 key:value
的配对表,key
值是准备被替换的键值,而 value
是将要被替换的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // Rollup plugins import babel from 'rollup-plugin-babel'; import eslint from 'rollup-plugin-eslint'; import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; import replace from 'rollup-plugin-replace'; export default { entry: 'src/scripts/main.js', dest: 'build/js/main.min.js', format: 'iife', sourceMap: 'inline', plugins: [ resolve({ jsnext: true, main: true, browser: true, }), commonjs(), eslint({ exclude: [ 'src/styles/**', ] }), babel({ exclude: 'node_modules/**', }), replace({ ENV: JSON.stringify(process.env.NODE_ENV || 'development'), }), ], }; |
在我们的配置中,我们会找到每一个 ENV
并用 process.env.NODE_ENV
去替换 —— 在 Node 应用或是 development
中我们会用传统的方式去设置环境。我们会使用 JSON.stringify
来确保值是双引号的,不像 ENV
这样。
首先,重新构建 bundle 并在浏览器中检查。此时控制台应该像以前一样已经输出显示了 —— 这意味着我们的默认值被接受了。
为了查看真实的效果,让我们在 production
环境中运行下面代码
1
| `NODE_ENV=production ./node_modules/.bin/rollup -c`
|
注意: 在 Windows 中,使用 SET NODE_ENV=production ./node_modules/.bin/rollup -c
来避免在设置环境变量时产生错误。
当我们重新加载浏览器时,在控制台中就不再有输出内容了:
这样我们通过零代码修改,仅使用一个环境变量就禁用了日志记录。
本教程最后一步的任务是添加 UglifyJS 来最小化压缩 bundle。它可以通过移除注上释、缩短变量名、重整代码来极大程度的减少 bundle 的体积大小 —— 这样在一定程度降低了代码的可读性,但是在网络通信上变得更有效率。
我们将会使用 UglifyJS 来压缩 bundle,这里我们通过 rollup-plugin-uglify
来实现。
用下面的命令来安装:
1
| npm install --save-dev rollup-plugin-uglify
|
rollup.config.js
接下来,让我们在 Rollup 配置中添加 Uglify 。然而,为了在开发中使代码更具可读性,让我们来设置只在生产环境中压缩混淆代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // Rollup plugins import babel from 'rollup-plugin-babel'; import eslint from 'rollup-plugin-eslint'; import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; import replace from 'rollup-plugin-replace'; import uglify from 'rollup-plugin-uglify'; export default { entry: 'src/scripts/main.js', dest: 'build/js/main.min.js', format: 'iife', sourceMap: 'inline', plugins: [ resolve({ jsnext: true, main: true, browser: true, }), commonjs(), eslint({ exclude: [ 'src/styles/**', ] }), babel({ exclude: 'node_modules/**', }), replace({ ENV: JSON.stringify(process.env.NODE_ENV || 'development'), }), (process.env.NODE_ENV === 'production' && uglify()), ], }; |
我们使用了短路计算策略,这是一种常见的(尽管有些讨厌)捷径来在条件性的情况下设置一个值。[4]
在我们的例子中,我们只会在 NODE_ENV
设置为 production
时加载 uglify()
。
当配置保存后,让我们设置 NODE_ENV
为 production 并运行 Rollup:
1
| `NODE_ENV=production ./node_modules/.bin/rollup -c`
|
注意: 在 Windows 中, 使用 SET NODE_ENV=production ./node_modules/.bin/rollup -c
来避免在设置环境变量时产生错误。
这样的输出并不整洁,但它的体积十分小。
在之前,我们的 bundle 大小是 42KB。 在通过 UglifyJS 运行后,它减少到了 29KB —— 这样我们在没有额外付出的情况下就节省了超过 30% 的文件体积。
The cost of small modules —— 是这篇文章让我对 Rollup 产生了兴趣,因为它展示了 Rollup 相比 webpack 和 Browserify 的一些显著的优势。
本文中的代码被托管在 GitHub 上。你可以 fork 项目 来修改并测试它,开启一个 issue 来报告 bug,或是 创建一个 PR来提出改进或修改。
这是一个相当复杂难以理解的概念, 所以如果不能完全明白也不要感到有压力。简而言之,我们希望我们的代码能在我们的作用域内,从而避免与其他的脚本产生冲突。这里 IIFE 是一个包含我们的代码并在它自身作用域产生的闭包 ↩
重要的是需要记住,目前我们处理的是这样的一个小例子,这并不是很复杂就使文件大小大了一倍。在这时文件大小的比对是 3KB 和 8KB。 ↩
就像之前花费无数个小时去追踪一个 bug,最终发现原因仅仅是傻傻的变量名拼写错误一样。linter 对工作效率的提高十分明显,这并不是我们夸大其词。 ↩
例如,我们很常见到用这样的方式来指定默认值(例如: var foo = maybeThisExists || 'default';
)。 ↩
原文地址:Writing better CSS with currentColor
原文作者:Alkshendra Maurya
译文出自:掘金翻译计划
译者:yangzj1992
校对者: linpu.li, Nicolas(Yifei) Li
首发于: 掘金
总有一些极其强大的 CSS 属性在目前已经有了很好的浏览器支持,但却很少被开发者使用。 currentColor
就是这样的属性之一。
MDN 把 currentColor 定义为:
currentColor
代表了当前元素被应用上的 color 颜色值。它允许让继承自属性或子元素属性的 color 属性为默认值而不再继承。
在本文中,我们将通过一些有趣的方式来概述如何使用 CSS currentColor
这一关键字。
currentColor
关键字按某种规则获取了 color 属性的值并赋值给了自身。
在任何你想要默认继承 color
属性值的地方都可以使用 currentColor
这一关键字。这样当你改变 color
关键字的属性值时,它会自动的通过规则反映在所有 currentColor
关键字使用的地方。这难道不是很棒吗?😀
1 2 3 4 5 | .box { color: red; border: 1px solid currentColor; box-shadow: 0 0 2px 2px currentColor; } |
在上面的代码片段里,你可以看到我们不是在所有的地方都重复相同的 color 值,而是用 currentColor 来代替。这使得 CSS 变得更加容易管理,你将不再需要在不同的地方来追踪 color 值
来看一下 currentColor
可能的用例和例子:
简化 color 定义
像链接,边框,图标以及阴影的值总是随着它们的父元素 color 值保持一致,这可以通过简化的 currentColor 来替换一遍又一遍的特定 color 值;从而使代码更加易于管理。
例如:
1 2 3 4 5 6 7 8 9 10 | .box { color: red; } .box .child-1 { background: currentColor; } .box .child-2 { color: currentColor; border 1px solid currentColor; } |
在上面的代码片段中,你可以看到我们不是在边框、阴影上指定一个颜色,而是在这些属性上使用了 currentColor
,这将使它们自动变为 red
。
简化过渡和动画
currentColor 可以使 transitions 和 animations 变得更加简单。
让我们考虑一下最早的代码示例,并且改变一下 hover 时的 color
值。
1 2 3 | .box:hover { color: purple; } |
这里,我们不需要再在 :hover
里写三个不同的属性,我们只需改变 color
值;所有使用 currentColor
的属性会自动在 hover 时发生改变。
在伪元素上使用
像是:before
和 :after
这样的伪元素也同样可以通过用 currentColor 来获取它的父元素的值。这就可以用于创建带有动态颜色的『提示框』,或是使用 body 颜色的『覆盖层』,并给它一个半透明的效果。
1 2 3 4 5 6 7 | .box { color: red; } .box:before { color: currentColor; border: 1px solid currentColor; } |
这里,:before
伪元素的 color
和 border-color
会从父元素 div 中获得并可以被组建成类似提示框的东西。
在 SVG 中使用
SVG 中 currentColor
的值同样可以从父元素中获取。当你在不同地方应用 SVG 并想从父元素中继承 color 值而又不想每次明确提及时,使用它是相当有帮助的。
1 2 3 | svg { fill: currentColor; } |
在这里,svg 将会使用与它父元素相同的填充颜色,并且会动态的随着父元素颜色的修改而发生变化。
在渐变中使用
currentColor
可以同样用于创建 CSS 渐变,其中渐变属性的一部分可以被设置成父元素的 currentColor
。
1 2 3 | .box { background: linear-gradient(top bottom right, currentColor, #FFFFFF); } |
在这里,顶部的渐变颜色将会总是与父元素保持一致。虽然在这种情况下只会有一个动态颜色的限制,但对基于父元素颜色来生成动态的渐变来说,这仍然是一个简洁的方法。
这儿有一个 Codepen 示例来演示上述的所有例子。
CSS currentColor
是从 CSS3 引入 SVG 规范时产生的,自 2003 年以来一直存在。因此浏览器对 currentColor
的支持是很可靠的,除了 IE8 和一些更低版本的浏览器。
下面这张图展示了目前有关浏览器支持情况的信息,信息来自 caniuse.com:
CSS currentColor
尽管是一个很好的特性,但还尚未得到充分运用。它提供了很棒的支持并带来了相当的可能性来使你保持你的代码更加的整洁。
尽管 CSS 变量有它自己的方式,但是养成使用 currentColor
的习惯还是很酷的。
这只是一个我发现的很有趣的简单的话题,如果有人也对此话题感兴趣。请让我知道你的想法并在下面留言!😊
]]>前两天不经意间又看到了极客公园几年前发布的创新大会的宣传视频。不禁还是有些感慨。
当时我还在读大学,看到的是 2013 年版的极客公园宣传视频,后来又看到了 2011 年版的。这两个视频里各个团队所展现的青春与活力一直是我当时很向往的。在当时,这些产品也或多或少算是一时风头的新兴人气产品。然而随着时间的流逝,也正如视频里所说的那样:很少有产品能一直流行。
这里面的一些产品,可能有的也真的只是昙花一现,能坚持到现在还绽放傲人成绩的并不多。在工作之后也是更深刻的懂得了每个产品的背后真的是许多研发产品运营团队的同事们的共同努力和支撑所维系的。做好一个让公司满意、让用户满意的产品真的很不容易。
不管它们最终的结局时怎样,是犯了哪些错误导致了产品的落幕,然而在当时那几年,它们确实是同类产品中的佼佼者,我们的世界也确实因为它们的存在而与众不同过。
刚实习时,老大也曾对我说过,一个优秀的工程师一定要对自己手上的产品负责。在这里也再次勉励自己,希望在自己的研发生涯里能尽可能的让自己所负责的产品尽可能的更优秀。
这里也晒一张我所在的团队的照片,但也是去年12月的照片了。之后团队因为公司和个人的原因也换组、调离、出出入入了不少人。但觉得至少这张照片很能展现当时我们团队阳光、青春的模样,也以此作纪念。
最后,两个视频的 BGM 也很不错,很喜欢它们的歌词和旋律。分别是在格莱美获奖的 Rascal Flatts -《Bless The Broken Road》和 Taylor Swift -《Long Live》
]]>今天下午,我莫名无法向 GitHub push 代码了...最终从5点多调试到晚上11点半..感觉略坑..遂记录如下..
报错内容:
1
| fatal: unable to access 'https://***.git/': SSL peer handshake failed, the server most likely requires a client certificate to connect
|
1 2 | ssh_exchange_identification: Connection closed by remote host fatal: Could not read from remote repository. |
首先简单搜了一下发现可能是 ssh key 的问题..遂重新按照官方文档重新生成 ssh key。并再添加 key 后执行 ssh -T git@github.com
来测试,仍然报错:
ssh_exchange_identification: Connection closed by remote host
执行ssh -vT git@github.com
输出记录如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | $ ssh -vT git@github.com OpenSSH_6.9p1, LibreSSL 2.1.8 debug1: Reading configuration data /etc/ssh/ssh_config debug1: /etc/ssh/ssh_config line 21: Applying options for * debug1: Connecting to github.com [1.111.11.111] port 22. debug1: Connection established. debug1: identity file /Users/yangzhongjing/.ssh/id_rsa type 1 debug1: key_load_public: No such file or directory debug1: identity file /Users/yangzhongjing/.ssh/id_rsa-cert type -1 debug1: key_load_public: No such file or directory debug1: identity file /Users/yangzhongjing/.ssh/id_dsa type -1 debug1: key_load_public: No such file or directory debug1: identity file /Users/yangzhongjing/.ssh/id_dsa-cert type -1 debug1: key_load_public: No such file or directory debug1: identity file /Users/yangzhongjing/.ssh/id_ecdsa type -1 debug1: key_load_public: No such file or directory debug1: identity file /Users/yangzhongjing/.ssh/id_ecdsa-cert type -1 debug1: key_load_public: No such file or directory debug1: identity file /Users/yangzhongjing/.ssh/id_ed25519 type -1 debug1: key_load_public: No such file or directory debug1: identity file /Users/yangzhongjing/.ssh/id_ed25519-cert type -1 debug1: Enabling compatibility mode for protocol 2.0 debug1: Local version string SSH-2.0-OpenSSH_6.9 ssh_exchange_identification: Connection closed by remote host |
这里面/Users/yangzhongjing/.ssh/id_rsa
这个文件是存在的。不知道为什么会报:key_load_public: No such file or directory
..
后来在网上搜了很多内容,执行了以下方法:
vim /etc/hosts.allow
添加 sshd: ALL
ssh-add ~/.ssh/id_rsa
/etc/ssh/sshd_config
中的 MaxSessions 10
调大~/.ssh
目录重新生成ssh key然而并没有什么卵用...
最后在茫茫文海中搜到了一篇关于路由器端口禁用也可能会导致这个问题的留言..突然想到昨天我自己也调试了家里的路由器,添加了一些路由器插件...赶紧切到路由器后台,发现了幕后凶手...
最终..禁用插件,重启路由。问题解决..
SO...后来人可以试试上面列举到的搜索的方法..或者看看你的网络设置,一般可以解决这一报错问题..
]]>原文链接 : JavaScript variables lifecycle: why let is not hoisted
原文作者 : Dmitri Pavlutin
译文出自 : 众成翻译
译者 : yangzj1992
校对者: lisa
首发于: 众成翻译
变量提升是一个将变量或者声明函数提升到作用域起始处的过程,通常指的是变量声明 var
和函数声明 function fun() {...}
当 let
(以及具备了和 let
相似声明行为的 const
和 class
)等声明方式在 ES2015 中被引入后,许多的开发者包括我都使用了变量提升的定义来描述变量是如何被访问的。但经过对这个问题更多的搜索后,我十分惊讶的发现变量提升并不是可以用来准确描述 let
变量初始化和可用性的合适术语。
ES2015 为 let
提供了一个不同的改进机制。它要求了更严格的变量声明方式(你在定义变量前是无法访问它的)并且这也在结果上保证了更好的代码质量。
现在让我们一起深入了解关于这个过程的更多细节。
var
变量提升有时我会在作用域下的任何位置上看到一个奇怪的变量声明 var varname
和函数声明 function funName() {...}
。
1 2 3 4 5 6 7 8 9 10 11 | // var hoisting num; // => undefined var num; num = 10; num; // => 10 // function hoisting getPi; // => function getPi() {...} getPi(); // => 3.14 function getPi() { return 3.14; } |
变量 num
在它的声明语句 var num
之前就被访问了,所以它的值为 undefined
函数 function getPi() {...}
是定义在文件的末尾的。然而函数可以在它声明 getPi()
之前就被调用,因为它被提升到了作用域的顶部。
这就是典型的变量提升。
事实证明,在首次使用变量或函数后才声明变量或函数会很容易产生困惑。假设你正滚动查看一个大文件,然后发现了一个未声明的变量...你肯定会想它到底为什么在这里出现并且它是在哪定义的呢?
当然一个熟练的 JavaScript 开发者并不会这样编写代码。但在成千上万个 JavaScript Github 库中却可能存在着相当数量的这样的代码。
甚至在上面给出的代码示例中,我们也很难去明白代码中的声明流程。
我们应当自然地首先声明或是描述一个未知的术语。在这之后再对它进行使用。let
便是鼓励你遵循这种方法来设置变量。
当引擎使用变量时,它们的生命周期包含以下阶段:
声明阶段 这一阶段在作用域中注册了一个变量。
初始化阶段 这一阶段分配了内存并在作用域中让内存与变量建立了一个绑定。在这一步变量会被自动初始化为 undefined
。
赋值阶段 这一阶段为初始化变量分配具体的一个值。
一个变量在通过声明阶段时它还是处于 未初始化的 状态,这时它仍然还没有到达初始化阶段。
注意,按照变量的生命周期过程,声明阶段与我们通常所说的变量声明是不同的术语。简单来讲,引擎处理变量声明需要经过完整的这 3 个阶段:声明阶段,初始化阶段和赋值阶段。
var
变量的生命周期稍微熟悉下这些生命周期阶段,现在让我们用它们来描述引擎是如何处理 var
变量的。
假设一个场景,当 JavaScript 遇到了一个函数作用域,其中包含了 var variable
的语句。则在任何语句执行之前,这个变量在作用域的开头就通过了声明阶段并马上来到了初始化阶段(步骤一)。
同时 var variable
在函数作用域中的位置并不会影响它的声明和初始化阶段的进行。
在声明和初始化阶段之后,赋值阶段之前,变量的值便是 undefined
并已经可以被使用了。
在赋值阶段 variable = 'value'
语句使变量接受了它的初始化值(步骤二)。
这里的变量提升严格的说是指变量在函数作用域的开始位置就完成了声明和初始化阶段。在这里这两个阶段之间并没有任何的间隙。
让我们参考一个示例来研究。下面的代码创建了一个包含 var
语句的函数作用域:
1 2 3 4 5 6 7 8 | function multiplyByTen(number) { console.log(ten); // => undefined var ten; ten = 10; console.log(ten); // => 10 return number * ten; } multiplyByTen(4); // => 40 |
当 JavaScript 开始执行 multipleByTen(4)
时进入了函数作用域中,变量 ten
在第一个语句之前就经过了声明和初始化阶段,所以当调用 console.log(ten)
时打印为 undefined
。
当语句 ten = 10
为变量赋值了初始化值。在赋值后,语句 console.log(ten)
打印了正确的 10
值。
对于一个 函数声明语句 function funName() {...}
那就更简单了。
声明、初始化和赋值阶段在封闭的函数作用域的开头便立刻进行(只有一步)。 funName()
可以在作用域中的任意位置被调用,这与其声明语句所在的位置无关(它甚至可以被放在程序的最底部)。
下面的代码是一个函数提升的演示:
1 2 3 4 5 6 7 | function sumArray(array) { return array.reduce(sum); function sum(a, b) { return a + b; } } sumArray([5, 10, 8]); // => 23 |
当 JavaScript 执行 sumArray([5, 10, 8])
时,它便进入了 sumArray
的函数作用域。在作用域内,任何语句执行之前的瞬间,sum
就经过了所有的三个阶段:声明,初始化和赋值阶段。
这样 array.reduce(sum)
即使在它的声明语句 function sum(a, b) {...}
之前也可以使用 sum
。
let
变量的生命周期let
变量的处理方式不同于 var
。它的主要区分点在于声明和初始化阶段是分开的。
现在让我们研究这样一个场景,当解释器进入了一个包含 let variable
语句的块级作用域中。这个变量立即通过了声明阶段,并在作用域内注册了它的名称(步骤一)。
然后解释器继续逐行解析块语句。
这时如果你在这个阶段尝试访问 variable
,JavaScript 将会抛出 ReferenceError: variable is not defined
。因为这个变量的状态依然是未初始化的。
此时 variable
处于临时死区中。
当解释器到达语句 let variable
时,此时变量通过了初始化阶段(步骤二)。现在变量状态是初始化的并且访问它的值是 undefined
。
同时变量在此时也离开了临时死区。
之后当到达赋值语句 variable = 'value'
时,变量通过了赋值阶段(步骤三)。
如果 JavaScript 遇到这样的语句 let variable = 'value'
,那么变量会在这一条语句中同时经过初始化和赋值阶段。
让我们继续看一个示例。这里 let
变量 number
被创建在了一个块级作用域中:
1 2 3 4 5 6 7 8 | let condition = true; if (condition) { // console.log(number); // => Throws ReferenceError let number; console.log(number); // => undefined number = 5; console.log(number); // => 5 } |
当 JavaScript 进入 if (condition) {...}
块级作用域中,number
立即通过了声明阶段。
因为 number
尚未初始化并且处于临时死区,此时试图访问该变量会抛出 ReferenceError: number is not defined
.
之后语句 let number
使其得以初始化。现在变量可以被访问,但它的值是 undefined
。
之后赋值语句 number = 5
当然也使变量经过了赋值阶段。
const
和 class
类型与 let
有着相同的生命周期,除了它们的赋值语句只会发生一次。
let
的生命周期中无效如上所述,变量提升是变量的耦合声明并且在作用域的顶部完成初始化。
然而 let
生命周期中将声明和初始化阶段解耦。这一解耦使 let
的变量提升现象消失。
由于两个阶段之间的间隙创建了临时死区,在此时变量无法被访问。
这就像科幻的风格一样,在 let
生命周期中由于变量提升失效所以产生了临时死区。
使用 var
自由的去声明变量很容易出现错误。
基于这一点,ES2015 引进了 let
。它使用了一种改进的算法来声明变量并添加了块作用域。
因为声明和初始化阶段是解耦的,变量提升对于 let
变量(也包括 const
和 class
)是无效的。在初始化之前,变量处于临时死区中并不可被访问。
为了保证平稳的变量声明,推荐这些技巧以供参考:
声明,初始化变量后再使用变量。这个流程才是正确并易于遵循的。
尽可能的减少变量数。你暴露的变量越少,你的代码则会变得更加模块化。
这就是今天所有的内容。我们在下一篇文章再见。
]]>在上周日 2016.7.10 我的英语打卡到达了第 500 天。推算 500 天前大概是 2015 年 3 月之前。当时还是寒假,我决定在毕业前的最后一次六级考试中好好刷个分..(之前基本是裸考,顺便这最后一次一起陪我们宿舍某个一直没有过六级的哥们考完他最后一次的六级考试 Ծ ̮ Ծ)。在看了一些网上推荐的英语学习方法后,最后选择了觉得比较适合我的扇贝英语系列(扇贝全系列 APP)。
这里简单介绍并安利一下选择扇贝的主要原因:
然后说下我的英语成绩,说实话这是我学业生涯以来最惭愧的一科。小时候父母工资拮据时都还给我报了一个月 80 块钱的英语学习班(那时工资好像才几百元)。因此小时候还能混进英奥班学习,然而后来淘气加偏科,因此英语成绩相对的也就慢慢掉下来了。记得高考前几乎每一次英语考试中选词填空好多都是蒙的。最后高考时英语也才 100 多分。。现在想起来确实觉得实在是对不住小时候的高价英语补习班。。
后来在大学中慢慢认识到了英语的重要性,但确实大学期间也还是没有很大程度的投入精力去学习英语。所以当时也是借此为契机,准备在毕业后养成学习英语的习惯(也主要是每天学习的习惯)。而且身为一名程序猿,英语能力确实一定程度上也是自己吃饭的技能之一啊..(想起之前知乎上看到的一个梗 —— 问:2016 年,中国的前端在关注什么?答:关注国外的前端..)。因此我的梦想也是能有朝一日能够可以做到流利的与国外朋友或同行进行英语交流。出国不存在任何语言障碍的水平,这样我也就心满意足了。(马云那样一半的英语交流能力也就够吧..)
首先需要说明,在开始打卡前我的英语水平差不多就是六级低分飘过的水平..(再高水平的英语考试目前还没考过..)然而在扇贝训练后的那次最后一次六级考试却是华丽丽的刷分失败..(没过线的水平..)当时还跟朋友说,我天天背单词,背了 100 多天也居然还是没过线(╯‵□′)╯︵┻━┻..但说实话在毕业后又坚持下来的这 300 多天来看,我觉得我的英语还是有进步的。(这里真的不虚!)
目前我每天的英语学习计划是 50 单词 + 10 炼句 + 2 阅读 + 5 听力 (最近扇贝系列也刚出了口语系列,但目前我还没有把它加入每日计划的打算)。每天花费的时间平均大概在 30--45 分钟之间,基本上是可以接受的,也并不耽误正常工作休息时间。每天睡觉前看完 2 篇阅读,上班的路上背完单词和炼句,午休时间做完听力。基本上就是我每天正常的扇贝学习时间。
到 500 天打卡日为止累计学习单词数 10615 个,掌握单词数 10017 个,看完了 4 本英文原著。其他具体的学习内容感觉用扇贝的徽章图也比较好描述。。
其实打卡这件事跟坚持做其他事的差别并不大(健身、读书、听 VOA 等等),只要能真正的坚持下来肯定是不错的,我觉得这 500 天下来我无论是从学习的劲头上还是习惯上来说确实让我更多的产生了一些自信。相信自己之后在遇到更麻烦的挑战时也会敢于去尝试解决并相信自己能够做成。
谨以此日志记录这 500 天的扇贝打卡,并祝愿自己英语能力的梦想能早日实现。
更新:前两天看到一篇讲程序员英语学习的文章,感觉写的很不错,在此进行分享。
]]>原文链接 : Dark Side of UI. Benefits of Dark Background
原文作者 : Tubik Studio
译文出自 : 掘金翻译计划
译者 : yangzj1992
校对者: David Lin, Ruixi
首发于: 掘金
在用户界面的背景中是否选择使用暗色调依然是一个具有高度争议的问题。毋庸置疑,这个问题是很实际的:选择一个合适的背景在所有的产品功效上都起着至关重要的作用,因为它可能会是改善或是反而毁掉设计方案中布局和功能的关键因素。在今天,我们的文章将致力于讨论在 UI 设计中使用暗色背景的好处和缺陷,所以让我们前往 UI 的黑暗面吧。
在我们之前的文章中我们已经分析了一些可以影响选择通常的配色方案和基本的背景颜色的因素,也提到了一些在这个过程中要考虑的重点。这一次我们将更多的关注暗色设计的网站和移动应用的优缺点。我们在 Tubik Studio 中创建并测试了不同的用户界面,这些实际的工作经验证实了暗色背景会是强大而有吸引力的、能提供积极用户体验的解决方案。所以,理所当然的,让我们来开始讨论应该在何时何地怎样让它最大程度的发挥效果吧。
很久之前,在 2009 年曾有过一个公开的投票调查结果,ProBlogger 基于此已经公布了一些有趣的观点。读者被问及他们更喜欢哪种颜色的博客背景。几乎一半的读者回答更喜欢亮色背景 - 这对于传统的文本驱动型博客来说是十分合理的,在可读性方面如此可以胜过其他方案。然而,有 10% 的受访者回答他们更喜欢暗色背景,并且有超过三分之一的人提到选择的依据应该取决于博客的性质和内容。设计师在寻找设计方案时是不能忽略占有如此大比例的用户的。此外,在具有更少的文本型驱动内容的数字产品情况时,如网站或应用程序中,持上述观点的人数比例应该还会增加。这个例子很好的说明了用户研究和调查应该是设计过程中的重要组成部分。了解用户想要什么或是至少了解他们所能够接受的是什么,这能够将传统视觉的限度推向一个极致。
Richard H. Hall 和 Patrick Hanna 对于这个问题提供的科学研究中强调了视觉感知的背景颜色和效果的关键点。在分析了不同研究者之前对网络页面效果和可读性方面的实际试验后。作者们总结了:「他们发现正向对比(即白底黑字)会有更好的效果,并与之前提到的研究结合,说明颜色组合之间的对比度越大效果越好。」因此在合适的设计和测试下,在其他方面深色背景也可以像浅色背景一样具有好的效果,尤其是在对比性以及布局元素的易读性上。在用户测试角度上这项研究基于不同颜色组合和效果下包含了很多有趣及有用的信息。所以在此强烈推荐给设计师们。
用户体验设计的著名大师之一 Jacob Nielsen 曾提到过:「在使用高对比度颜色的文本和背景时。最优的易读性方案是要求使用黑色文字和白色背景(所谓的正向文本)。而白色文字和黑色背景(反向文本)几乎也是一样好的。尽管这在对比度上与正向文本是一样的,但倒配色方案会让人们略微有些迷惑并会稍微降低他们的阅读速度。易读性相当受配色方案的影响,像文本比纯黑稍亮的颜色,尤其是背景色比纯白稍暗的配色,易读性会变得很差。」
的确,可读性是产品效果表现上的重要指标并且它不仅只针对文本。它超越了文本的限制并且意味着所有有意义的象征,包括字母,数字,象形符号和图片都应该被留意到并轻易的在界面中识别出。因此,设计师在选择深色的背景时应该准备更额外深入的选择并测试不同设备上的字体、图标和图像。
最好的网页和应用程序的设计实践,例如 Awwwards 上最好的黑色网站合集上的例子,这里展现了大量使用深色背景作为基础配色的优秀设计方案,这些方案都没有以牺牲可读性为代价。为了避免低可读性这个问题,在设计过程中重要的是要记住:
webdesign.about.com 用表格展现了一个有趣的视觉感知方面需要考虑的展现效果。该表展示了不同的颜色组合之间的对比和效果水平并提供了一个有趣的事实:表中的黑色部分是唯一一个可以为几乎所有颜色提供良好对比度效果的颜色。因此在设计界面的每一个特定情况下去仔细试验,这一因素可以作为尝试使用深色背景的理由之一。
在可读性方面,对比度能是使内容更容易识别和清晰的因素之一。
关于对比度和可读性的这种提示信息在之前的一个早期的调查中有说过:「在深色背景下,确保你没有包含相当明亮的字体:使用柔和的白色到浅灰色字体,或使用单调的色彩来最小程度的减少巨大的反差和眩光;这个原则在做幻灯片时也同样适用:用至少 5% 的灰度来减少眩光的亮白。有趣的是,这样仍然在「阅览」时会被认为是白色的。同样的,将字体加粗,可以让字体有足够的大小让人不觉得文字被深色背景所「吞噬」」这个试验以及其他试验能够提供不同类型的调色法,而这些调色法能够为网页和应用页面提供高效、自然的内容。
还有一件事就是深色背景在某种程度上通常显得更沉重以及能更深入的呈现图形的内容如图片,相片,插图,海报和广告。良好的构图并遵守视觉层级原则可以显著的增强这种布局元素的视觉感知。这个因素在当界面基于更多图形材料而非文本时会使深色背景更高效并且具有吸引力。
色彩心理学也是在选择背景颜色时需要考虑到的,这不仅只是包含在所展现的有效范围中,还包含内容自身所承担的信息载体。黑暗的颜色通常与优雅和神秘感有关。此外,黑色往往与优雅,礼节,声望和权利有关。这或许是为什么许多强大的品牌都会使用黑白色调的主题,用深色来主导、用亮色来展示承载的信息,使用这样的方案来构建视觉展现效果。界面设计在这方面可以为其他设计解决方案和一般的产品展示提供额外的支持。
根据上述各点,我们可以总结得到在用户界面应用深色背景可以提供以下实际好处,包括:
在另一方面,深色背景需要彻底的关注和分析最微小的细节,如果它们没有以合适的方式呈现,那么这些细节则可能会在布局中变得模糊。因此我们应该考虑:
原文链接 : JSON Web Tokens (JWT) vs Sessions
原文作者 : Jacek Ciolek
译文出自 : 众成翻译
译者 : yangzj1992
校对者: lisa
首发于: 众成翻译
本质上它是一段签名的 JSON 格式的数据。由于它是带有签名的,因此接收者便可以验证它的真实性。同时由于它是 JSON 格式的因此它的体积也很小。如果你想了解有关它的正式定义,可以在 RFC 7519 中找到。
这篇文章发布于黑客新闻上。在这里也可以看一下关于这篇文章的案例分析,它主要包含了文章内容的公开分析、SEO 影响、性能影响以及更多其他的内容。
数据签名已经不是什么新事物了 - 令人值得兴奋的是如何在不依靠 sessions 的情况下使用 JWT 创建真正的 RESTful 服务,目前这个想法已经被事实证明有一段时间了。下面是介绍它在现实中具体实现的工作原理 - 首先在这里我来做一个类比:
想象一下你刚从国外度完假回来,你在边境上说 - 你可以让我通过,我是这里的公民。这样的回答很好也没有问题,但是你要如何去支持你的说法呢?最有可能的方案是你携带了护照来证明你的身份。这里我们假设边境工作人员也都被要求去核实护照是真正由你的国家的护照办签发的。那么护照就会被核实,这样他们也才会放你回国。
现在,让我们从 JWT 的角度看一下这个故事,它们各自又都扮演着什么样的角色:
护照办 - 发布 JWT 的身份验证服务。
护照 - 你通过"护照办"获得的 JWT 签名。你的身份对于任何人都是可读的,但是只有它是真实的时候相关方才会对其核实。
公民资格 - 在 JWT 中包含的你的声明(你的护照)。
边境 - 你的应用程序的安全层,在被允许访问受保护的资源之前由它来核实你的 JWT 令牌身份,在这种情况下指的是 - 国家。
国家 - 你想要获取的资源(例如 API)。
简单来说,JWT 非常的酷,因为你不用再为了鉴别用户而在你的服务器上去保留你的 session 数据。这个工作流将会变得像下面这样:
用户调用身份验证服务,通常是发送了用户名及密码。
身份验证服务响应并返回了签名的 JWT,上面包含了用户是谁的内容。
用户向安全服务发送请求收到安全服务返回的令牌。
安全层检验令牌上的签名并且在签名为真实的时候授权予以通过。
让我们考虑一下这样做的结果。
没有 sessions 意味着你没有会话存储。但除非您的应用程序需要横向扩展,否则这也不太重要,如果你的应用程序是运行在多个服务器上的,那么共享 session 数据将会成为一个负担。你需要一个专门的服务器来只存储会话数据或是共享磁盘空间或是在负载均衡上粘滞会话。当你不使用 sessions 时上面的这些也就自然不再需要了。
通常来讲 sessions 需要留意过期和垃圾收集的情况。JWT 可以在用户数据中包含自己的过期日期。因此安全层在检验 JWT 的授权时可以同时核对它的过期时间来拒绝访问。
只有在无 sessions 的情况下你可以创建真正的 RESTful 服务,因为它被认为是无状态的。 JWT 很小所以它可以在每一个请求中被一起发出去,就像一个 session cookie一样。然而与 session cookie 不同的是,它并不指向服务器上的任何存储数据, JWT 本身包含了这些数据。
在我们更深入讨论之前,有一件事需要了解。JWT 自身并不是一个东西。它是 JSON 网络签名(JWS)或 JSON 网络加密 (JWE)中的一种类型。它的定义如下:
一个 JWT 的声明内容会被编码为一个 JSON 对象,它被作为 JSON 网络签名结构的有效载荷或是作为 JSON 网络加密结构的明文信息。
前者给我们的只是一个签名并且它包含的数据(或是平时所称呼的 claims
的命名)是对任何人都可读的。后者则提供了加密的内容,所以只有拥有密钥的人可以解密它。JWS 在实现上更加容易并且基本用法上是不需要加密的 - 毕竟如果你在客户端上有密钥的话,你还不如把所有的东西不加密的好。因此 JWS 在大多数情况下都是适用的,也因此在之后我将主要关注 JWS。
头部 - 关于签名算法的信息,以 JSON 格式的负载类型(JWT)等等。
负载 - JSON 格式的实际的数据(或是声明)。
签名 - 额... 就是签名。
我将在之后具体解释这些细节。现在让我们先来分析下基础要素。
上述所提到的每一部分(头部,负载和签名)是基于 base64url 编码的,然后他们用 .
作为分隔符粘连起来组成 JWT。 下面是这个实现方式可能看上去的样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | var header = { // The signing algorithm. "alg": "HS256", // The type (typ) property says it's "JWT", // because with JWS you can sign any type of data. "typ": "JWT" }, // Base64 representation of the header object. headerB64 = btoa(JSON.stringify(header)), // The payload here is our JWT claims. payload = { "name": "John Doe", "admin": true }, // Base64 representation of the payload object. payloadB64 = btoa(JSON.stringify(payload)), // The signature is calculated on the base64 representation // of the header and the payload. signature = signatureCreatingFunction(headerB64 + '.' + payloadB64), // Base64 representation of the signature. signatureB64 = btoa(signature), // Finally, the whole JWS - all base64 parts glued together with a '.' jwt = headerB64 + '.' + payloadB64 + '.' + signatureB64; |
由此得到的 JWS 结果看上去整洁而优雅,有点像这样:
1
| `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.OLvs36KmqB9cmsUrMpUutfhV52_iSz4bQMYJjkI_TLQ`
|
你也可以试着在 jwt.io 这个网站上来创建令牌试试。
有一点相当重要,那就是签名是依据头部和负载计算出来的。因此头部和负载的授权也很容易同样被检验:
1 2 3 4 5 6 7 | [headerB64, payloadB64, signatureB64] = jwt.split('.'); if (atob(signatureB64) === signatureCreatingFunction(headerB64 + '.' + payloadB64) { // good } else // no good }) |
事实上,JWT 头部被称为 JOSE 头部。JOSE 表示的是 JSON 对象的签名和加密。也正如你期望的那样,JWS 和 JWE 都是这样的一个头部,然而它们各自之间存在着一套稍微不同的注册参数。下面是在 JWS 中使用的头部注册参数列表。所有的参数除了第一个参数(alg)以外,其他参数都是可选的:
alg 算法 (必选项)
typ 类型 (如果是 JWT 那么就带有一个值 JWT
,如果存在的话)
kid 密钥 ID
cty 内容类型
jku JWK 指定 URL
jwk JSON 网络值
x5u X.509 URL
x5c X.509 证书链
x5t X.509 证书 SHA-1 指纹
x5t#S256 X.509 证书 SHA-256 指纹
crit 临界值
前两个参数是最常用的,所以典型的头部看起来有点类似下面这样:
1 2 3 4 | { "alg": "HS256", "typ": "JWT" } |
上面列出的第三个参数 kid
是基于安全原因使用的。cty
参数在另一方面应该只被用于处理嵌套的 JWT。剩下的参数你可以在规范文档中阅读了解,我认为它们不适合在这篇文章中被提及。
alg
(算法)alg
参数的值可以是 JSON 网络算法(JWA)中的任意指定值 - 这是我所知道的另一个规范。下面是 JWS 的注册列表:
HS256 - HMAC 使用 SHA-256 算法
HS384 - HMAC 使用 SHA-384 算法
HS512 - HMAC 使用 SHA-512 算法
RS256 - RSASSA-PKCS1-v1_5 使用 SHA-256 算法
RS384 - RSASSA-PKCS1-v1_5 使用 SHA-384 算法
RS512 - RSASSA-PKCS1-v1_5 使用 SHA-512 算法
ES256 - ECDSA 使用 P-256 和 SHA-256 算法
ES384 - ECDSA 使用 P-384 和 SHA-384 算法
ES512 - ECDSA 使用 P-521 和 SHA-512 算法
PS256 - RSASSA-PSS 使用 SHA-256 和基于 SHA-256 算法的 MGF1
PS384 - RSASSA-PSS 使用 SHA-384 和基于 SHA-384 算法的 MGF1
PS512 - RSASSA-PSS 使用 SHA-512 和基于 SHA-512 算法的 MGF1
none - 没有数字签名或 MAC 执行
请注意最后一个值 none
,从安全性的角度来看这是最有趣的。这是已知的被用来进行降级防御攻击的方法。它是如何工作的呢?想象一个客户端生成的带有一些声明的 JWT 。它在头部指定 none
值的签名算法并进行发送验证。如果攻击者比较单纯,那么它会使 alg
参数为真来确保被授权通过,然而实际上则是不会被允许的。
底线是,你的应用的安全层应该总是对头部的 alg
参数进行校验。那里就是 kid
参数用的上的地方。
typ
(类型)这一个参数非常简单。如果它是已知的,那么它就是 JWT,因为应用不会去索取其他的值,如果这个参数没有值就会被忽视掉。因此它是可选的。如果需要被指定值,它应该按大写字母拼写 - JWT
。
在某些情况下,当应用程序接受到没有 JWT 类型的请求却又包含了 JWT 时,去重新指定它是很重要的,因为这样应用程序才不会崩溃。
kid
(密钥 id)如果你的应用程序中的安全层只使用了一个算法来签名 JWTs,你不用太担心 alg
参数,因为你会总是使用相同的密钥和算法来校验令牌的完整性。但是,如果你的应用程序使用了一堆不同的算法和密钥,你就需要能够分辨出是由谁签署的令牌。
正如我们之前看到的,单独依靠 alg
参数可能会导致一些...不便。然而,如果你的应用维护了一个密钥/算法的列表,并且每一对都有一个名称(id),你可以添加这个密钥 id 到头部,这样在之后验证 JWT 时你会有更多的信心去选择算法。这就是头部参数 kid
- 你的应用中用来签名令牌所使用的密钥 id 。这个 id 是由你来任意指定的。最重要的是 - 这是你给的 id ,所以你可以验证。
cty
(内容类型)这里把规范介绍的很清楚,所以这里我就只是引用了:
在通常情况下,在不使用嵌套签名或是加密操作时,是不推荐使用这个头部参数的。而在使用嵌套签名或加密时,这个头部参数必须存在;在这种情况下,它的值必须是 "JWT",来表明这是一个在 JWT 中嵌套的 JWT。虽然媒体类型名字对大小写并不敏感,但这里为了与现有遗留实现兼容还是推荐始终用
JWT
大写字母来拼写。
claims
这个名称是否让你感到困惑?在最初它也确实让我很困惑。我相信你需要重复读几次来尝试适应它。简而言之,claims
是 JWT 的主要内容 - 是我们十分关心的签名的数据。它被叫做 claims
是因为通常它就是声明这个意思 - 客户端声明了用户名,用户角色或者其他什么的来让它可以获得对资源的访问。
还记得我在最开始提到的那个可爱的故事吗?你的公民资格就是你的声明而你的护照则就是 - JWT
你可以在声明中放置任何你想要的参数,这儿有一个注册表应当被视为公认的参考实现方法。请注意这里的每一个参数都是可选的并且大多数是应用程序特定的,下面就是这个列表:
exp - 过期时间
nbf - 有效起始日期
iat - 发行时间
sub - 主题
iss - 发行者
aud - 受众
jti - JWT ID
值得注意的是,除了最后三个(issuer ,audience 和 JWT ID)参数通常是在更复杂的情况下(例如包含多个发行者时)才被使用。下面让我们来讨论一下它们吧。
exp
(过期时间)exp
是时间戳值表示着在什么时候令牌会失效。规范上要求"当前日期/时间"必须在指定的 exp
值之前,从而保证令牌可以得到处理。这里也表明了存在一些余地(几分钟)来应对时间差。
nbf
(有效起始时间)nbf
是时间戳值表示着在什么时候令牌开始生效。规范上要求"当前日期/时间"必须与指定的 nbf
值相等或在其之后,从而保证令牌可以得到处理。这里也表明了存在一些余地(几分钟)来应对时间差。
iat
(发行时间)iat
是时间戳值表示什么时候令牌被发行。
sub
(主题)sub
在规范上被要求"是JWT 中的声明中通常用于陈述主题的值"。这里主题必须是内容中唯一的发行者或全局上的唯一值。sub
声明可以用来鉴别用户,例如 JIRA 文档上那样。
iss
(发行者)iss
是被用来确认令牌的发行者的字符串值。如果值中包含 :
那么它就是一个 URI。如果有很多的发行者而在一个安全层中应用程序需要去识别发行人时,它将会是有用的。例如 Salesforce 要求了去使用 OAuth client_id 来作为 iss
的值。
aud
(受众)aud
是被用来确认令牌的可能接受者的字符串值或数组。如果值中包含 :
那么它就是一个 URI。
通常使用 URI 资源的声明是有效的。例如,在 OAuth 中,接受者是授权服务器。应用程序处理令牌时,在针对不同的接受者的情况下,必须验证接受者是否是正确的或者拒绝令牌。
jti
(JWT id)令牌的唯一标识符。每个发布的令牌的 jti
必须是唯一的,即使有很多发行人也是一样。jti
声明可以用于一次性的不能重放的令牌。
在最常见的场景中,客户端的浏览器将在认证服务中认证并接受返回的 JWT。然后客户端用某种方式(如内存,localStorage)存储这个令牌并与受保护的资源一起发送返回。通常令牌发送时是作为 cookie 或是 HTTP 请求中 Authorization
头部。
1 2 3 | GET /api/secured-resource HTTP/1.1 Host: example.com Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.OLvs36KmqB9cmsUrMpUutfhV52_iSz4bQMYJjkI_TLQ |
首选头部方法是出于安全的原因 - cookies 会很容易受 CSRF(跨站请求伪造)的影响,除非 CSRF 令牌是使用过的。
其次,cookies 只能发送返回到被发出的相同的域下(或者最多二级域下)。如果身份验证服务驻留在不同的域下,那么 cookies 得需要更强烈的创造性才行。
因为没有 session 数据存储在服务端了,所以不能再通过破坏 session 来注销了。因此登出成为了客户端的职责 - 一旦客户丢失了令牌不能再被授权,就可以被认为是登出了。
我认为 JWTs 是一个在脱离 sessions 的情况下非常聪明的授权方式。它允许创建真正的服务端无状态的基于 RESTful 的服务,这也意味着不需要 session 存储。
与浏览器自动发送 session cookie 到任意匹配域/路径组合(老实说,在大多数情况下这里只有域的情况)的 URL 不一样的是,JWTs 可以选择性的只向需要身份授权的资源来发送。
对于客户端和服务端来说,它的实现非常简单,特别是已经有专门的库来制造签名和验证令牌了。
感谢阅读!
如果你喜欢这篇文章的话,欢迎分享它。同样也十分欢迎你对它进行评论!
]]>原文链接 : How to write low garbage real-time Javascript
原文作者 : Ashley
译文出自 : 掘金翻译计划
译者 : yangzj1992
校对者: L9m, Dwight, 宁金
首发于: 掘金
编辑于 2012 年 3 月 27 日: 哇,这篇文章已经写了有很长一段时间了,十分感谢那些精彩的回复!其中有一些对于一些技术的指正,如使用 'delete' 。我知道了使用它可能会导致其他的降速问题,因此,我们在引擎中极少使用它。一如既往的你还需要对所有的事进行权衡并且需要通过其他关注点来平衡垃圾回收机制,这也只是一个在我们引擎中发现的的实用、简单的技术列表,它并不是一个完整的参考列表。但是我希望它还是有用的!
一个用 Javascript 编写的 HTML5 游戏,要达到流畅体验的一个最大阻碍就是垃圾回收 ( GC ) 卡顿。 Javascript 并没有一个显式的内存管理,意味着你创造东西后却不能释放它们占用的内存。因此迟早浏览器便会替你决定去清理它们:这时代码执行就会被暂停,浏览器会找出哪一部分内存是现在仍在被使用的,并把其他所有东西占用的内存释放掉。这篇博文将会去探究避开 GC 开销的技术细节,这对方便进行使用任何插件或是使用 Construct 2 进行 Javascript SDK开发都应该能派上用场。
浏览器有很多技术性手段来减少 GC 卡顿,但是如果你的代码创造了许多垃圾,迟早浏览器也将会暂停并进行清理。随着对象逐步创建的过程中,之后浏览器又突然清理,这最后将导致内存使用情况图表呈现 z 字形。例如,下面是 Chrome 在玩太空爆破手时的内存使用情况。
当在玩一个 Javascript 游戏时会呈现 z 字形的内存占用情况。这可能是一个内存泄漏错误,但是实际上是 JavaScript 的正常操作。
此外,游戏以 60 fps 运行时只有 16 ms 的时间来渲染每一帧,但是 GC 会很轻易的产生最少 100 ms 以上明显的卡顿,在更糟的情况下,这会导致不断卡顿的游戏体验,因此对于像游戏引擎一样实时运行的 Javascript 代码,解决办法是努力尝试在典型帧的持续时间内你不要创建任何东西。这实际上是相当困难的,因为有许多看上去无害的 Javascript 语句实际上却创造了垃圾,它们都必须从每帧动画的代码路径里删除掉。在 Construct 2 中我们竭尽全力减少每一处引擎的垃圾开销,但是你可以从图表中看到上面仍然有许多小的对象被创建所以 Chrome 还会每隔数秒进行一次清除。要注意这里只是一个小的清理 - 这里并没有大量的内存被清理出来,因为一个更高更极端的z曲线会更引起关注,但是它可能已经足够好了,因为小型的垃圾集合执行会更快并且偶尔的小卡顿也一般不太引人注意 - 因此我们应该看到了,有时我们确实很难避免产生新的资源分配。
同样重要的包括第三方插件以及开发人员行为也需要遵守这些原则,否则,一个写的不好的插件可以产生许多垃圾并会让游戏十分卡顿,尽管主引擎 Construct 2 已经是一个非常低垃圾开销的引擎了。
首先,最明显的是,关键词 new
指示了资源的分配,例如 new Foo()
在可能的情况下,它会在启动时尝试创建一个对象,并且尽可能长时间、简单的重新使用相同的对象。
不太明显的是,这里有三种快捷语法方式来相似的调用 new
:
{}
(创建一个新对象)
[]
(创建一个新数组)
function () { ... }
(创建一个新函数,也会被垃圾收集)
对于对象,用避免 {}
一样的方式来避免 new
- 尝试去回收对象。请注意这包括像 { "foo": "bar" }
这样带属性的对象,也就是我们在函数中常用的一次性返回多个值。或许将每一次的返回值写入一个相同的(全局)对象来返回的写法是更好的 - 在文档中要仔细记录这一点,因为如果你保持引用这样的返回对象,可能在每次调用改变的时候发生错误。
实际上你可以回收一个存在的对象(如果它没有原型链)通过删除它的所有属性,将它还原为一个空的对象如 {}
一样。为此你可以使用 cr.wipe(obj)
函数,它的定义如下:
1 2 3 4 5 6 7 8 9 10 | // remove all own properties on obj, effectively reverting it to a new object cr.wipe = function (obj) { for (var p in obj) { if (obj.hasOwnProperty(p)) delete obj[p]; } }; |
因此在某些情况下,你可以调用 cr.wipe(obj)
并为其再次添加属性来重用一个对象。比起重新简单分配 {}
现场清除一个对象可能需要更长的时间,但是在实时处理的代码中更重要的是避免产生垃圾,从而减少未来可能产生的卡顿情况。
分配 []
到一个数组中被经常用来作为一个快捷方式去清除这个数组(例如 arr = [];
),但请注意这将创建一个新的空数组并使旧的数组成为一个垃圾!更好的写法是 arr.length = 0;
,这种方式具有相同的效果但却继续使用了相同的数组对象。
函数则有一点棘手,函数通常在执行时创建并且不倾向于在运行时进行过多分配 - 但这意味着它们在动态创建时很容易被忽视。一个例子是返回函数的函数。主要的游戏循环使用了 setTimeout
或者 requestAnimationFrame
方法来调用一个成员函数类似如下:
1 2 3 4 5 6 7 | setTimeout( (function (self) { return function () { self.tick(); }; })(this) , 16); |
这看起来像是一个合理的方式来每 16ms 调用一次 this.tick()
。然而,这也意味着每一次执行 tick 函数都会返回一个新函数!这可以通过永久存储函数的方法来避免,例如:
1 2 3 4 5 6 | // at startup this.tickFunc = (function (self) { return function () { self.tick(); }; })(this); // in the tick() function setTimeout(this.tickFunc, 16); |
这将在每次执行 tick 函数时重复使用相同的函数来代替产生一个新的函数。这个方法可以应用到任意其他地方的返回函数中或是运行创建的函数中。
随着我们的进展,进一步的避免产生垃圾变得更加困难,由于 Javascript 本身就是围绕着 GC 所设计的。许多 Javascript 中方便的库函数也总是创建了新的对象。这儿没有什么你可以做的但是当你返回文档查阅那些返回值时。例如,数组中的 slice()
方法会返回一个数组(基于保持不变的原始数组范围内),字符串的 substr
会返回一个新的字符串(基于保持不变的原始字符串字符的范围),等等。调用这些函数都会产生垃圾,而你能做的就是不要去调用它们,或是在极端情况下重写你的函数使它们不再产生垃圾。例如在 Construct 2 这种引擎,由于各种原因一个经常的操作是通过索引去删除数组里的一个元素。这个方法的快捷使用方式如下:
1 2 3 | var sliced = arr.slice(index + 1); arr.length = index; arr.push.apply(arr, sliced); |
然而 slice()
返回一个原始数组的后半部分来组成了一个新的数组,并且在被 (arr.push.apply
)复制后产生了垃圾。由于这是我们引擎中一个生产垃圾的热门处,它被改写为了一个迭代版本:
1 2 3 4 | for (var i = index, len = arr.length - 1; i < len; i++) arr[i] = arr[i + 1]; arr.length = len; |
显然重写大量的库函数是相当痛苦的,所以你需要仔细的权衡需求实现的方便性以及垃圾产生之间的平衡。如果它在每帧中被调用了很多次,你可能最好重写这个你需要的函数库。
这里可以很容易的使用 {}
语法来沿着递归函数传递数据。通过一个数组来表示一个堆栈,在这个堆栈中对递归的每一级进行 push 和 pop 是更好的。更好的是,实际上你并不需要在数组中 pop - 你应该将数组中最后一个对象像垃圾一样处理掉。来代替使用一个 top index
变量进行简单减量。然后为了代替 pushing ,则增加 top index
并且如果有的话就重用数组中的下一个对象,否则执行真正的 push。
此外,在所有可能的情况下避免向量对象(如 vector2 中的 x 和 y 属性)。虽然可能函数返回这些对象会让它们立刻改变或返回这两个值时会方便些,你可以在每一帧中轻松地结束数百个这样的创建对象,这将导致可怕的 GC 性能。这些函数必须分离出来在每个单独的组件中工作,例如:使用 getX()
和 getY()
来代替 getPosition()
来返回一个 vector2 对象。
有时候你无法摆脱一个库是一个产生垃圾的噩梦。 Box2Dweb 是一个典型的例子:它每一帧产生了数百个 b2Vec2 对象并且不断的在浏览器产生垃圾,并最终导致垃圾处理器产生显著的卡顿效果。在这种情况下最好的办法是创建一个缓存回收机制。我们一直在测试 Box2D (Box2Dweb-closure) 的修正版本,它似乎可以使 GC 暂停进行缓解(虽然没有完全解决)。查阅 b2Vec2.js 的 Get
和 Free
代码。这里有一个名字叫 free cache
的数组,在之后的整个代码中如果不再使用 b2Vec2,它就会在 free cache 中被释放,当需要请求一个新的 b2Vec2,而它如果在 free cache 中还存在那么它就会被重用,否则才会分配一个新的。这并不完美,在一些测试后通常只有一半的 b2Vec2s 被创建并回收,但它确实帮助 GC 缓解了压力从而减少了频繁的卡顿。
在 Javascript 中很难去完全避免垃圾。它的垃圾收集模式根本上是不符合像游戏这样的实时软件的需求的。从 Javascript 代码中需要进行大量的工作来消除垃圾,因为有很多直接的代码含有创建大量垃圾的副作用。然而,只要仔细小心一些,Javascript 也是可以在实时项目中不产生或是制造很少的垃圾开销,而对于需要保持高度响应性的游戏和应用程序这也是至关重要的。
]]>正则表达式就是事先声明一组规则,用于匹配字符串中的字符。
修饰符 | 描述 |
---|---|
i | 执行对大小写不敏感的匹配。 |
g | 执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)。 |
m | 执行多行匹配。 |
在正则表达式的模式中,有一些字符是有特殊含义的,被称为元字符。元字符都是针对单个字符匹配的。
元字符 | 描述 |
---|---|
. | 查找单个字符,除了换行和行结束符。 |
\w | 匹配大小写英文字符及数字 0 到 9 之间的任意一个及下划线,相当于 [a-zA-Z0-9_] |
\W | 不匹配大小写英文字符及数字 0 到 9 之间的任意一个,相当于 [^a-zA-Z0-9_] |
\s | 匹配任何空白字符,相当于 [\f\n\r\t\v] |
\S | 匹配任何非空白字符,相当于 [^\s] |
\d | 匹配任何 0 到 9 之间的单个数字,相当于 [0-9] |
\D | 不匹配任何 0 到 9 之间的单个数字,相当于 [^0-9] |
\b | 匹配单词边界。 |
\B | 匹配非单词边界。 |
\0 | 查找 NUL 字符。 |
\n | 查找换行符。 |
\f | 查找换页符。 |
\r | 查找回车符。 |
\t | 查找制表符。 |
\v | 查找垂直制表符。 |
\xxx | 查找以八进制数 xxx 规定的字符。 |
\xdd | 查找以十六进制数 dd 规定的字符。 |
[\u4e00-\u9fa5] | 匹配任意单个汉字(这里用的是 Unicode 编码表示汉字的 ) |
^ | 匹配字符串的开头 |
$ | 匹配字符串的结尾 |
方括号用于查找某个范围内的字符
表达式 | 描述 |
---|---|
[...] | 匹配方括号中的所有字符 |
[^...] | 匹配非方括号中的所有字符 |
表达式 | 描述 |
---|---|
n* | 匹配任何包含零个或多个 n 的字符串。 |
n+ | 匹配任何包含至少一个 n 的字符串。 |
n? | 匹配任何包含零个或一个 n 的字符串。 |
n{X} | 匹配包含 X 个 n 的序列的字符串。 |
n{X,Y} | 匹配包含 X 或 Y 个 n 的序列的字符串。 |
n{X,} | 匹配包含至少 X 个 n 的序列的字符串。 |
n$ | 匹配任何结尾为 n 的字符串。 |
^n | 匹配任何开头为 n 的字符串。 |
?=n | 匹配任何其后紧接指定字符串 n 的字符串。 |
?!n | 匹配任何其后没有紧接指定字符串 n 的字符串。 |
表达式 | 描述 |
---|---|
(exp) | 匹配 exp,并捕获文本到自动命名的组里 |
(? |
匹配 exp,并捕获文本到名称为 name 的组里,也可以写成(?'name'exp) |
(?:exp) | 匹配 exp,不捕获匹配的文本,也不给此分组分配组号 |
(?=exp) | 正向先行断言——代表字符串中的一个位置,紧接该位置之后的字符序列能够匹配 exp。 |
(?!exp) | 负向先行断言——代表字符串中的一个位置,紧接该位置之后的字符序列不能匹配 exp |
(?<=exp) | 正向后行断言,代表字符串中的一个位置,紧接该位置之前的字符序列能够匹配 exp。 |
(?<!exp) | 负向后行断言,代表字符串中的一个位置,紧接该位置之前的字符序列不能匹配 exp。 |
(?#comment) | 这种类型的分组不对正则表达式的处理产生任何影响,用于提供注释让人阅读 |
数字:^[0-9]*$
n 位的数字:^\d{n}$
至少 n 位的数字:^\d{n,}$
m-n 位的数字:^\d{m,n}$
零和非零开头的数字:^(0|[1-9][0-9]*)$
非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?$
带 1-2 位小数的正数或负数:^(\-)?\d+(\.\d{1,2})?$
正数、负数、和小数:^(\-|\+)?\d+(\.\d+)?$
有两位小数的正实数:^[0-9]+(.[0-9]{2})?$
有 1~3 位小数的正实数:^[0-9]+(.[0-9]{1,3})?$
非零的正整数:^[1-9]\d*$
或 ^([1-9][0-9]*){1,3}$
或 ^\+?[1-9][0-9]*$
非零的负整数:^\-[1-9][]0-9"*$
或 ^-[1-9]\d*$
非负整数:^\d+$
或 ^[1-9]\d*|0$
非正整数:^-[1-9]\d*|0$
或 ^((-\d+)|(0+))$
非负浮点数:^\d+(\.\d+)?$
或 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$
非正浮点数:^((-\d+(\.\d+)?)|(0+(\.0+)?))$
或 ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$
正浮点数:^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$
或 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$
负浮点数:^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$
或 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$
浮点数:^(-?\d+)(\.\d+)?$
或 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$
汉字:^[\u4e00-\u9fa5]{0,}$
英文和数字:^[A-Za-z0-9]+$
或 ^[A-Za-z0-9]{4,40}$
长度为 3-20 的所有字符:^.{3,20}$
由 26 个英文字母组成的字符串:^[A-Za-z]+$
由 26 个大写英文字母组成的字符串:^[A-Z]+$
由 26 个小写英文字母组成的字符串:^[a-z]+$
由数字和 26 个英文字母组成的字符串:^[A-Za-z0-9]+$
由数字、26 个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}$
中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$
中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$
或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$
可以输入含有 ^%&',;=?$" 等字符:[^%&',;=?$\x22]+
禁止输入含有 ~ 的字符:[^~\x22]+
Email 地址:^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?
InternetURL:[a-zA-z]+://[^\s]* 或 ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$
手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$
电话号码("XXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"和"XXXXXXXX):^(\(\d{3,4}-)|\d{3.4}-)?\d{7,8}$
国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}
身份证号(15 位、18 位数字):
15 位: ^[1-9]\\d{7}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}$
18 位: ^[1-9]\\d{5}[1-9]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}([0-9]|X)$
短身份证号码(数字、字母 x 结尾):^([0-9]){7,18}(x|X)?$
或 ^\d{8,18}|[0-9x]{8,18}|[0-9X]{8,18}?$
帐号是否合法(字母开头,允许 5-16 字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
密码(以字母开头,长度在 6~18 之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$
强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在 8-10 之间):^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$
日期格式:^\d{4}-\d{1,2}-\d{1,2}
一年的 12 个月(01~09 和 1~12):^(0?[1-9]|1[0-2])$
一个月的 31 天(01~09 和 1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$
xml 文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+\\.[x|X][m|M][l|L]$
双字节字符:[^\x00-\xff]
(包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计 2,ASCII字符计 1))
空白行的正则表达式:\n\s*\r
(可以用来删除空白行)
HTML 标记的正则表达式:<(\S*?)[^>]*>.*?</\1>|<.*? />
(网上流传的版本太糟糕,上面这个也仅仅能部分,对于复杂的嵌套标记依旧无能为力)
首尾空白字符的正则表达式:^\s*|\s*$或(^\s*)|(\s*$)
(可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)
校验金额(2位小数): ^[0-9]+(.[0-9]{2})?$
腾讯 QQ 号:[1-9][0-9]{4,}
(腾讯 QQ 号从 10000 开始)
中国邮政编码:[1-9]\d{5}(?!\d)
(中国邮政编码为 6 位数字)
IP 地址:\d+\.\d+\.\d+\.\d+
(提取IP地址时有用)
IP 地址:((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))
IPV6 地址: (([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))
URL 链接: ((http|ftp|https)://)(([a-zA-Z0-9\._-]+\.[a-zA-Z]{2,6})|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,4})*(/[a-zA-Z0-9\&%_\./-~-]*)?
EMOJI 表情: ([\uE000-\uF8FF]|\uD83C[\uDF00-\uDFFF]|\uD83D[\uDC00-\uDDFF])
检查 IE 版本 ^.*MSIE [5-8](?:\\.[0-9]+)?(?!.*Trident\\/[5-9]\\.0).*$
这里的一篇图灵文章可以帮助你了解正则表达式的更深层原理:模式、自动机和正则表达式
]]>原文链接 : Build A Journaling App with Meteor 1.3 (Beta), React, React-Bootstrap, and Mantra
原文作者 : Ken Rogers
译文出自 : 掘金翻译计划
译者 : yangzj1992
校对者: Zhongyi Tong, 刘鑫
首发于: 掘金
由于目前 Meteor 1.3 正式版仍在开发中,在这份 Meteor 指南里我们采用了目前可以获取到的 Meteor 1.3 beta 版本进行开发。尽管 Meteor 1.3 版本很棒并有着许多精彩的改进,但部分人对于到底应该如何使用它来进行开发仍有一些困惑。 MDG(Meteor Development Group) 目前正在编写 Meteor 1.3 版指南,随着 1.3 正式版的发布,我们将会获得 Meteor 1.3 最佳开发实践的确切信息。
旁注:我写了一本关于使用 Meteor 1.3 ,React ,React-Bootstrap 遵循 Mantra 框架规范进行应用开发的书,点击这里可以了解更多并免费获取前三章的内容。
我写这份指南的目的是让开发者现在就能用上 Meteor 1.3。当你阅读本指南时,需要留意 1.3 版本目前仍处于 beta 阶段,因此内容可能发生任何变化。我会尽我所能的更新这份指南来适应最新版本。如果你发现了什么过期的内容,希望能指出来让我知道。
在这份指南中,我们将要构建一个简单的任务清单,开个玩笑,不会再是任务清单了。我们将用 Meteor 1.3 ,React 和 React-Bootstrap 构建一个基本的日志应用。
我们将采用 Arunoda 的 Mantra 规范。如果你对 Mantra 不够熟悉,你可以访问这里了解更多。 基本来说, Mantra 应用程序架构规范向我们提供了一个宜于维护的方式去构建 Meteor 应用。
在我们开始前,你需要安装好 Meteor 并需要对 Meteor 的原理及使用方法具备一定的理解。如果你并不熟悉,可以看看官方 Meteor 向导。
首先我们将通过一些资源来熟悉 Meteor 1.3 和 Mantra ,然后运用它们创建一个简单的日志应用。
首先我们要介绍 Meteor 1.3 并且了解它的主要改动包含什么。在 1.3 版本中它最大的改动是完全支持 ES2015 并提供了模块功能。
一开始你会发现这和我们以往开发 Meteor 应用很不一样,但一旦你习惯了你会发现体验是相当不错的,尤其是你要使用 Mantra 的架构的话。
这里有一篇关于 Meteor 1.3 的模块机制是怎样工作的精彩介绍:https://github.com/meteor/meteor/blob/release-1.3/packages/modules/README.md
使用模块可以让我们更容易的去写更多的代码,更加模块化。这样我们可以更好地组织我们的应用,由于 Meteor 1.3 也添加了对 npm 包的支持,我们不必再像过去那样只有 Meteor 包支持的情况下进行开发了。
接下来,你可以看看这三篇文章来了解如何在 Meteor 1.3 中配置 React ,并用它来处理数据。第二篇会向你介绍容器组件,这是使用 Mantra 开发的一个重要部分。
通常来说,我们需要做的第一件事就是通过 Meteor 1.3 来创建我们的 Meteor 项目,像下面这样。
1
| meteor create journal --release 1.3-modules-beta.8
|
但是稍等一下,构建一个 Mantra 应用需要非常多的项目设置 ,为了加快开发速度,我已经使用 Meteor 1.3,React,Mantra 创建创建了一个样板项目。我们就用它来代替初始方案直接开始。
如果你想知道这些具体做了什么,查看 Mantra 规范和 Mantra 博客应用实例。
现在我们安装完样板项目,它完全包含了遵循 Mantra 规范的 Meteor 项目中所有你需要的核心文件和目录。
你可以通过以下命令 clone 项目:
1
| git clone git@github.com:kenrogers/mantraplate.git
|
然后切换到刚创建的目录中运行
1
| npm install
|
这样会安装本应用依赖的所有的包。你可以查看示例项目来熟悉整个目录结构。
它包含完整的布局,路由系统以及具有注册,登录登出功能的用户系统。
在这份指南中,我们将要讨论这些内容是如何组合在一起的,以及如何使用户在应用中添加日志记录的功能。
在我们添加内容前我们来看看样例项目的目录结构,你可以发现,在客户端文件夹中我们将整个应用分成一个个模块,这些模块是你的应用的主要组成部分。
我们总是需要一个核心模块,如果你的 APP 比较简单,这个核心模块就是你所唯一需要的。在我们的 APP 中包含了核心模块和用户模块,这里还要加入一个条目模块来添加我们的日志记录。
这样的模块结构让我们可以轻松地组织我们的代码。
在用户模块中,看看 containers 和 components 文件夹中的 NewUser 文件,。container 文件夹如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import NewUser from '../components/NewUser.jsx'; import {useDeps, composeWithTracker, composeAll} from 'mantra-core'; export const composer = ({context, clearErrors}, onData) => { const {LocalState} = context(); const error = LocalState.get('CREATE_USER_ERROR'); onData(null, {error}); return clearErrors; }; export const depsMapper = (context, actions) => ({ create: actions.users.create, clearErrors: actions.users.clearErrors, context: () => context }); export default composeAll( composeWithTracker(composer), useDeps(depsMapper) )(NewUser); |
你可以看到我们在这里实际上并没有进行任何渲染,我们只是做一些设置和清理的工作,然后在 NewUser 组件中我们才实际上渲染了视图。
如果你运行应用并访问 /register 路由,打开 React 开发者工具,你可以看到 react-komposer 正在后台执行。它会创建一个容器组件负责处理底层子组件的数据或是 UI 组件。
当我们获取数据时容器组件的用途将会得到具体的展现,但是这里我们不这样处理。
对于这个日志程序我们准备使用 React-Bootstrap 。它可以很方便地使用 Bootstrap 来创建 React 应用。这种方式易于上手,并且保持了模块化,正如我们所愿。
让我们设置好并添加一个简单的表单。
首先让我们为项目添加 react-bootstrap
1
| npm install react-bootstrap
|
因为 React-Bootstrap 并不依赖任何特定的 Bootstrap 库,所以我们需要自行添加,现在让我们添加 Twitter 的官方 Meteor 包。
1
| meteor add twbs:bootstrap
|
首先我们用 React-Bootstrap 来修改 MainLayout.jsx 文件的内容如下:
1 2 3 4 5 6 7 8 9 10 11 | import React from 'react'; import {Grid, Row} from 'react-bootstrap'; const Layout = ({content = () => null }) => ( <grid> <row> <h1>Journal</h1> {content()} </row> </grid> ); export default Layout; |
在这里,我们从 react-boostrap 包中引入 Grid 和 Row 组件,并且像使用 div 一样为它们添加合适的 bootstrap 类。想要了解更多关于这个优秀的包的工作原理,可以在这里查看组件列表。
现在让我们修改 NewUser 和 Login UI 的组件让他们更友好地贴近 Bootstrap 。打开 NewUser.jsx 文件进行如下修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import React from 'react'; import { Col, Panel, Input, ButtonInput, Glyphicon } from 'react-bootstrap'; class NewUser extends React.Component { render() { const {error} = this.props; return ( <col xs="{12}" sm="{6}" smoffset="{3}"> <panel> <h1>Register</h1> {error ? <p style="{{color:" 'red'}}="">{error}</p> : null} <form> <input ref="”email”" type="”email”" placeholder="”Email”"> <input ref="”password”" type="”password”" placeholder="”Password”"> <buttoninput onclick="{this.createUser.bind(this)}" bsstyle="”primary”" type="”submit”" value="”Sign" up”=""> </buttoninput></form> </panel> ) } createUser(e) { e.preventDefault(); const {create} = this.props; const {email, password} = this.refs; create(email.getValue(), password.getValue()); email.getInputDOMNode().value = ''; password.getInputDOMNode().value = ''; } } export default NewUser; |
这个表单十分简单,它仅仅负责显示自身并调用 create 方法。这里我们简单介绍一下。
在我们的 actions 文件夹中,它们负责处理我们应用的逻辑,下面这一行
1
| create(email.getValue(), password.getValue());
|
将调用该方法并创建实际用户。 Mantra 重点强调了希望把一切分离成单独的文件。因此,我们将文件分为展示、逻辑、以及这个应用程序的每个组件。
现在让我们修改登录表单如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import React from 'react'; import { Col, Panel, Input, ButtonInput, Glyphicon } from 'react-bootstrap'; class Login extends React.Component { render() { const {error} = this.props; return ( <col xs="{12}" sm="{6}" smoffset="{3}"> <panel> <h1>Login</h1> {error ? <p style="{{color:" 'red'}}="">{error}</p> : null} <form> <input ref="”email”" type="”email”" placeholder="”Email”"> <input ref="”password”" type="”password”" placeholder="”Password”"> <buttoninput onclick="{this.login.bind(this)}" bsstyle="”primary”" type="”submit”" value="”Login”/"> </buttoninput></form> </panel> ) } login(e) { e.preventDefault(); const {loginUser} = this.props; const {email, password} = this.refs; loginUser(email.getValue(), password.getValue()); email.getInputDOMNode().value = ''; password.getInputDOMNode().value = ''; } } export default Login; |
这基本上是一个相同的表单,但我们将用登录方法来代替它的逻辑。
React-Boostrap 非常易于使用,我们只需要安装好项目,使用 import 函数引入每个我们想要引用的组件,就像其他类型一样渲染这些组件。
我们处理使用数据的方法则有一些不同,因为它是组件,而不是我们实际需要处理的输入内容,我们需要使用特殊的 React-Bootstrap 函数 getValue() 来帮我们轻松地取值。
现在,我们将添加新的模块来管理我们的日志条目,首先让我们设置目录和文件。
1 2 3 4 5 6 7 | mkdir client/modules/entries cd client/modules/entries mkdir actions components containers touch index.js touch actions/index.js actions/entries.js touch components/NewEntry.jsx components/Entry.jsx components/EntryList.jsx touch containers/NewEntry.js containers/Entry.js containers/EntryList.js |
好了,现在我们有了应用中所需要的所有文件和文件夹。让我们来做一些真正的开发工作吧。
首先,让我们再来看一下我们创建的应用结构。这里我们制造了一个简单的 Mantra 模块。我们通过这些目录文件来看看他们是怎么做到交互的。通过这些将会让你很好地理解如何使用 Meteor 1.3 和 Mantra 。
索引
Mantra 有一个庞大的单一入口。这个索引文件负责导入内容随后导出路由和动作,这样在我们导入模块时即可使用。通过这种方式我们不用担心再单独导入每个文件。
1 2 3 4 5 6 | import actions from './actions'; import routes from '../core/routes.jsx'; export default { routes, actions }; |
动作
动作文件夹负责我们应用的所有逻辑。你可以看到我们在这里创建了两个文件。首先是一个索引文件。这是一个类似目的模块的索引文件。我们向里面添加下面的内容。
1 2 3 4 | import entries from './entries'; export default { entries }; |
上面所做的就是导入条目文件,在条目文件中有我们的动作逻辑。这只是为了更容易地从其他文件导入我们的逻辑。
接下来我们要添加实际逻辑,这些包含了我们的应用逻辑。这里我们要添加一个创建条目的函数方法。
你可以通过查看例子中 users 模块的方法文件来了解这是怎么工作的。
在 actions.js 中添加下面的内容来补全条目模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 | export default { create({Meteor, LocalState, FlowRouter}, text) { if (!text) { return LocalState.set('CREATE_ENTRY_ERROR', 'Text is required.'); } LocalState.set('CREATE_ENTRY_ERROR', null); Meteor.call('entries.create', text, (err) => { if (err) { return LocalState.set('CREATE_ENTRY_ERROR', err.message); } }); } }; |
当我们填写表格来创建一个新条目时,这就是会被执行的方法,我们就快设置好这些组件了,让我们先别管服务端的东西,为我们的条目创建集合和方法。
在 lib 目录中打开 collections.js 文件然后添加条目集合。
1
| export const Entries = new Mongo.Collection('entries');
|
现在在 server 目录下的 methods 目录中添加 entries.js 文件,并添加以下内容来创建一个创建新条目的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 | import {Entries} from '/lib/collections'; import {Meteor} from 'meteor/meteor'; import {check} from 'meteor/check'; export default function () { Meteor.methods({ 'entries.create'(text) { check(text, String); const createdAt = new Date(); const entry = {text, createdAt}; Entries.insert(entry); } }); } |
这是一个我们刚创建的将要被调用的方法。
我们还需要将下面代码添加到 methods 文件夹中的 index.js 文件。
1 2 3 4 | import entries from './entries'; export default function () { entries(); } |
组件
组件目录存放着我们的 UI 组件。这里的组件只负责显示我们的接口内容,他们不操作任何数据,这些是容器组件需要做的。
让我们创建 UI 组件,然后我们将建立相应的容器组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import React from 'react'; import {Grid, Row, Col} from 'react-bootstrap'; const Entry = ({entry}) => ( <grid> <row> <col xs="{6}" xsoffset="{3}"> <p> {entry.text} </p> </row> </grid> ); export default Entry; |
这里获取到的 {entry} 对象是我们容器组件要传递给它属性。它包含了我们的数据。
接下来我们创建 NewEntry 组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import React from 'react'; import { Col, Panel, Input, ButtonInput, Glyphicon } from 'react-bootstrap'; class NewEntry extends React.Component { render() { const {error} = this.props; return ( <col xs="{12}" sm="{6}" smoffset="{3}"> <panel> <h1>Add a New Entry</h1> {error ? <p style="{{color:" 'red'}}="">{error}</p> : null} <form> <input ref="”text”" type="”textarea”" placeholder="”Add" your="" entry”=""> <buttoninput onclick="{this.newEntry.bind(this)}" bsstyle="”primary”" type="”submit”" value="”Create”/"> </buttoninput></form> </panel> ) } newEntry(e) { e.preventDefault(); const {create} = this.props; const {text} = this.refs; create(text.getValue()); text.getInputDOMNode().value = ''; } } export default NewEntry; |
这里我们使用了更多的 React-Bootstrap 组件,你会留意到为了获取输入的值,我们用了一个特别的 getValue() 方法。这是因为我们的渲染组件实际上并不是输入框,输入框是在这些组件的内部。所以我们需要使用这个函数来访问它。
最后,我们创建一个 EntryList 组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import React from 'react'; import {Grid, Row, Col, Panel} from 'react-bootstrap'; const EntryList = ({entries}) => ( <grid> <row> {entries.map(entry => ( <col xs="{3}" key="{entry._id}"> <panel> <p>{entry.title}</p> <a href="{`/entry/${entry._id}`}">View Entry</a> </panel> ))} </row> </grid> ); export default EntryList; |
接下来,我们通过属性来获取数据,设置一些 React-Bootstrap 组件,并为每个入口映射一个对应专属的面板。
现在,让我们来设置这些容器组件,首先从最简单的 NewEntry 容器组件开始。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import NewEntry from '../components/NewEntry.jsx'; import {useDeps, composeWithTracker, composeAll} from 'mantra-core'; export const composer = ({context, clearErrors}, onData) => { const {LocalState} = context(); const error = LocalState.get('CREATE_ENTRY_ERROR'); onData(null, {error}); return clearErrors; }; export const depsMapper = (context, actions) => ({ create: actions.entries.create, clearErrors: actions.entries.clearErrors, context: () => context }); export default composeAll( composeWithTracker(composer), useDeps(depsMapper) )(NewEntry); |
这里你应该已经对 react-komposer 较为熟悉了,我们将用它来创建这一容器组件。它负责创建一个容器组件,用于处理错误、调用合适的动作。在大多数情况下,它还将获取数据并通过属性传给 UI 组件。
depsMapper 通过 react-komposer 中的 useDeps 函数检索动作及上下文内容并将它们传递给 UI 组件。
clearErrors 方法负责清除组件卸载时发生的所有错误。
让我们在创建条目方法时创建这一方法。
1 2 3 | clearErrors({LocalState}) { return LocalState.set('SAVING_ERROR', null); } |
现在我们将要创建 EntryList 组件的容器。这个稍许有些复杂,因为我们会实际上获取一些数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 | import EntryList from '../components/EntryList.jsx'; import {useDeps, composeWithTracker, composeAll} from 'mantra-core'; export const composer = ({context}, onData) => { const {Meteor, Collections} = context(); if (Meteor.subscribe('entries.list').ready()) { const entries = Collections.Entries.find().fetch(); onData(null, {entries}); } }; export default composeAll( composeWithTracker(composer), useDeps() )(EntryList); |
这也确实与其他容器组件较为相似,但一个重要的区别在于,我们会检查我们的入口集合条目结合,并将它们分配给一个变量。最终我们通过 onData 函数将这个变量传给 UI 组件。
让我们在 publications 目录下的 entries.js 文件中设置发布
1 2 3 4 5 6 7 8 9 10 11 12 13 | import {Entries} from '/lib/collections'; import {Meteor} from 'meteor/meteor'; import {check} from 'meteor/check'; export default function () { Meteor.publish('entries.list', function () { const selector = {}; const options = { fields: {_id: 1, text: 1}, sort: {createdAt: -1} }; return Entries.find(selector, options); }); } |
同时我们将要为此发布创建一个 index 文件。
1 2 3 4 | import entries from './entries'; export default function () { entries(); } |
我们需要在 server 目录中打开 main.js 文件,取消注释行,导入 publications 和 methods ,所以文件就像这样:
1 2 3 4 5 | import publications from './publications'; import methods from './methods'; // publications(); // methods(); |
最后我们将要为独立的 Entry 组件创建容器组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import Entry from '../components/Entry.jsx'; import {useDeps, composeWithTracker, composeAll} from 'mantra-core'; export const composer = ({context, entryId}, onData) => { const {Meteor, Collections} = context(); if (Meteor.subscribe('entries.single', entryId).ready()) { const entry = Collections.Entries.findOne(entryId); onData(null, {entry}); } else { const entry = Collections.Entries.findOne(entryId); if (entry) { onData(null, {entry}); } else { onData(); } } }; export default composeAll( composeWithTracker(composer), useDeps() )(Entry); |
此容器使用了一个 entryId (将通过我们之后设立的一个路由进行传递)并且找到一个合适的入口,来通过属性传递它给UI组件。
让我们在之前设置的发布列表中快速设置发布来展示发布条目。
1 2 3 4 5 | Meteor.publish('entries.single', function (entryId) { check(entryId, String); const selector = {_id: entryId}; return Entries.find(selector); }); |
现在让我们设置我们的路由吧。
路由
打开 routes 文件来添加一些新的路由,修改 routes 文件类似如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | import React from 'react'; import {mount} from 'react-mounter'; import Layout from './components/MainLayout.jsx'; import Home from './components/Home.jsx'; import NewUser from '../users/containers/NewUser.js'; import Login from '../users/containers/Login.js'; import EntryList from '../entries/containers/EntryList.js'; import Entry from '../entries/containers/Entry.js'; import NewEntry from '../entries/containers/NewEntry.js'; export default function (injectDeps, {FlowRouter}) { const MainLayoutCtx = injectDeps(Layout); FlowRouter.route('/', { name: 'items.list', action() { mount(MainLayoutCtx, { content: () => (<entrylist>) }); } }); FlowRouter.route('/entry/:entryId', { name: 'entries.single', action({entryId}) { mount(MainLayoutCtx, { content: () => (<entry entryid="{entryId}/">) }); } }); FlowRouter.route('/new-entry', { name: 'newEntry', action() { mount(MainLayoutCtx, { content: () => (<newentry>) }); } }); FlowRouter.route('/register', { name: 'users.new', action() { mount(MainLayoutCtx, { content: () => (<newuser>) }); } }); FlowRouter.route('/login', { name: 'users.login', action() { mount(MainLayoutCtx, { content: () => (<login>) }); } }); FlowRouter.route('/logout', { name: 'users.logout', action() { Meteor.logout(); FlowRouter.go('/'); } }); }</login></newuser></newentry></entry></entrylist> |
在运行我们的应用之前我们还需要做最后一件事,打开 main.js 文件并导入我们的 entries 模块,修改内容如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import {createApp} from 'mantra-core'; import initContext from './configs/context'; // modules import coreModule from './modules/core'; import usersModule from './modules/users'; import entriesModule from './modules/entries'; // init context const context = initContext(); // create app const app = createApp(context); app.loadModule(coreModule); app.loadModule(usersModule); app.loadModule(entriesModule); app.init(); |
现在我们设置了我们的所有路由并且应用已经准备好运行,让我们切换目录到根目录并运行
1
| meteor
|
你可以看到应用程序在 Mantra 提供的默认加载效果中启动,让我们添加一个条目,这样我们应该可以在屏幕上看到效果了。
访问 localhost:3000/new-entry
,填写并提交表单来添加一个条目。
然后访问根目录,你应该可以看到一个可以逐个查看链接的的条目列表。
希望这个简单的 Mantra 引导以及目前的 Meteor 1.3 beta 版本有助于让你更加了解如何运用它们来构建一个应用。
]]>1 2 3 4 5 6 7 | var deg = 0; block.addEventListener('click', function(){ var self = this; setInterval(function(){ self.style.transform = 'rotate(' + (deg++) +'deg)'; }); }); |
弊端:动画应该用当前时间和开始时间的差值来计算当前动画元素的位置而不是像上例那样用属性增量来实现动画,因为每隔多少毫秒增加一点属性的话,浏览器timer并不能保证在那个准确的时间点执行,而且那样做也很难精确控制动画的各个物理量。(造成丢帧)
建议使用 requestAnimationFrame
定义绘制每一帧前的工作。 requestAnimationFrame(callback)
方法可以自动调节频率。callback 工作太多时无法在一帧内完成,会自动降低为 30 FPS, 虽然频率会降低但比丢帧好。
渲染一帧目标( 1 / 60 FPS = 16 ms-)
具体表格体现如下:
时间 | 增量 | |
---|---|---|
幅度控制 | ✓ | ✓ |
时间控制 | ✓ | × |
速度控制 | ✓ | ✓ |
不会延迟 | ✓ | × |
不会掉帧 | × | ✓ |
1 2 3 4 5 6 7 8 | block.addEventListener('click', function(){ var self = this, startTime = Date.now(); setInterval(function(){ var T = 1000; var p = (Date.now() - startTime) / T; self.style.transform = 'rotate(' + (360 * p) +'deg)'; }); }); |
问:让滑块在 2 秒内向右匀速移动 200px
1 2 3 4 5 6 7 8 9 | block.addEventListener('click', function(){ var self = this, startTime = Date.now(), distance = 200, T = 2000; requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / T); self.style.transform = 'translateX(' + (distance * p) +'px)'; if(p < 1.0) requestAnimationFrame(step); }); }); |
基本公式:
时间:$t = T⋅p$
位移:$S_t = S⋅p = v⋅t$
速度:$v = \frac{S⋅p}{t} = \frac{S}{T}$
加速度:$a = 0$
问:让滑块在 2 秒内向右匀加速移动 200px,速度从 0 开始
1 2 3 4 5 6 7 8 9 | block.addEventListener('click', function(){ var self = this, startTime = Date.now(), distance = 200, T = 2000; requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / T); self.style.transform = 'translateX(' + (distance * p * p) +'px)'; if(p < 1.0) requestAnimationFrame(step); }); }); |
时间:$t = T⋅p$
位移:$S_t = S⋅p^2 = t^2 ⋅\frac{S}{T^2}$
速度:$v = \frac{2S}{T^2}⋅t = \frac{2Sp}{T}$
加速度:$a = \frac{2S}{T^2}$
让滑块在 2 秒内向右匀减速移动 200px,速度从最大减为 0
1 2 3 4 5 6 7 8 9 10 | block.addEventListener('click', function(){ var self = this, startTime = Date.now(), distance = 200, T = 2000; requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / T); self.style.transform = 'translateX(' + (distance * p * (2-p)) +'px)'; if(p < 1.0) requestAnimationFrame(step); }); }); |
时间:$t = T⋅p$
位移:$S_t = \frac{2S}{T}⋅t - t^2 ⋅\frac{S}{T^2} = Sp(2-p)$
速度:$v = \frac{2S(1-p)}{T} = \frac{2S}{T} - \frac{2S}{T^2}⋅t$
加速度:$a = - \frac{2S}{T^2}$
让滑块沿斜线运动 2s,x、y 移动距离都是 200px
1 2 3 4 5 6 7 8 9 10 11 12 13 | block.addEventListener('click', function(){ var self = this, startTime = Date.now(), distance = 200, T = 2000; requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / T); var tx = distance * p; var ty = tx; self.style.transform = 'translate(' + tx + 'px' + ',' + ty +'px)'; if(p < 1.0) requestAnimationFrame(step); }); }); |
demo (就是 x 轴 y 轴各自方向的匀速运动)
让滑块做抛物线运动(平抛)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | block.addEventListener('click', function(){ var self = this, startTime = Date.now(), disX = 200, disY = 200, T = 1000 * Math.sqrt(2 * disY / 98); //假设10px是1米,disY = 20米 requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / T); var tx = disX * p; var ty = disY * p * p; self.style.transform = 'translate(' + tx + 'px' + ',' + ty +'px)'; if(p < 1.0) requestAnimationFrame(step); }); }); |
demo ($y = ax^2 + bx + c$)
让滑块做简谐摆运动(简谐振动)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | block.addEventListener('click', function(){ var self = this, startTime = Date.now(), distance = 100, T = 2000; requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / T); var tx = distance * Math.sin(2 * Math.PI * p); self.style.transform = 'translateX(' + tx + 'px)'; if(p < 1.0) requestAnimationFrame(step); }); }); |
demo (正弦曲线)
让滑块沿正弦曲线运动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | block.addEventListener('click', function(){ var self = this, startTime = Date.now(), distance = 100, T = 2000; requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / T); var ty = distance * Math.sin(2 * Math.PI * p); var tx = 2 * distance * p; self.style.transform = 'translate(' + tx + 'px,' + ty + 'px)'; if(p < 1.0) requestAnimationFrame(step); }); }); |
让滑块做圆周运动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | block.addEventListener('click', function(){ var self = this, startTime = Date.now(), r = 100, T = 2000; requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / T); var rotation = 360 * p; self.style.transformOrigin = '0 ' + r + 'px'; self.style.transform = 'rotate(' + rotation + 'deg)'; if(p < 1.0) requestAnimationFrame(step); }); }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | block.addEventListener('click', function(){ var self = this, startTime = Date.now(), r = 100, T = 2000; requestAnimationFrame(function step(){ var p = Math.min(1.0, (Date.now() - startTime) / T); var tx = -r * Math.sin(2 * Math.PI * p), ty = -r * Math.cos(2 * Math.PI * p); self.style.transform = 'translate(' + tx + 'px,' + ty + 'px)'; if(p < 1.0) requestAnimationFrame(step); }); }); |
圆的轨迹方程
动画时长:$T$
动画进程:$p = \frac{t}{T}(p ∈ [0,1])$
easing:$e = f(p)$
动画方程:$[x,y] = G(e)$
动画开始、进行中、结束:onStart、onProgress、onFinished
对动画进行简易封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | /** * [Animator description] * @param {[type]} duration [时间] * @param {[type]} progress [运动公式] * @param {[type]} easing [缓动函数] */ function Animator(duration, progress, easing){ this.duration = duration; this.progress = progress; this.easing = easing || function(p){return p}; } Animator.prototype = { start: function(finished){ var startTime = Date.now(); var duration = this.duration, self = this; requestAnimationFrame(function step(){ var p = (Date.now() - startTime) / duration;//当前时间 var next = true; if(p < 1.0){ self.progress(self.easing(p), p);//传p执行动画 }else{ if(typeof finished === 'function'){ //结束判断 next = finished() === false; }else{ next = finished === false; } if(!next){ self.progress(self.easing(1.0), 1.0);//动画中断 }else{ startTime += duration;//循环时间 self.progress(self.easing(p), p); } } if(next) requestAnimationFrame(step);//反复执行 }); } }; |
1 2 3 4 5 6 7 8 9 10 11 | var animator = new Animator(2000, function(p){ var tx = -100 * Math.sin(2 * Math.PI * p), ty = -100 * Math.cos(2 * Math.PI * p); block.style.transform = 'translate(' + tx + 'px,' + ty + 'px)'; }); block.addEventListener('click', function(){ animator.start(false); }); |
让滑块先向右然后再向下运动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | var a1 = new Animator(1000, function(p){ var tx = 100 * p; block.style.transform = 'translateX(' + tx + 'px)'; }); var a2 = new Animator(1000, function(p){ var ty = 100 * p; block.style.transform = 'translate(100px,' + ty + 'px)'; }); block.addEventListener('click', function(){ a1.start(function(){ a2.start(); }); }); |
demo这里有两个连续的动画,所以我们最好对动画做一个队列的封装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | function AnimationQueue(animators){ this.animators = animators || []; } AnimationQueue.prototype = { status: 'ready', append: function(){ var args = [].slice.call(arguments); this.animators.push.apply(this.animators, args); }, flush: function(){ if(this.animators.length){ var self = this; function play(){ var animator = self.animators.shift(); animator.start(function(){ if(self.animators.length){ play(); } }); } play(); } } }; |
让滑块沿一个矩形边界运动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | var a1 = new Animator(1000, function(p){ var tx = 100 * p; block.style.transform = 'translateX(' + tx + 'px)'; }); var a2 = new Animator(1000, function(p){ var ty = 100 * p; block.style.transform = 'translate(100px,' + ty + 'px)'; }); var a3 = new Animator(1000, function(p){ var tx = 100 * (1-p); block.style.transform = 'translate(' + tx + 'px, 100px)'; }); var a4 = new Animator(1000, function(p){ var ty = 100 * (1-p); block.style.transform = 'translateY(' + ty + 'px)'; }); block.addEventListener('click', function(){ var animators = new AnimationQueue(); animators.append(a1, a2, a3, a4); animators.flush(); }); |
添加了 animator 是否为 Animator 的实例判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | function AnimationQueue(animators){ this.animators = animators || []; } AnimationQueue.prototype = { status: 'ready', append: function(){ var args = [].slice.call(arguments); this.animators.push.apply(this.animators, args); }, flush: function(){ if(this.animators.length){ var self = this; function play(){ var animator = self.animators.shift(); if(animator instanceof Animator){ animator.start(function(){ if(self.animators.length){ play(); } }); }else{ animator.apply(self); if(self.animators.length){ play(); } } } play(); } } }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | var a1 = new Animator(1000, function(p){ var tx = 100 * p; block.style.transform = 'translateX(' + tx + 'px)'; }); var a2 = new Animator(1000, function(p){ var ty = 100 * p; block.style.transform = 'translate(100px,' + ty + 'px)'; }); var a3 = new Animator(1000, function(p){ var tx = 100 * (1-p); block.style.transform = 'translate(' + tx + 'px, 100px)'; }); var a4 = new Animator(1000, function(p){ var ty = 100 * (1-p); block.style.transform = 'translateY(' + ty + 'px)'; }); block.addEventListener('click', function(){ var animators = new AnimationQueue(); animators.append(a1, a2, a3, a4, function(){ this.append(a1, a2, a3, a4, arguments.callee); }); animators.flush(); }); |
小球方程 设 20px 为 1 米,下落距离为 200px(10 米)
$g \approx 10米/秒$
$S = \frac{1}{2}gT^2 = 10米$
$T = \sqrt\frac{2S}{g} = \sqrt2\approx1.414秒 = 1414毫秒$
下落阶段:$S_t = Sp^2$
上升阶段:$S_t = S - S_p(2 - p)$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | var a1 = new Animator(1414, function(p){ var ty = 200 * p * p; block.style.transform = 'translateY(' + ty + 'px)'; }); var a2 = new Animator(1414, function(p){ var ty = 200 - 200 * p * (2-p); block.style.transform = 'translateY(' + ty + 'px)'; }); block.addEventListener('click', function(){ var animators = new AnimationQueue(); animators.append(a1,a2, function(){ this.append(a1, a2, arguments.callee); }); animators.flush(); }); |
设每一个周期损耗为0.7:$T = 0.7T$ ` 上升距离:$S = 0.49S$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | block.addEventListener('click', function(){ var T = 1414; var a1 = new Animator(T, function(p){ var s = this.duration * 200 / T; var ty = s * (p * p - 1); block.style.transform = 'translateY(' + ty + 'px)'; }); var a2 = new Animator(T, function(p){ var s = this.duration * 200 / T; var ty = - s * p * (2-p); block.style.transform = 'translateY(' + ty + 'px)'; }); var animators = new AnimationQueue(); function foo(){ a2.duration *= 0.7; if(a2.duration <= 0.0001){ animators.animators.length = 0; } } animators.append(a1 ,foo, a2, function b(){ a1.duration *= 0.7; this.append(a1, foo, a2, b); }); animators.flush(); }); |
小球直径:$d = 50px$
圆周长:$l = πd$
周期:$T = 2秒$
滚动时间:$t_{max} = 4秒$
滚动距离等于:$S = πd·\frac{t_{max}}{T} = 314px$
1 2 3 4 5 6 7 8 9 10 11 | var a1 = new Animator(4000, function(p){ var rotation = 'rotate(' + 720 * p + 'deg)'; var x = 50 + 314 * p + 'px'; block.style.transform = rotation; block.style.left = x; }); block.addEventListener('click', function(){ a1.start(); }); |
1)小球做半径 100px 的匀速圆周运动,周期 2s 2)在 2.8s 后小球从手中甩出
甩出小球公式:
$x = − rsin(πt)$
$v_x = − πrcos(πt)$
$y = r − rcos(πt)$
$v_y = πrsin(πt)$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | var a1 = new Animator(2800, function(p){ var x = -100 * Math.sin(2.8 * Math.PI * p); var y = 100 - 100 * Math.cos(2.8 * Math.PI * p); block.style.transform = 'translate(' + x + 'px,' + y + 'px)'; }); var a2 = new Animator(5000, function(p){ var x = -100 * Math.sin(2.8 * Math.PI) -100 * Math.cos(2.8 * Math.PI) * Math.PI * 5 * p; var y = 100 - 100 * Math.cos(2.8 * Math.PI) + 100 * Math.sin(2.8 * Math.PI) * Math.PI * 5 * p; block.style.transform = 'translate(' + x + 'px,' + y + 'px)'; }); block.addEventListener('click', function(){ a1.start(function(){ a2.start(); }); }); |
滑块 1s 匀加速运动 100px,匀速运动 100px, 然后再经过 50px 速度减为 0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | var a1 = new Animator(1000, function(p){ var x = 100 * p * p; block.style.transform = 'translateX(' + x + 'px)'; }); var a2 = new Animator(500, function(p){ var x = 100 + 100 * p; block.style.transform = 'translateX(' + x + 'px)'; }); var a3 = new Animator(500, function(p){ var x = 200 + 50 * p * (2 - p); block.style.transform = 'translateX(' + x + 'px)'; }); block.addEventListener('click', function(){ var animators = new AnimationQueue(); animators.append(a1, a2, a3); animators.flush(); }); |
此类型运动运动过程均可以用熟悉的速度-时间图表示
我们可以使用二阶贝塞尔曲线来构造平滑动画,这里如果对贝塞尔曲线还不太了解,可以看看下面这篇详解 贝塞尔曲线扫盲
这里有一些贝塞尔曲线的实现资源库和文档:
借助资源库我们可以方便实现简单的贝塞尔曲线动画
1 2 3 4 5 6 7 8 9 10 11 12 13 | var easing = BezierEasing(0.86, 0, 0.07, 1); //easeInOutQuint var a1 = new Animator(2000, function(ep,p){ var x = 200 * ep; block.style.transform = 'translateX(' + x + 'px)'; }, easing); block.addEventListener('click', function(){ a1.start(); }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 | var easing = BezierEasing(0.68, -0.55, 0.265, 1.55); //easeInOutQuint var a1 = new Animator(2000, function(ep,p){ var x = 200 * ep; block.style.transform = 'translateX(' + x + 'px)'; }, easing); block.addEventListener('click', function(){ a1.start(); }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var easing = BezierEasing(0.68, -0.55, 0.265, 1.55); //easeInOutQuint var a1 = new Animator(2000, function(ep,p){ var x = 200 * ep; var y = -200 * p; block.style.transform = 'translate(' + x + 'px,' + y + 'px)'; }, easing); block.addEventListener('click', function(){ a1.start(); }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | var easing = BezierEasing(0.68, -0.55, 0.265, 1.55); //easeInOutQuint var a1 = new Animator(2000, function(ep,p){ var y = -200 * ep; var x = 200 * p; var r = 360 * ep; block.style.transform = 'translate(' + x + 'px,' + y + 'px) rotateY(' + r + 'deg)'; }, easing); block.addEventListener('click', function(){ a1.start(); }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <style type="text/css"> .sprite {display:inline-block; overflow:hidden; background-repeat: no-repeat;background-image:url(http://res.h5jun.com/matrix/8PQEganHkhynPxk-CUyDcJEk.png);} .bird0 {width:86px; height:60px; background-position: -178px -2px} .bird1 {width:86px; height:60px; background-position: -90px -2px} .bird2 {width:86px; height:60px; background-position: -2px -2px} #bird{ position: absolute; left: 100px; top: 100px; zoom: 0.5; } </style> <div id="bird" class="sprite bird1"></div> <script type="text/javascript"> var i = 0; setInterval(function(){ bird.className = "sprite " + 'bird' + ((i++) % 3); }, 1000/10); </script> |
transitions属性:
支持浏览器:IE10+,GC,FF
timing functions属性:
linear
(规定以相同速度开始至结束的过渡效果(等于 cubic-bezier(0,0,1,1))
ease
(规定慢速开始,然后变快,然后慢速结束的过渡效果(cubic-bezier(0.25,0.1,0.25,1))
ease-in
(规定以慢速开始的过渡效果(等于 cubic-bezier(0.42,0,1,1))
ease-out
(规定以慢速结束的过渡效果(等于 cubic-bezier(0,0,0.58,1))
ease-in-out
(规定以慢速开始和结束的过渡效果(等于 cubic-bezier(0.42,0,0.58,1))
cubic-bezier(n,n,n,n)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <style> #block{ position:absolute; left: 200px; top: 100px; width: 20px; height: 20px; background: #0c8; text-align: center; border-radius: 50%; transform-origin: 0 100px; transform: rotate(0deg); } #block.play { transform: rotate(360deg); transition: transform 2.0s linear; } </style> <div id="block"></div> <script> block.addEventListener('click', function(){ block.className = 'play'; }); </script> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <style> #block{ position:absolute; left: 50px; top: 200px; width: 20px; height: 20px; background: #0c8; text-align: center; border-radius: 50%; } #block.play { transform: translateX(200px); transition: transform 2.0s cubic-bezier(0.68, -0.55, 0.265, 1.55); } </style> <div id="block"></div> <script> block.addEventListener('click', function(){ block.className = 'play'; }); </script> |
1 2 3 4 5 6 7 8 9 10 11 12 | #block.play { border-radius: 0; transform: scale(2.0); background: #c80; transition: all 2.0s cubic-bezier(0.68, -0.55, 0.265, 1.55) 3s; } #block.play2 { /* transition 覆盖*/ background: #c8f; transition: all 2.0s linear 0.5s; transform: scale(2.0) rotate(360deg); } |
animations:
支持浏览器:IE10+,GC,FF
主要属性:
keyframes'name
(规定需要绑定到选择器的 keyframe 名称)
duration
(规定完成动画所花费的时间,以秒或毫秒计)
timing functions
(规定动画的速度曲线)
delay
(规定在动画开始之前的延迟)
iteration count
(规定动画应该播放的次数)
direction
(规定是否应该轮流反向播放动画)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #block{ position:absolute; left: 200px; top: 100px; width: 20px; height: 20px; background: #0c8; text-align: center; border-radius: 50%; animation: roll 2.0s linear 0s infinite alternate; transform-origin: 0 100px; } @keyframes roll{ 0%{transform:rotate(0deg)} 100%{transform:rotate(360deg)} } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #block{ position:absolute; left: 200px; top: 100px; width: 20px; height: 20px; background: #0c8; text-align: center; border-radius: 50%; animation: roll 4.0s linear 0s infinite; transform-origin: 0 100px; } @keyframes roll{ 0%{transform:rotate(0deg)} 50%{transform:rotate(360deg)} 100%{transform:rotate(0deg)} } |
动画组合:
主要内容:
animation-fill-mode
webkitAnimationEnd
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | <style> #block{ position:absolute; left: 150px; top: 200px; width: 20px; height: 20px; background: #0c8; text-align: center; border-radius: 50%; animation: anim 2.0s linear 0s forwards; } @keyframes anim{ 0%{border-radius: 50%} 50%{border-radius: 0; background: #c80;} 100%{border-radius: 20%; transform:scale(2.0); background: #08c;} } </style> <div id="block"></div> <script> function Animator(duration, progress, easing){ this.duration = duration; this.progress = progress; this.easing = easing || function(p){return p}; } Animator.prototype = { start: function(finished){ var startTime = Date.now(); var duration = this.duration, self = this; requestAnimationFrame(function step(){ var p = (Date.now() - startTime) / duration; var next = true; if(p < 1.0){ self.progress(self.easing(p), p); }else{ if(typeof finished === 'function'){ next = finished() === false; }else{ next = finished === false; } if(!next){ self.progress(self.easing(1.0), 1.0); }else{ startTime += duration; self.progress(self.easing(p), p); } } if(next) requestAnimationFrame(step); }); } }; var easing = BezierEasing(0.68, -0.55, 0.265, 1.55); var a1 = new Animator(2000, function(ep,p){ var x = 150 + 200 * ep; block.style.left = x + 'px'; }, easing); block.addEventListener('webkitAnimationEnd', function(){ a1.start(); }); </script> |
具体参考即为如下几点: 由于渲染三阶段分为:Layout--->Paint--->Composite,针对此三者进行优化。 优化目标:15FPS 不流畅 ,30FPS+ 感觉流畅,60FPS 舒适完美
触发 Layout 的方式有:
触发 Paint 的方式: 当修改 border-radius, box-shadow,color 等展示相关属性时,会触发 paint 要点:
Composite 小结: