这里整理记录以下过去一段时间学习和收集的一些JavaScript高级技巧
-
-
Save uolcano/98d2525de5b9d4b8e8be155211c136b8 to your computer and use it in GitHub Desktop.
在多个全局作用域的情况下,instanceof
会出现类型检测出错的情况,需要使用更安全的Object.prototype.toString.call(param)
方案。
但是这个方案有其局限性,可以利用ES原生操作符或属性补充。
方案对比\检测对象 | 原始值类型 | 内置引用类型 | 自定义引用类型 | 描述 |
---|---|---|---|---|
typeof |
可检测,除Null类型外 | Function类型可检测,其他检测为"object" |
检测为"object" |
常用于原始值和函检测 |
instanceof |
—— | 可检测 | 可检测 |
|
Object.prototype.toString.call |
可检测 | 可检测 | 检测为"Object" |
只能准确检测原生类型 |
实例的constructor 属性 |
可检测,除Undefined和Null类型外 | 可检测 | 可检测 | 实例的constructor 属性继承自构造函数对应的原型 |
结合Object.prototype.toString.call
和实例的constructor
属性,可较全面准确地检测数据类型
/**
* 跨全局作用域的,全类型检测
* @param {any} param 待检测数据
* @return {string} 被检测数据的类型
*/
function typeOf (param) {
// 检测ES原生类型
var type = Object.prototype.toString.call(param).slice(8, -1);
// Object.create(null)生成的对象,以及宿主环境里的对象并未继承Object.prototype
// 需要借用Object.prototype.hasOwnProperty,检测对象的自有属性
var _hasOwn = Object.prototype.hasOwnProperty;
// 检测自定义类型
if (type == 'Object') {
if (param.constructor &&
// 排除Object类型
!_hasOwn.call(param, 'constructor') &&
!_hasOwn.call(param, 'isPrototypeOf')) {
type = param.constructor.toString().match(/function\s*([^\(\s]*)/)[1];
}
}
// 返回的数据类型跟数据的构造函数名相同,即类型名区分大小写
return type;
}
惰性载入是一种提升加载和运算性能的技术。
前端主要存在两种形式的惰性载入:
- 函数的惰性初始化
- 页面内容的延迟加载
函数惰性载入常用于浏览器兼容性判断的逻辑分支中替代原有函数,避免反复判断带来的性能损耗。
有两种实现方式,区分在于逻辑分支替换原有函数的时机:
-
当第一次调用该函数时进行替换,然后执行
function createXHR(){ if (typeof XMLHttpRequest != "undefined"){ // 替换原函数 createXHR = function(){ return new XMLHttpRequest(); }; } else if (typeof ActiveXObject != "undefined"){ // 替换原函数 createXHR = function(){ if (typeof arguments.callee.activeXString != "string"){ var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"], i, len; for (i=0,len=versions.length; i < len; i++){ try { new ActiveXObject(versions[i]); arguments.callee.activeXString = versions[i]; } catch (ex){} } } return new ActiveXObject(arguments.callee.activeXString); }; } else { createXHR = function(){ throw new Error("No XHR object available."); }; } // 最后执行被替换后的函数 return createXHR(); }
-
在函数F声明的时候就执行一个匿名函数,匿名函数的返回值作为函数F的主体
var createXHR = (function(){ // 立即执行函数的返回值做为声明的函数 if (typeof XMLHttpRequest != "undefined"){ return function(){ return new XMLHttpRequest(); }; } else if (typeof ActiveXObject != "undefined"){ return function(){ if (typeof arguments.callee.activeXString != "string"){ var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"], i, len; for (i=0,len=versions.length; i < len; i++){ try { new ActiveXObject(versions[i]); arguments.callee.activeXString = versions[i]; break; } catch (ex){} } } return new ActiveXObject(arguments.callee.activeXString); }; } else { return function(){ throw new Error("No XHR object available."); }; } })(); // 加载即执行
延迟加载的目的是按需加载,提升页面初加载速度
-
瀑布流/无限卷动是较常见的延迟加载技术,下面是一个简单的图片延迟载入示例
<head> <meta charset="UTF-8"> <title>Infinite Scroll</title> <style> body {width: 100%;height:100%;} img {display: block;margin-bottom: 50px;height: 400px;} </style> </head> <body> <img src="loading.gif" data-src="../images/bg/01.jpg"> <img src="loading.gif" data-src="../images/bg/02.jpg"> <img src="loading.gif" data-src="../images/bg/03.jpg"> <img src="loading.gif" data-src="../images/bg/04.jpg"> <img src="loading.gif" data-src="../images/bg/05.jpg"> <img src="loading.gif" data-src="../images/bg/06.jpg"> <img src="loading.gif" data-src="../images/bg/07.jpg"> <img src="loading.gif" data-src="../images/bg/08.jpg"> <script type="text/javascript"> function infiScroll () { var images = document.getElementsByTagName('img'), len = images.length, n = 0; return function () { var seeHeight = document.documentElement.clientHeight || document.body.clientHeight, scrollTop = document.documentElement.scrollTop || document.body.scrollTop, img; for (var i = n; i < len; i++) { img = images[i]; if (img.offsetTop <= seeHeight + scrollTop) { if (img.getAttribute('src') === 'loading.gif') { img.src = img.getAttribute('data-src'); n++; } } } }; } var loadImages = infiScroll(); window.addEventListener('load', loadImages, false); window.addEventListener('scroll', loadImages, false); </script> </body>
目前Chrome 51+版本的浏览器实现了一个新的IntersectionObserver API,可以异步地自动“观察”元素是否进入了可视区域。上面页面代码的脚本部分可以全部替换成如下来实现:
var io = new IntersectionObserver(function(items) { items.forEach(function(item) { var target = item.target; if (target.getAttribute('src') === 'loading.gif') { target.src = target.getAttribute('data-src'); } }); }); Array.from(document.getElementsByTagName('img')).forEach(function(item) { // 插入观察队列 io.observe(item); });
-
脚本和样式表,也可以动态加载。尤其是在页面需要加载的数据大且部分数据暂时用不上的时候,特别有用。
// 动态加载脚本 function loadScript(url) { var script = document.createElement('script'); script.type = 'text/javascript'; script.src = url; document.body.appendChild(script); } // 动态加载样式表 function loadStyles(url) { var link = document.createElement('link'); link.rel = 'stylesheet'; link.type = 'text/css'; link.href = url; document.getElementsByTagName('head')[0].appendChild(link); }
动态脚本技术还可以应用在跨站数据访问CORS上,即JSONP技术。
- JavaScript高级程序设计(第三版) P277 P600
- 延迟加载(Lazyload)三种实现方式
- IntersectionObserver API
函数绑定是JavaScript一大特性第一类函数(First-class Function)的展现 —— 函数可以作为参数传入另一个函数,并且返回别的函数,这个函数已经绑定了固定的参数或作用域。
研究函数绑定的内部实现原理,可以帮助我们加深对JavaScript这门语言的了解
从简单的开始,了解其原理
function bind(fn, context) { // fn是待绑定作用域的函数
return function () {
return fn.apply(context, arguments); // 只绑定了作用域
}
}
ES5开始,function类型提供了原生的bind方法Function.prototype.bind(thisArg, arg1, arg2, ...)
var math = {
a: 1,
b: 2,
add: function () {
return this.a + this.b;
}
};
math.add.bind({a: 10, b: -1})(); // output: 9
但是旧版本的浏览器不支持,时常需要兼容的代码封装
if (!Function.prototype.bind) {
Function.prototype.bind = function (context) {
// 由于arguments不是真Array类型,故借用数组原型的slice方法取带绑定参数
var args = Array.prototype.slice.call(arguments, 1),
_self = this;
return function () {
// 拼接绑定的参数和后接收的参数,最后一起传入绑定作用域的函数,执行
return _self.apply(context, args.concat(Array.prototype.slice.call(arguments)));
}
}
}
完整版
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
//建立“参数池”,并存储第一次绑定参数,this指向的对象除外
var aArgs = Array.prototype.slice.call(arguments, 1),
//第一次绑定的this指向的对象
fToBind = this,
fNOP = function() {},
fBound = function() {
// 如果fToBind是构造函数,保留指向实例的this,如果不是,将fToBind绑定到传入的oThis作用域上
return fToBind.apply(this instanceof fNOP ? this : oThis,
// 返回的函数可以再次接受参数,并通过concat补充到“参数池”
aArgs.concat(Array.prototype.slice.call(arguments)));
};
if (this.prototype) {
// 注意特殊点:js的function既是函数也是对象(有prototype属性),但是Function的原型属性prototype是没有prototype属性的
// 当this == Function.prototype时,Function.prototype没有prototype
// 当fToBind是一个构造函数时,fBound函数中的this是这个构造函数对应的原型的实例的实例
fNOP.prototype = this.prototype;
}
//fBound->fBound.prototype==>someProto === fToBind.prototype, this === bind的调用者 === fToBind
// ↑
// fNOP.prototype
// 构成原型链继承,fBound构建的实例继承了fNOP.prototype(即this.prototype或fToBind.prototype)
// 这一段对应fBound的函数声明中“this instanceof fNOP”的条件操作
fBound.prototype = new fNOP();
return fBound;
};
}
函数柯理化(currying),把可以接受多个参数的函数转变为一个接受部分参数的函数,这个函数会返回一个接受剩余参数并且返回结果的新函数的技术。
这个技术可以帮助我们把复杂的逻辑拆分成多个小的,进行逐步或独立的运算,便于测试与调整。应用面很广,在异步逻辑中很常见。
Javascript的函数柯理化,应用到了闭包和第一类函数的特性。在绑定参数方面,原理跟bind函数相似。
我们来看一个简单的应用
function add (n) {
return function (m) {
return n + m;
}
}
add(1)(3); // 4
也可以传入数个参数,在函数声明时并未知道明确多少个
function add () {
var _slice = Array.prototype.slice,
args1 = _slice.call(arguments);
return function () {
var args2 = _slice.call(arguments);
return args1.concat(args2).reduce(function (acc, val) {return acc + val});
}
}
add(1, 3)(5, 7, 9);
当然为了代码复用,我们可以封装一个柯理化工具函数
var _slice = Array.prototype.slice;
function add() {
return _slice.call(arguments).reduce(function(acc, val) {
return acc + val;
});
}
function curry(fn) {
var args1 = _slice.call(arguments, 1);
return function() {
var args2 = _slice.call(arguments);
return fn.apply(null, args1.concat(args2));
}
}
function curryAdv(fn, len) {
var len = len || fn.length;
return function() {
var args = _slice.call(arguments);
if (args.length >= len) {
return fn.apply(null, args);
} else {
// 作为传入curry的参数列表,第一个参数是需要柯理化的函数fn
var argsToFill = [fn].concat(args);
// 利用apply和curry将argsToFill绑定到返回的一个匿名函数
// 因为这是递归,根据尾调优化,需要优化性能就最好放在逻辑分支最后
// 且只传入数据而不需等待返回值再操作
return curryAdv(curry.apply(null, argsToFill), len - args.length);
}
}
}
var sum = curry(add, 1, 3);
sum(5, 7, 9); // 25
var sumAdv = curryAdv(add, 5);
sumAdv(1, 3)(5)(7, 9); // 25
在异步事件处理时,柯理化可以帮助我们提高代码可读性
// 由于CORS跨域保护
// 这段代码需要在任何mozilla.org域名的页面下运行
// 可用chrome的devtools测试
function ajax(method, url) {
var xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function() {
pipe(xhr);
}
xhr.send(null);
var pipe = function () {};
return {
getFn: function(fn) {
pipe = fn;
}
};
}
ajax('get', 'https://developer.mozilla.org/')
.getFn(function(xhr) {
console.log(succeed to get ajax, xhr.statusText);
});
一个关于柯理化的面试题,非常有趣。要求实现如下输出的sum函数
sum(1); // 1
sum(1)(2); // 3
sum(1)(2)(3); // 6
实现如下
// 返回值是Function类型的
function sum(n) {
var fn = function(x) {
if (x !=null) n+=x;
return fn;
};
fn.valueOf = function() {
return n;
};
return fn;
}
// type Function
sum(1); // 1
sum(1)(2); // 3
sum(1)(2)(3); // 6
// type Number
+sum(1)(2)(3); // 6
其实返回值fn
是Function类型的,只不过在调试工具的console中或者通过window.console
这类函数打印结果时是会隐式地、依次地调用函数fn
的valueOf
和toString
方法(先调用valueOf
如果返回值不是Undefined/Boolean/Number/String类型的,就继续调用toString
),返回函数对象本身或函数的实现依赖字符串(implementation-dependent string)源码。
但是由于这里将valueOf
重写为返回参数的和,但是返回值又必须是Function类型的,所以就得到一个Function类型的数字。
不过,如果利用加号操作符可以将其转换为Number类型:+sum(1)(2)(3)
可以改进以下,得到正确的数值
// sum(1)(2)返回值是Function类型的3,sum(1)(2)()返回值是一个Number类型的3
function sum(n) {
function fn (x) {
return x == null ? n : (n+=x, fn);
};
fn.valueOf = function() {
return n;
};
return fn;
}
// type Function
sum(1); // 1
sum(1)(2); // 3
sum(1)(2)(3); // 6
// type Number
sum(1)(2)(); // 3
这里由于对最后参数为空的情况做了分支处理,所以sum(1)(2)()
得到的才是真正的数值
再改进以实现多态化
// 返回值是Function类型的
var _slice = Array.prototype.slice;
function sum() {
var allArgs = _slice.call(arguments);
function fn() {
var args = _slice.call(arguments);
allArgs = allArgs.concat(args);
return fn;
}
fn.valueOf = function() {
return allArgs.reduce(function(acc, val) {
return acc + val;
});
}
return fn;
}
// type Function
sum(1)(2); // 3
sum(1)()(2); // 3
sum()(1)(2); // 3
sum(1)(2, 3)(4)(); // 10
如前面所示,可以通过加号操作符将结果转换为Number类型:+sum(1)()(2)
由于JavaScript是单线程运行的,定时器仅仅是将待执行代码推入一个计划队列,并不能确保某个确定时间点后一定执行。
利用重复定时,可以实现自定义动画
<head>
<meta charset="UTF-8">
<title>定时器动画</title>
<style type="text/css">
* {margin: 0;padding: 0;}
body {height: 100%;}
#box {width:50px;height:50px;background: orange;}
</style>
</head>
<body>
<div id="box"></div>
<script type="text/javascript">
function slide(el, pos, delay) {
var style = el.style,
step = 10;
style.position = 'absolute';
setTimeout(function() {
var left = parseFloat(style.left || 0);
if (pos - left > step) {
style.left = left + step + 'px';
setTimeout(arguments.callee, delay);
} else {
style.left = pos + 'px';
style = null;
}
}, delay);
}
slide(document.getElementById('box'), 200, 100);
</script>
</body>
除了自定义动画,函数间断执行、节流控制等需要定时控制的的代码都需要用到定时器
利用setTimeout
,可以将执行逻辑安全的推入执行队列中,而不会出现乱序或跳动,保证代码在某段时间内的独占运行
function doSomething(){
// your codes
}
setTimeout(doSomething, 0); // 注意超时间隔是0
在大量处理的循环或过深嵌套的函数调用的时候,一次性处理完对所有操作,会阻塞浏览器处理其他事务的线程,比如:用户交互操作。所以一般需要分块、定时进行。
对大量处理的循环,可以进行数组分块处理(Array chunking)。
但同时要注意的是数据分块的处理结果需要异步操作。
function chunk(arr, proc, ctx, fn) {
var DELAY = 100;
var result = [];
setTimeout(function() {
var itm = arr.shift();
result.push(proc.call(ctx, itm));
if (arr.length > 0) {
setTimeout(arguments.callee, DELAY);
} else {
fn(result); // 执行异步回调
result = null;
}
}, 0);
}
function x2(n) {
console.log('get data: ', n);;
return n * 2;
}
chunk([1,3,5,7,9], x2, null, function(arr) {
console.log(arr);
});
当然除了利用定时器分割处理,有些大的数据循环本身可以优化逻辑
简单的优化循环
function loop(arr, proc) {
var i = arr.length - 1;
if (i < 0) return; // 配合后测试循环
do {
proc(arr[i]);
} while (--i >= 0); // 后测试循环,减值迭代,精简终止条件
}
Duff技术优化循环
function duffLoop(arr, proc) {
var len = arr.length,
// 分割成8个一组,不够8个的是最后一组
repeats = Math.ceil(len / 8),
startAt = len % 8,
i = 0;
do { // 从最后一组开始
switch (startAt) { //
case 0: proc(arr[i++]);
case 7: proc(arr[i++]);
case 6: proc(arr[i++]);
case 5: proc(arr[i++]);
case 4: proc(arr[i++]);
case 3: proc(arr[i++]);
case 2: proc(arr[i++]);
case 1: proc(arr[i++]);
}
startAt = 0;
} while (--repeats > 0);
}
进阶版Duff
function duffLoopAdv(arr, proc) {
var len = arr.length,
// 分割成8个一组,不够8个的一组先行运算
repeats = Math.floor(len / 8),
leftNum = len % 8,
i = 0;
if (leftNum > 0) {
do {
proc(arr[i++]);
} while (--leftNum > 0);
}
if (repeats <= 0) return;
do {
proc(arr[i++]);
proc(arr[i++]);
proc(arr[i++]);
proc(arr[i++]);
proc(arr[i++]);
proc(arr[i++]);
proc(arr[i++]);
proc(arr[i++]);
} while (--repeats > 0);
}
结合定时器和Duff
/**
* 控制数组分块循环处理
* @param {array} arr 待处理的数组
* @param {function} proc 处理数组项的函数
* @param {object} ctx proc的调用对象
* @param {function} fn 执行完成后的异步回调
* @param {number} delay 分割处理延时
*/
function chunkAdv(arr, proc, ctx, fn, delay) {
var len = arr.length,
repeats = Math.floor(len/8),
leftNum = len%8,
i = 0,
result = [];
if (leftNum > 0) {
do {
result.push(proc.call(ctx, arr[i++]));
}while(--leftNum > 0);
}
if (repeats <= 0) {
fn(result);
return;
}
setTimeout(function() {
result.push(proc.call(ctx, arr[i++]));
result.push(proc.call(ctx, arr[i++]));
result.push(proc.call(ctx, arr[i++]));
result.push(proc.call(ctx, arr[i++]));
result.push(proc.call(ctx, arr[i++]));
result.push(proc.call(ctx, arr[i++]));
result.push(proc.call(ctx, arr[i++]));
result.push(proc.call(ctx, arr[i++]));
if (--repeats > 0) {
setTimeout(arguments.callee, delay);
} else {
fn(result);
result = null;
}
}, delay);
}
function x2(n) {
console.log('get data: ', n);;
return n * 2;
}
chunkAdv([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22], x2, null, function(arr) {
console.log(arr);
}, 500);
在频繁触发事件中,抑制一定的处理逻辑,可以有效优化页面,避免崩溃。
常见于UI事件的优化处理。
-
debounce
用于控制在连续调用完成后再过一定时间的函数执行,如同:压住的弹簧,松开以后过一小会儿才能弹起来function debounce(fn, wait) { var timeout = null; return function() { var args = arguments, that = this; // 再次调用时重置定时器 clearTimeout(timeout); timeout = setTimeout(function() { fn.apply(that, args); }, wait); }; }
-
throttle
用于控制在连续调用的一定时间段内函数执行的密度,如同:控制水龙头的出水量,拧到足够小,水会间隔一段时间滴出来,而不是连续流出function throttle(fn, wait) { var prev = 0; return function() { var args = arguments, that = this, now = +new Date(); if (!prev) prev = now; // 首次调用,不执行 // 且最末尾调用,也可能不执行 if (now - prev >= wait) { fn.apply(that, args); prev = 0; } } }
不过由于以上两个实现比较简单,控制程度不高,没有把复杂情况和多用性解决。连续调用触发时,往往会屏蔽掉首次函数执行,也就是后文
underscore.js
中提到的开始边界的执行。 -
underscore.js
对上述两个节流函数有更好的实现_.now = Date.now || function() { return new Date().getTime(); }; /** * 执行空闲控制 返回的匿名函数被连续调用时,空闲时间不小于wait毫秒,func才会执行 * @param {function} func 调用空闲时执行的函数 * @param {number} wait 调用空闲的时间间隔 * @param {boolean} immediate 如需设置在开始边界执行func,则设置为true * 否则默认为末尾边界执行 * @return {function} 返回待被调用的函数,控制传入的func的执行 */ _.debounce = function(func, wait, immediate) { var timeout, args, context, timestamp, result; var later = function() { // 定时器生效时,求得前后时间差 var last = _.now() - timestamp; // 按理说,定时器生效时,时间差last应该是大于或等于超时时间wait的 // 但是考虑到在代码运行时系统时间有可能调整,等等特殊情况 if (last < wait && last >= 0) { // 重启定时器,设置新超时时间为 // 旧超时时间wait与旧定时器生效时前后时间差last的差值 timeout = setTimeout(later, wait - last); } else { // 定时器准确生效,重置timeout timeout = null; // 如果没有开启开始边界执行 // 那么在定时器生效时调用func if (!immediate) { result = func.apply(context, args); if (!timeout) context = args = null; } } }; return function() { context = this; args = arguments; // 参考起始时间 timestamp = _.now(); var callNow = immediate && !timeout; // 当首次调用时,开启定时器 if (!timeout) timeout = setTimeout(later, wait); // 若设置了立即调用immediate,当首次调用时,立即执行func if (callNow) { result = func.apply(context, args); // func执行完成后,释放参数 context = args = null; } // 如若不是首次调用,在定时器生效前,直接返回,而不再设置定时器 return result; }; }; /** * 执行频率控制 返回的匿名函数被连续调用时,func的执行频率控制在1次/wait毫秒 * @param {function} func 需要控制执行频率的函数 * @param {number} wait 控制执行的时间间隔 * @param {object} options 若想设置开始边界不执行,则单独传入{leading: false} * 若想设置末尾边界不执行,则单独传入{trailing: false} * 若想双边界都执行,则无需传入此参数 * @return {function} 返回待被调用的函数,控制传入的func的执行 */ _.throttle = function(func, wait, options) { var context, args, result; var timeout = null; // 参考起始时间 var previous = 0; if (!options) options = {}; var later = function() { // 若设置了开始边界不执行,则定时器生效时,重置起始时间,与#1契合 previous = options.leading === false ? 0 : _.now(); // 清除定时器,优化性能 timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; return function() { var now = _.now(); // #1 首次执行时,若设置了开始边界不执行,则将起始时间设置为当前时间 if (!previous && options.leading === false) previous = now; // 达到要求时间差wait的剩余差值 var remaining = wait - (now - previous); context = this; args = arguments; // 若再次调用时已经满足要求时间差wait // 或者在代码运行时系统时间变更导致now - previous小于0 // 则执行func if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); // 清除定时器,优化性能 timeout = null; } // 保留当前时间作为下一次调用的参考起始时间 previous = now; result = func.apply(context, args); if (!timeout) context = args = null; // 若定时器未设定,且未设置末尾边界不执行,则开启定时器 } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } // 若设置了开始边界不执行,又设置了末尾别介不执行 // 则保留参考起始时间,直接跳过,等待下次执行 return result; }; };
-
较新浏览器原生提供的requestanimationframe API可以帮助实现节流控制,而且比
setInterval
和setTimeout
这两个定时器控制更加精准<head> <meta charset="UTF-8"> <title>rAF api</title> <style type="text/css"> #box {width: 50px;height: 50px;background: orange;} </style> </head> <body> <div id="box"></div> <script type="text/javascript"> var el = document.getElementById('box'), style = el.style, start = null; style.position = 'absolute'; function update(timestamp) { // 首次调用时,初始化动画参考起始时间 if (!start) start = timestamp; // 当前时间与动画起始时间的差值 var diff = timestamp - start; // 每次移动时,每10ms向右移动1px // 最大移动右移为200px style.left = Math.min(diff / 10, 200) + 'px'; // 时间差值小于2000ms,也就是右移未达到200px时 // 持续动画效果 if (diff < 2000) { requestAnimationFrame(update); } } // 开始结束时间以及延迟间隔时间全由rAF自动决定 requestAnimationFrame(update); </script> </body>
- JavaScript高级程序设计(第三版) P612 P615 P669
- Why is setTimeout(fn, 0) sometimes useful?
- JS魔法堂:函数节流(throttle)与函数去抖(debounce)
- underscore.js
- Debounce & Throtte JavaScript demo in different implementations
- 浅谈 Underscore.js 中 _.throttle 和 _.debounce 的差异
- JavaScript 节流函数 throttle 详解
- 浅谈javascript的函数节流
- jQuery throttle / debounce: Sometimes, less is more!
- Debouncing and Throttling Explained Through Examples
- requestAnimationFrame API