这里会收录一些好玩的 js 技巧,写的不全,欢迎大家随时补充啊。
以下场景往往由于事件频繁被触发,因而频繁执行DOM操作、资源加载等重行为,导致UI停顿甚至浏览器崩溃。
-
window对象的resize、scroll事件
-
拖拽时的mousemove事件
-
射击游戏中的mousedown、keydown事件
-
文字输入、自动完成的keyup事件
实际上对于window的resize事件,实际需求大多为停止改变大小n毫秒后执行后续处理;而其他事件大多的需求是以一定的频率执行后续处理。针对这两种需求就出现了debounce
和throttle
两种解决办法。
throttle
和 debounce
是解决请求和响应速度不匹配问题的两个方案。二者的差异在于选择不同的策略。
- throttle 等时间间隔执行函数。
- debounce 时间间隔 t 内若再次触发事件,则重新计时,直到停止时间大于或等于 t 才执行函数。
当调用动作n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间。
接口定义如下:
/**
* 空闲控制 返回函数连续调用时,空闲时间必须大于或等于 idle,action 才会执行
* @param idle {number} 空闲时间,单位毫秒
* @param action {function} 请求关联函数,实际应用需要调用的函数
* @return {function} 返回客户调用函数
*/
debounce(idle, action)
undersorce.js 中实现方案如下:
_.debounce = function(func, wait, immediate) {
// immediate默认为false
var timeout, args, context, timestamp, result;
var later = function() {
// 当wait指定的时间间隔期间多次调用_.debounce返回的函数,则会不断更新timestamp的值,导致last < wait && last >= 0一直为true,从而不断启动新的计时器延时执行func
var last = _.now() - timestamp;
if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;
}
}
};
return function() {
context = this;
args = arguments;
timestamp = _.now();
// 第一次调用该方法时,且immediate为true,则调用func函数
var callNow = immediate && !timeout;
// 在wait指定的时间间隔内首次调用该方法,则启动计时器定时调用func函数
if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
};
};
预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期。
接口定义如下:
/**
* 频率控制 返回函数连续调用时,action 执行频率限定为 次 / delay
* @param delay {number} 延迟时间,单位毫秒
* @param action {function} 请求关联函数,实际应用需要调用的函数
* @return {function} 返回客户调用函数
*/
throttle(delay,action)
undersorce.js 中实现方案如下:
_.throttle = function(func, wait, options) {
/* options的默认值
* 表示首次调用返回值方法时,会马上调用func;否则仅会记录当前时刻,当第二次调用的时间间隔超过wait时,才调用func。
* options.leading = true;
* 表示当调用方法时,未到达wait指定的时间间隔,则启动计时器延迟调用func函数,若后续在既未达到wait指定的时间间隔和func函数又未被调用的情况下调用返回值方法,则被调用请求将被丢弃。
* options.trailing = true;
* 注意:当options.trailing = false时,效果与上面的简单实现效果相同
*/
var context, args, result;
var timeout = null;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
var now = _.now();
if (!previous && options.leading === false) previous = now;
// 计算剩余时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 当到达wait指定的时间间隔,则调用func函数
// 精彩之处:按理来说remaining <= 0已经足够证明已经到达wait的时间间隔,但这里还考虑到假如客户端修改了系统时间则马上执行func函数。
if (remaining <= 0 || remaining > wait) {
// 由于setTimeout存在最小时间精度问题,因此会存在到达wait的时间间隔,但之前设置的setTimeout操作还没被执行,因此为保险起见,这里先清理setTimeout操作
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// options.trailing=true时,延时执行func函数
timeout = setTimeout(later, remaining);
}
return result;
};
};
memoization适用于递归计算场景,递归操作会随着递归次数的增加,导致执行时间的成指数上升。 memoization的技巧在于将计算过的结果『缓存』下来,避免重复计算带来的成本,提高执行效率。
Memoization 是一种将函数返回值缓存起来的方法,Memoization 原理非常简单,就是把函数的每次执行结果都放入一个键值对(数组也可以,视情况而定)中,在接下来的执行中,在键值对中查找是否已经有相应执行过的值,如果有,直接返回该值,没有才 真正执行函数体的求值部分。很明显,找值,尤其是在键值对中找值,比执行函数快多了。现代 JavaScript 的开发也已经大量使用这种技术。
例如计算斐波那契数列:
function fibonacci(n) {
count++;
if (n == 0 || n == 1) {
return n;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
当n逐渐增大,运行时间越来越长。
这里我们写一个简单的 memoizer
函数来测试一下:
function memoizer(fundamental, cache) {
cachecache = cache || {};
var shell = function(arg) {
if (! (arg in cache)) {
cache[arg] = fundamental(shell, arg);
}
return cache[arg];
};
return shell;
}
重新包装 fibonacci
函数:
var fibonacci = memoizer(function(recur, n) {
return recur(n - 1) + recur(n - 2);
}, { "0": 0, "1": 1} );
fibonacci(50);
这次会很快计算出想要的结果。关于 memozization 更多细节内容,请查看性能优化:memoization
试着想一下,如果在某一个场景,我们做一个游戏,用户玩到一半的时候退出了,这样用户下次进来可以选择继续上一次的进度继续玩,那么现在问题来了:用户玩的进度以及用户的积分等简单的描述数据,我们都可以记录下来,但是游戏里绘制的障碍物、飞行物以及很多装饰类的小玩意儿,他们甚至是每次用户点开始随机输出的,要把画布上所有的东西以及它们的大小,位置等都记录下来,实在是没必要。
于是种子随机数就闪亮登场了,我们如果在画布上元素随机绘制的时候,有一个种子值,页面上所有元素的位置、大小等都是根据这个种子来算的,那么等到第二次绘制的时候只需要传入这个种子,就可以重现之前未完成的画布元素。
这个时候我们用 Math.random 就不好用了,为此我们需要实现一个指定种子的随机数,如果种子不变,那么随机数不会产生变化。
方案1:
Math.random = function(seed){
return ('0.'+Math.sin(seed).toString().substr(6));
}
这也是一个比较常用的方案
方案2:
Math.seed = 5;
Math.seededRandom = function(max, min) {
max = max || 1;
min = min || 0;
Math.seed = (Math.seed * 9301 + 49297) % 233280;
var rnd = Math.seed / 233280.0;
return min + rnd * (max - min);
};
像Math.seededRandom这种伪随机数生成器叫做线性同余生成器(LCG, Linear Congruential Generator),几乎所有的运行库提供的rand都是采用的LCG,形如:
I n+1=aI n+c(mod m)
生成的伪随机数序列最大周期m,范围在0到m-1之间。要达到这个最大周期,必须满足:
- c与m互质
- a - 1可以被m的所有质因数整除
- 如果m是4的倍数,a - 1也必须是4的倍数
以上三条被称为Hull-Dobell定理。作为一个伪随机数生成器,周期不够大是不好意思混的,所以这是要求之一。因此才有了:a=9301, c = 49297, m = 233280这组参数,以上三条全部满足。
方案3:
我们可以采用seedRandom库来实现。
Navigation Timing 是一个可以在web中精确测量性能的javascript API。这个API提供了一个简单的方法来获得页面导航、加载事件的精确而又详细的时间状态。目前在 IE9、Chrome、Firefox nightly builds 中可用。
该API通过window.performance对象的属性来报告页面被导航或被加载的时间及相关信息。
- navigation:用户是怎样导航到当前页面的。
- timing:页面被导航或被加载完毕时所耗费的时间。
- 在Chrome浏览器中,同时提供performance.memory属性,用于报告JavaScript的内存耗费情况。
function getPerformanceTiming () {
var performance = window.performance;
if (!performance) {
// 当前浏览器不支持
console.log('你的浏览器不支持 performance 接口');
return;
}
var t = performance.timing;
var times = {};
//【重要】页面加载完成的时间
//【原因】这几乎代表了用户等待页面可用的时间
times.loadPage = t.loadEventEnd - t.navigationStart;
//【重要】解析 DOM 树结构的时间
//【原因】反省下你的 DOM 树嵌套是不是太多了!
times.domReady = t.domComplete - t.responseEnd;
//【重要】重定向的时间
//【原因】拒绝重定向!比如,http://example.com/ 就不该写成 http://example.com
times.redirect = t.redirectEnd - t.redirectStart;
//【重要】DNS 查询时间
//【原因】DNS 预加载做了么?页面内是不是使用了太多不同的域名导致域名查询的时间太长?
// 可使用 HTML5 Prefetch 预查询 DNS ,见:[HTML5 prefetch](http://segmentfault.com/a/1190000000633364)
times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;
//【重要】读取页面第一个字节的时间
//【原因】这可以理解为用户拿到你的资源占用的时间,加异地机房了么,加CDN 处理了么?加带宽了么?加 CPU 运算速度了么?
// TTFB 即 Time To First Byte 的意思
// 维基百科:https://en.wikipedia.org/wiki/Time_To_First_Byte
times.ttfb = t.responseStart - t.navigationStart;
//【重要】内容加载完成的时间
//【原因】页面内容经过 gzip 压缩了么,静态资源 css/js 等压缩了么?
times.request = t.responseEnd - t.requestStart;
//【重要】执行 onload 回调函数的时间
//【原因】是否太多不必要的操作都放到 onload 回调函数里执行了,考虑过延迟加载、按需加载的策略么?
times.loadEvent = t.loadEventEnd - t.loadEventStart;
// DNS 缓存时间
times.appcache = t.domainLookupStart - t.fetchStart;
// 卸载页面的时间
times.unloadEvent = t.unloadEventEnd - t.unloadEventStart;
// TCP 建立连接完成握手的时间
times.connect = t.connectEnd - t.connectStart;
return times;
}
ES6 指定默认参数在它们被实际使用的时候才会被执行,这个特性让我们可以强制要求传参:
/**
* Called if a parameter is missing and
* the default value is evaluated.
*/
function mandatory() {
throw new Error("Missing parameter");
}
function foo(mustBeProvided = mandatory()) {
return mustBeProvided;
}
函数调用 mandatory()
只有在参数 mustBeProvided
缺失的时候才会被执行。
方法 forEach()
允许你遍历一个数组的元素和索引:
var arr = ["a", "b", "c"];
arr.forEach(function (elem, index) {
console.log("index = "+index+", elem = "+elem);
});
// Output:
// index = 0, elem = a
// index = 1, elem = b
// index = 2, elem = c
ES6 的 for-of 循环支持 ES6 迭代(通过 iterables 和 iterators)和解构。如果你通过数组的新方法 enteries() 再结合解构,可以达到上面 forEach 同样的效果:
const arr = ["a", "b", "c"];
for (const [index, elem] of arr.entries()) {
console.log(`index = ${index}, elem = ${elem}`);
}
如果你将一对变量放入一个数组,然后将数组解构赋值相同的变量(顺序不同),你就可以不依赖中间变量交换两个变量的值:
[a, b] = [b, a];
ES6 的模板字面量与文字模板相比,更接近于字符串字面量。但是,如果你将它们通过函数返回,你可以使用他们来做简单的模板渲染:
const tmpl = addrs => `
<table>
${addrs.map(addr => `
<tr><td>${addr.first}</td></tr>
<tr><td>${addr.last}</td></tr>
`).join("")}
</table>
`;
tmpl 函数将数组 addrs 用 map(通过箭头函数) join 拼成字符串。tmpl() 可以批量插入数据到表格中:
const data = [
{ first: "<Jane>", last: "Bond" },
{ first: "Lars", last: "<Croft>" },
];
console.log(tmpl(data));
// Output:
// <table>
//
// <tr><td><Jane></td></tr>
// <tr><td>Bond</td></tr>
//
// <tr><td>Lars</td></tr>
// <tr><td><Croft></td></tr>
//
// </table>
当 ES6 类继承另一个类,被继承的类可以是通过任意表达式创建的动态类:
// Function id() simply returns its parameter
const id = x => x;
class Foo extends id(Object) {}
这个特性可以允许你实现一种合成器模式,用一个函数来将一个类 C 映射到一个新的继承了C的类。例如,下面的两个函数 Storage 和 Validation 是合成器:
const Storage = Sup => class extends Sup {
save(database) { ··· }
};
const Validation = Sup => class extends Sup {
validate(schema) { ··· }
};
你可以使用它们去组合生成一个如下的 Employee 类:
class Person { ··· }
class Employee extends Storage(Validation(Person)) { ··· }