开胃小菜——impress.js代码详解

README

友情提醒,下面有大量代码,由于网页上代码显示都是同一个颜色,所以推荐大家复制到自己的代码编辑器中看。

今天闲来无事,研究了一番impress.js的源码。由于之前研究过jQuery,看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:stepenterimpress:stepleave(下文将省略impress前缀)。

init是初始化事件,stepenter是进入下一步事件,stepleave是离开上一步事件。

init事件只在初始化时候触发,且只被触发一次,因为impress.js内部有一个initialized变量,初始化之后这个变量会置True,从而保证只初始化一次。 下一节中我们会详细讲解init事件,这里暂时跳过。

那么stepenterstepleave有什么用呢? 假设我们现在处在第1步,我们按一下键盘上的→键就会切换到第2步,这背后impress.js实际上连续触发了两个事件:stepleavestepenter,两者一先一后连贯起来就构成了我们看到的切换效果。

流程分析

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事件上面都绑定了什么函数:

  1 root.addEventListener("impress:init", function(){
  2     // 改变step当前状态
  3     steps.forEach(function (step) {
  4         step.classList.add("future");
  5     });
  6 
  7     root.addEventListener("impress:stepenter", function (event) {
  8         event.target.classList.remove("past");
  9         event.target.classList.remove("future");
 10         event.target.classList.add("present");
 11     }, false);
 12 
 13     root.addEventListener("impress:stepleave", function (event) {
 14         event.target.classList.remove("present");
 15         event.target.classList.add("past");
 16     }, false);
 17 
 18 }, false);
 19 
 20 // 处理hash相关操作
 21 root.addEventListener("impress:init", function(){
 22 
 23     var lastHash = "";
 24     root.addEventListener("impress:stepenter", function (event) {
 25         window.location.hash = lastHash = "#/" + event.target.id;
 26     }, false);
 27 
 28     window.addEventListener("hashchange", function () {
 29         if (window.location.hash !== lastHash) {
 30             goto( getElementFromHash() );
 31         }
 32     }, false);
 33 
 34     goto(getElementFromHash() || steps[0], 0);
 35 }, false);
 36 
 37 // 绑定键盘事件、触摸事件和点击事件
 38 document.addEventListener("impress:init", function (event) {
 39     var api = event.detail.api;
 40 
 41     // 绑定键盘事件
 42     document.addEventListener("keydown", function ( event ) {
 43         if ( event.keyCode === 9 || ( event.keyCode >= 32 && event.keyCode <= 34 ) || (event.keyCode >= 37 && event.keyCode <= 40) ) {
 44             event.preventDefault();
 45         }
 46     }, false);
 47 
 48     document.addEventListener("keyup", function ( event ) {
 49         if ( event.keyCode === 9 || ( event.keyCode >= 32 && event.keyCode <= 34 ) || (event.keyCode >= 37 && event.keyCode <= 40) ) {
 50             switch( event.keyCode ) {
 51                 case 33: // pg up
 52                 case 37: // left
 53                 case 38: // up
 54                          api.prev();
 55                          break;
 56                 case 9:  // tab
 57                 case 32: // space
 58                 case 34: // pg down
 59                 case 39: // right
 60                 case 40: // down
 61                          api.next();
 62                          break;
 63             }
 64 
 65             event.preventDefault();
 66         }
 67     }, false);
 68 
 69     // 绑定链接点击事件
 70     document.addEventListener("click", function ( event ) {
 71         var target = event.target;
 72         while ( (target.tagName !== "A") &&
 73                 (target !== document.documentElement) ) {
 74             target = target.parentNode;
 75         }
 76 
 77         if ( target.tagName === "A" ) {
 78             var href = target.getAttribute("href");
 79 
 80             // if it's a link to presentation step, target this step
 81             if ( href && href[0] === '#' ) {
 82                 target = document.getElementById( href.slice(1) );
 83             }
 84         }
 85 
 86         if ( api.goto(target) ) {
 87             event.stopImmediatePropagation();
 88             event.preventDefault();
 89         }
 90     }, false);
 91 
 92     // 绑定对象点击事件
 93     document.addEventListener("click", function ( event ) {
 94         var target = event.target;
 95         while ( !(target.classList.contains("step") && !target.classList.contains("active")) &&
 96                 (target !== document.documentElement) ) {
 97             target = target.parentNode;
 98         }
 99 
100         if ( api.goto(target) ) {
101             event.preventDefault();
102         }
103     }, false);
104 
105     // 绑定触摸事件
106     document.addEventListener("touchstart", function ( event ) {
107         if (event.touches.length === 1) {
108             var x = event.touches[0].clientX,
109                 width = window.innerWidth * 0.3,
110                 result = null;
111 
112             if ( x < width ) {
113                 result = api.prev();
114             } else if ( x > window.innerWidth - width ) {
115                 result = api.next();
116             }
117 
118             if (result) {
119                 event.preventDefault();
120             }
121         }
122     }, false);
123 
124     // 绑定页面resize事件
125     window.addEventListener("resize", throttle(function () {
126         api.goto( document.querySelector(".step.active"), 500 );
127     }, 250), false);
128 
129 }, false);

 

我们来梳理一遍,初始化过程做了什么事:

  • 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 };

 

很简单吧?他们都是基于goto写的,所以核心的goto搞懂了也就明白prev和next了。

消化代码

非常感谢你能看到这里——或者是直接跳到这里——这篇文章大概是我写过的最长的文章了,如果你觉得不错的话请点个“推荐”吧!

本来想都写到这里的,但是这样的话会让本来就很长的文章变得更长。。。所以就把代码详解写成了一个Gist,感兴趣的朋友可以看看: 代码详解

posted @ 2014-05-27 23:00  numbbbbb  阅读(1499)  评论(0编辑  收藏  举报