开胃小菜——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()函数都干了什么:
- var init = function () {
- if (initialized) { return; }
- // 首先设定viewport
- var meta = $("meta[name='viewport']") || document.createElement("meta");
- meta.content = "width=device-width, minimum-scale=1, maximum-scale=1, user-scalable=no";
- if (meta.parentNode !== document.head) {
- meta.name = 'viewport';
- document.head.appendChild(meta);
- }
- // 初始化config对象
- var rootData = root.dataset;
- config = {
- width: toNumber( rootData.width, defaults.width ),
- height: toNumber( rootData.height, defaults.height ),
- maxScale: toNumber( rootData.maxScale, defaults.maxScale ),
- minScale: toNumber( rootData.minScale, defaults.minScale ),
- perspective: toNumber( rootData.perspective, defaults.perspective ),
- transitionDuration: toNumber( rootData.transitionDuration, defaults.transitionDuration )
- };
- // 计算当前scale
- windowScale = computeWindowScale( config );
- // 将所有step放到canvas中,再将canvas放到root中。
- // 注意这里的canvas和css3中的canvas没关系,这里的canvas只是一个div
- arrayify( root.childNodes ).forEach(function ( el ) {
- canvas.appendChild( el );
- });
- root.appendChild(canvas);
- // 设置html元素的初始高度
- document.documentElement.style.height = "100%";
- // 设置body元素的初始属性
- css(body, {
- height: "100%",
- overflow: "hidden"
- });
- // 设置根元素的初始属性
- var rootStyles = {
- position: "absolute",
- transformOrigin: "top left",
- transition: "all 0s ease-in-out",
- transformStyle: "preserve-3d"
- };
- css(root, rootStyles);
- css(root, {
- top: "50%",
- left: "50%",
- transform: perspective( config.perspective/windowScale ) + scale( windowScale )
- });
- css(canvas, rootStyles);
- // 不能确定impress-disabled类是否存在,所以先remove一下
- body.classList.remove("impress-disabled");
- body.classList.add("impress-enabled");
- // 获取所有step并初始化他们
- steps = $$(".step", root);
- steps.forEach( initStep );
- // 设置canvas的初始状态
- currentState = {
- translate: { x: 0, y: 0, z: 0 },
- rotate: { x: 0, y: 0, z: 0 },
- scale: 1
- };
- initialized = true;
- // 触发init事件
- triggerEvent(root, "impress:init", { api: roots[ "impress-root-" + rootId ] });
- };
init()函数搞清楚了,下面我们分析第二阶段:运行绑定到impress:init
事件上的函数。 来看看impress:init
事件上面都绑定了什么函数:
- root.addEventListener("impress:init", function(){
- // 改变step当前状态
- steps.forEach(function (step) {
- step.classList.add("future");
- });
- root.addEventListener("impress:stepenter", function (event) {
- event.target.classList.remove("past");
- event.target.classList.remove("future");
- event.target.classList.add("present");
- }, false);
- root.addEventListener("impress:stepleave", function (event) {
- event.target.classList.remove("present");
- event.target.classList.add("past");
- }, false);
- }, false);
- // 处理hash相关操作
- root.addEventListener("impress:init", function(){
- var lastHash = "";
- root.addEventListener("impress:stepenter", function (event) {
- window.location.hash = lastHash = "#/" + event.target.id;
- }, false);
- window.addEventListener("hashchange", function () {
- if (window.location.hash !== lastHash) {
- goto( getElementFromHash() );
- }
- }, false);
- goto(getElementFromHash() || steps[0], 0);
- }, false);
- // 绑定键盘事件、触摸事件和点击事件
- document.addEventListener("impress:init", function (event) {
- var api = event.detail.api;
- // 绑定键盘事件
- document.addEventListener("keydown", function ( event ) {
- if ( event.keyCode === 9 || ( event.keyCode >= 32 && event.keyCode <= 34 ) || (event.keyCode >= 37 && event.keyCode <= 40) ) {
- event.preventDefault();
- }
- }, false);
- document.addEventListener("keyup", function ( event ) {
- if ( event.keyCode === 9 || ( event.keyCode >= 32 && event.keyCode <= 34 ) || (event.keyCode >= 37 && event.keyCode <= 40) ) {
- switch( event.keyCode ) {
- case 33: // pg up
- case 37: // left
- case 38: // up
- api.prev();
- break;
- case 9: // tab
- case 32: // space
- case 34: // pg down
- case 39: // right
- case 40: // down
- api.next();
- break;
- }
- event.preventDefault();
- }
- }, false);
- // 绑定链接点击事件
- document.addEventListener("click", function ( event ) {
- var target = event.target;
- while ( (target.tagName !== "A") &&
- (target !== document.documentElement) ) {
- target = target.parentNode;
- }
- if ( target.tagName === "A" ) {
- var href = target.getAttribute("href");
- // if it's a link to presentation step, target this step
- if ( href && href[0] === '#' ) {
- target = document.getElementById( href.slice(1) );
- }
- }
- if ( api.goto(target) ) {
- event.stopImmediatePropagation();
- event.preventDefault();
- }
- }, false);
- // 绑定对象点击事件
- document.addEventListener("click", function ( event ) {
- var target = event.target;
- while ( !(target.classList.contains("step") && !target.classList.contains("active")) &&
- (target !== document.documentElement) ) {
- target = target.parentNode;
- }
- if ( api.goto(target) ) {
- event.preventDefault();
- }
- }, false);
- // 绑定触摸事件
- document.addEventListener("touchstart", function ( event ) {
- if (event.touches.length === 1) {
- var x = event.touches[0].clientX,
- width = window.innerWidth * 0.3,
- result = null;
- if ( x < width ) {
- result = api.prev();
- } else if ( x > window.innerWidth - width ) {
- result = api.next();
- }
- if (result) {
- event.preventDefault();
- }
- }
- }, false);
- // 绑定页面resize事件
- window.addEventListener("resize", throttle(function () {
- api.goto( document.querySelector(".step.active"), 500 );
- }, 250), false);
- }, false);
我们来梳理一遍,初始化过程做了什么事:
- init()函数中主要初始化画布、step以及impress对象内部用到的一些状态
- 绑定到
impress:init
事件上的函数把其他需要绑定的事件都进行了绑定,让impress可以正常工作
接下来我们分析step切换过程,来看看goto函数都干了什么
什么?你有点累了?加把劲,一定要看完goto
- var goto = function ( el, duration ) {
- if ( !initialized || !(el = getStep(el)) ) {
- //如果没初始化或者el不是一个step就返回
- return false;
- }
- // 为了避免载入时候浏览器滚动,手动滚动到0,0
- window.scrollTo(0, 0);
- var step = stepsData["impress-" + el.id];
- // 清理当前活跃step上面的标记
- if ( activeStep ) {
- activeStep.classList.remove("active");
- body.classList.remove("impress-on-" + activeStep.id);
- }
- // 给el加活跃标记
- el.classList.add("active");
- body.classList.add("impress-on-" + el.id);
- // 计算canvas相对于当前step的变换参数
- var target = {
- rotate: {
- x: -step.rotate.x,
- y: -step.rotate.y,
- z: -step.rotate.z
- },
- translate: {
- x: -step.translate.x,
- y: -step.translate.y,
- z: -step.translate.z
- },
- scale: 1 / step.scale
- };
- // 处理缩放
- var zoomin = target.scale >= currentState.scale;
- duration = toNumber(duration, config.transitionDuration);
- var delay = (duration / 2);
- // 如果el就是当前活跃step,重新计算scale
- if (el === activeStep) {
- windowScale = computeWindowScale(config);
- }
- var targetScale = target.scale * windowScale;
- // 触发stepleave事件
- if (activeStep && activeStep !== el) {
- onStepLeave(activeStep);
- }
- css(root, {
- transform: perspective( config.perspective / targetScale ) + scale( targetScale ),
- transitionDuration: duration + "ms",
- transitionDelay: (zoomin ? delay : 0) + "ms"
- });
- css(canvas, {
- transform: rotate(target.rotate, true) + translate(target.translate),
- transitionDuration: duration + "ms",
- transitionDelay: (zoomin ? 0 : delay) + "ms"
- });
- if ( currentState.scale === target.scale ||
- (currentState.rotate.x === target.rotate.x && currentState.rotate.y === target.rotate.y &&
- currentState.rotate.z === target.rotate.z && currentState.translate.x === target.translate.x &&
- currentState.translate.y === target.translate.y && currentState.translate.z === target.translate.z) ) {
- delay = 0;
- }
- // 存储当前状态
- currentState = target;
- activeStep = el;
- // 触发stepenter事件
- window.clearTimeout(stepEnterTimeout);
- stepEnterTimeout = window.setTimeout(function() {
- onStepEnter(activeStep);
- }, duration + delay);
- return el;
- };
好了,下面简单看看prev和next函数:
- var prev = function () {
- var prev = steps.indexOf( activeStep ) - 1;
- prev = prev >= 0 ? steps[ prev ] : steps[ steps.length-1 ];
- return goto(prev);
- };
- var next = function () {
- var next = steps.indexOf( activeStep ) + 1;
- next = next < steps.length ? steps[ next ] : steps[ 0 ];
- return goto(next);
- };
很简单吧?他们都是基于goto写的,所以核心的goto搞懂了也就明白prev和next了。
消化代码
非常感谢你能看到这里——或者是直接跳到这里——这篇文章大概是我写过的最长的文章了,如果你觉得不错的话请点个“推荐”吧!
本来想都写到这里的,但是这样的话会让本来就很长的文章变得更长。。。所以就把代码详解写成了一个Gist,感兴趣的朋友可以看看: 代码详解
开胃小菜——impress.js代码详解的更多相关文章
- 图片滚动(UP)的JS代码详解(offsetTop、scrollTop、offsetHeigh)【转】
源地址 信息技术教材配套的光盘里有一段设置图片滚动的JS代码,与网络上差不多,实现思路:一个设定高度并且隐藏超出它高度的内容的容器demo,里面放demo1和 demo2,demo1是滚动内容,dem ...
- js对象详解(JavaScript对象深度剖析,深度理解js对象)
js对象详解(JavaScript对象深度剖析,深度理解js对象) 这算是酝酿很久的一篇文章了. JavaScript作为一个基于对象(没有类的概念)的语言,从入门到精通到放弃一直会被对象这个问题围绕 ...
- BM算法 Boyer-Moore高质量实现代码详解与算法详解
Boyer-Moore高质量实现代码详解与算法详解 鉴于我见到对算法本身分析非常透彻的文章以及实现的非常精巧的文章,所以就转载了,本文的贡献在于将两者结合起来,方便大家了解代码实现! 算法详解转自:h ...
- ASP.NET MVC 5 学习教程:生成的代码详解
原文 ASP.NET MVC 5 学习教程:生成的代码详解 起飞网 ASP.NET MVC 5 学习教程目录: 添加控制器 添加视图 修改视图和布局页 控制器传递数据给视图 添加模型 创建连接字符串 ...
- Github-karpathy/char-rnn代码详解
Github-karpathy/char-rnn代码详解 zoerywzhou@gmail.com http://www.cnblogs.com/swje/ 作者:Zhouwan 2016-1-10 ...
- 代码详解:TensorFlow Core带你探索深度神经网络“黑匣子”
来源商业新知网,原标题:代码详解:TensorFlow Core带你探索深度神经网络“黑匣子” 想学TensorFlow?先从低阶API开始吧~某种程度而言,它能够帮助我们更好地理解Tensorflo ...
- JAVA类与类之间的全部关系简述+代码详解
本文转自: https://blog.csdn.net/wq6ylg08/article/details/81092056类和类之间关系包括了 is a,has a, use a三种关系(1)is a ...
- Java中String的intern方法,javap&cfr.jar反编译,javap反编译后二进制指令代码详解,Java8常量池的位置
一个例子 public class TestString{ public static void main(String[] args){ String a = "a"; Stri ...
- Kaggle网站流量预测任务第一名解决方案:从模型到代码详解时序预测
Kaggle网站流量预测任务第一名解决方案:从模型到代码详解时序预测 2017年12月13日 17:39:11 机器之心V 阅读数:5931 近日,Artur Suilin 等人发布了 Kaggl ...
随机推荐
- PHP 基础系列(三) 【转】PHP 函数实现原理及性能分析
作者:HDK (百度) 前言 在任何语言中,函数都是最基本的组成单元.对于PHP的函数,它具有哪些特点?函数调用是怎么实现的?php函数的性能如何,有什么使用建议?本文将从原理出发进行分析结合实际的性 ...
- redis-desktop-manager 安装——redis的可视化工具
一.redis-desktop-manager介绍 Redis可视化工具之一,RedisDesktopManager是开源的,托管在github上:https://github.com/uglide/ ...
- Anaconda安装与常用命令及方法(深度学习入门1)
Anaconda是一个软件发行版,它附带了 conda.Python 和 150 多个科学包及其依赖项. 安装Anaconda Anaconda分为Linux.Windows.Mac等版本,去 htt ...
- supervisord的配置
https://blog.csdn.net/xyang81/article/details/51555473 这位大佬写的很详细 你们可以去参考一下
- HashMap和Hashtable的详细区别
1.Hashtable是线程安全,HashMap是非线程安全 HashMap的性能会高于Hashtable,我们平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用 ...
- .net MVC 页面页面跳转后提示消息实现办法
mvc在RedirectToAction之后,会清理掉ViewData中的所有数据,因此通过ViewData给下一个页面传递提示消息不太好,如果是通过参数方式传递,刷新跳转后的页面时,消息还会再次提示 ...
- react系列(零)安装
安装 在最初的阶段,可以使用在线编辑的网站来学习React基本的语法. 从 Hello World 开始,可以在Codepen,或者codesandbox上进行编写. 当然,也可以使用npm或者yar ...
- platform平台总线
一.何为平台总线 (1)相对于usb.pci.i2c等物理总线来说,platform总线是虚拟的.抽象出来的.(2)CPU与外部通信的2种方式:地址总线式连接和专用协议类接口式连接.平台总线,是扩展到 ...
- javascript根据文件字节数返回文件大小
function getFileSize(fileByte) { var fileSizeByte = fileByte; var fileSizeMsg = ""; if(fil ...
- 将图片写入二进制文件,再从二进制文件还原图片(c++)
#include "string" #include "iostream" #include "fstream" using namespa ...