js
研究了一番impress.js:
个人感觉impress.js的代码量(算上注释一共不到1000行)和难度(没有jQuery的各种black magic= =)都非常适合新手学习,所以写一个总结,帮助大家理解源码。
考虑到很多朋友并不喜欢深入细节,下文分为四部分:
函数目录:汇总所有函数及其作用,方便查看
事件分析:了解impress.js的运行基础
流程分析:了解impress.js的运行流程
消化代码:具体到行的代码讲解
前三部分是必看的,最后一部分可以根据个人兴趣选择。由于我看代码一向喜欢抠细节,在我看来细节才是最能提高能力并且最有趣的地方,所以我会把每行代码甚至每个变量每个表达式都讲清楚,让你真正的看懂impress.js。
由于最后一节会写详细解释,所以前几节中出现的代码我不会详细解释,只会说明大概的功能,方便大家理解。对细节感兴趣的朋友可以看最后一节。
函数目录
你可以暂时先跳过这一节或者简单浏览一下,后面看代码的时候可以再来查函数作用。
函数名 函数作用
pfx 给css3属性加上当前浏览器可用的前缀
arrayify 将Array-Like对象转换成Array对象
css 将指定属性应用到指定元素上
toNumber 将参数转换成数字,如果无法转换返回默认值
byId 通过id获取元素
$ 返回满足选择器的第一个元素
$$ 返回满足选择器的所有元素
triggerEvent 在指定元素上触发指定事件
translate 将translate对象转换成css使用的字符串
rotate 将rotate对象转换成css使用的字符串
scale 将scale对象转换成css使用的字符串
perspective 将perspective对象转换成css使用的字符串
getElementFromHash 根据hash来获取元素,hash就是URL中形如#step1的东西
computeWindowScale 根据当前窗口尺寸计算scale因子,用于放大和缩小
empty 什么用都没有的函数,当浏览器不支持impress的时候会用到,一点用都没有
impress 主函数,构造impress对象,这是一个全局对象
onStepEnter 用于触发impress:stepenter事件
onStepLeave 用于触发impress:stepleave事件
initStep 初始化给定step
init 主初始化函数
getStep 获取指定step
goto 切换到指定step
prev 切换到上一个step
next 切换到下一个step
throttle 可以延后运行某个函数
事件分析
先明白一个基本概念——step。 step就是impress.js画布中的基本单位,一个step就是一幕,你按一次键盘上的←键或者→键就会切换一次step。
事件是impress.js运行的基础,共有三个,分别是impress:init, impress:stepenter和impress:stepleave(下文将省略impress前缀)。
init是初始化事件,stepenter是进入下一步事件,stepleave是离开上一步事件。
init事件只在初始化时候触发,且只被触发一次,因为impress.js内部有一个initialized变量,初始化之后这个变量会置True,从而保证只初始化一次。 下一节中我们会详细讲解init事件,这里暂时跳过。
那么stepenter和stepleave有什么用呢? 假设我们现在处在第1步,我们按一下键盘上的→键就会切换到第2步,这背后impress.js实际上连续触发了两个事件:stepleave和stepenter,两者一先一后连贯起来就构成了我们看到的切换效果。
流程分析
impress对象暴露了四个API,分别是goto(), init(), next(), prev()。由于next()和prev()都是基于goto()写的,所以我们下面重点分析goto()和init()。
impress.js的运行流程可以分为两大部分——初始化过程以及step切换过程,正好对应init()和goto()。就像上面说到的。初始化过程只会被运行一次,而切换过程可能被触发很多次。
我们先来分析重中之重——初始化过程
初始化过程分为两个阶段,第一个阶段是运行init()函数,第二个阶段是运行绑定到impress:init上的函数。这两个阶段之间的连接非常简单,就是在init()函数的结尾触发impress:init事件,这样绑定上去的函数就会全部触发了。
来看看init()函数都干了什么:
1 var init = function () {
2 if (initialized) { return; }
3
4 // 首先设定viewport
5 var meta = $("meta[name='viewport']") || document.createElement("meta");
6 meta.content = "width=device-width, minimum-scale=1, maximum-scale=1, user-scalable=no";
7 if (meta.parentNode !== document.head) {
8 meta.name = 'viewport';
9 document.head.appendChild(meta);
10 }
11
12 // 初始化config对象
13 var rootData = root.dataset;
14 config = {
15 width: toNumber( rootData.width, defaults.width ),
16 height: toNumber( rootData.height, defaults.height ),
17 maxScale: toNumber( rootData.maxScale, defaults.maxScale ),
18 minScale: toNumber( rootData.minScale, defaults.minScale ),
19 perspective: toNumber( rootData.perspective, defaults.perspective ),
20 transitionDuration: toNumber( rootData.transitionDuration, defaults.transitionDuration )
21 };
22
23 // 计算当前scale
24 windowScale = computeWindowScale( config );
25
26 // 将所有step放到canvas中,再将canvas放到root中。
27 // 注意这里的canvas和css3中的canvas没关系,这里的canvas只是一个div
28 arrayify( root.childNodes ).forEach(function ( el ) {
29 canvas.appendChild( el );
30 });
31 root.appendChild(canvas);
32
33 // 设置html元素的初始高度
34 document.documentElement.style.height = "100%";
35
36 // 设置body元素的初始属性
37 css(body, {
38 height: "100%",
39 overflow: "hidden"
40 });
41
42 // 设置根元素的初始属性
43 var rootStyles = {
44 position: "absolute",
45 transformOrigin: "top left",
46 transition: "all 0s ease-in-out",
47 transformStyle: "preserve-3d"
48 };
49
50 css(root, rootStyles);
51 css(root, {
52 top: "50%",
53 left: "50%",
54 transform: perspective( config.perspective/windowScale ) + scale( windowScale )
55 });
56 css(canvas, rootStyles);
57
58 // 不能确定impress-disabled类是否存在,所以先remove一下
59 body.classList.remove("impress-disabled");
60 body.classList.add("impress-enabled");
61
62 // 获取所有step并初始化他们
63 steps = $$(".step", root);
64 steps.forEach( initStep );
65
66 // 设置canvas的初始状态
67 currentState = {
68 translate: { x: 0, y: 0, z: 0 },
69 rotate: { x: 0, y: 0, z: 0 },
70 scale: 1
71 };
72
73 initialized = true;
74
75 // 触发init事件
76 triggerEvent(root, "impress:init", { api: roots[ "impress-root-" + rootId ] });
77 };
init()函数搞清楚了,下面我们分析第二阶段:运行绑定到impress:init事件上的函数。 来看看impress:init事件上面都绑定了什么函数:
按 Ctrl+C 复制代码
按 Ctrl+C 复制代码
我们来梳理一遍,初始化过程做了什么事:
init()函数中主要初始化画布、step以及impress对象内部用到的一些状态
绑定到impress:init事件上的函数把其他需要绑定的事件都进行了绑定,让impress可以正常工作
接下来我们分析step切换过程,来看看goto函数都干了什么
什么?你有点累了?加把劲,一定要看完goto
1 var goto = function ( el, duration ) {
2
3 if ( !initialized || !(el = getStep(el)) ) {
4 //如果没初始化或者el不是一个step就返回
5 return false;
6 }
7
8 // 为了避免载入时候浏览器滚动,手动滚动到0,0
9 window.scrollTo(0, 0);
10
11 var step = stepsData["impress-" + el.id];
12
13 // 清理当前活跃step上面的标记
14 if ( activeStep ) {
15 activeStep.classList.remove("active");
16 body.classList.remove("impress-on-" + activeStep.id);
17 }
18 // 给el加活跃标记
19 el.classList.add("active");
20
21 body.classList.add("impress-on-" + el.id);
22
23 // 计算canvas相对于当前step的变换参数
24 var target = {
25 rotate: {
26 x: -step.rotate.x,
27 y: -step.rotate.y,
28 z: -step.rotate.z
29 },
30 translate: {
31 x: -step.translate.x,
32 y: -step.translate.y,
33 z: -step.translate.z
34 },
35 scale: 1 / step.scale
36 };
37
38 // 处理缩放
39 var zoomin = target.scale >= currentState.scale;
40
41 duration = toNumber(duration, config.transitionDuration);
42 var delay = (duration / 2);
43
44 // 如果el就是当前活跃step,重新计算scale
45 if (el === activeStep) {
46 windowScale = computeWindowScale(config);
47 }
48
49 var targetScale = target.scale * windowScale;
50
51 // 触发stepleave事件
52 if (activeStep && activeStep !== el) {
53 onStepLeave(activeStep);
54 }
55
56 css(root, {
57 transform: perspective( config.perspective / targetScale ) + scale( targetScale ),
58 transitionDuration: duration + "ms",
59 transitionDelay: (zoomin ? delay : 0) + "ms"
60 });
61
62 css(canvas, {
63 transform: rotate(target.rotate, true) + translate(target.translate),
64 transitionDuration: duration + "ms",
65 transitionDelay: (zoomin ? 0 : delay) + "ms"
66 });
67
68 if ( currentState.scale === target.scale ||
69 (currentState.rotate.x === target.rotate.x && currentState.rotate.y === target.rotate.y &&
70 currentState.rotate.z === target.rotate.z && currentState.translate.x === target.translate.x &&
71 currentState.translate.y === target.translate.y && currentState.translate.z === target.translate.z) ) {
72 delay = 0;
73 }
74
75 // 存储当前状态
76 currentState = target;
77 activeStep = el;
78
79 // 触发stepenter事件
80 window.clearTimeout(stepEnterTimeout);
81 stepEnterTimeout = window.setTimeout(function() {
82 onStepEnter(activeStep);
83 }, duration + delay);
84
85 return el;
86 };
好了,下面简单看看prev和next函数:
1 var prev = function () {
2 var prev = steps.indexOf( activeStep ) - 1;
3 prev = prev >= 0 ? steps[ prev ] : steps[ steps.length-1 ];
4
5 return goto(prev);
6 };
7
8 var next = function () {
9 var next = steps.indexOf( activeStep ) + 1;
10 next = next < steps.length ? steps[ next ] : steps[ 0 ];
11
12 return goto(next);
13 };
诸如此类的例子还有很多!