开胃小菜——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: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
事件上面都绑定了什么函数:
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,感兴趣的朋友可以看看: 代码详解