Advanced Tricks on JavaScript
在多个全局作用域的情况下,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;
}
由于构造函数会操作this
,必须要保证构造函数的正确使用,否则会污染全局变量window
的命名空间
function Car (brand) {
// 确保得到的是一个实例而不是window的属性
if (this instanceof Person) {
this.brand = brand;
} else {
return new Car(brand);
}
}
惰性载入是一种提升加载和运算性能的技术。
前端主要存在两种形式的惰性载入:
函数惰性载入常用于浏览器兼容性判断的逻辑分支中替代原有函数,避免反复判断带来的性能损耗。
有两种实现方式,区分在于逻辑分支替换原有函数的时机:
当第一次调用该函数时进行替换,然后执行
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一大特性第一类函数(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>