11

前言

之前做简历用到了impress.js,就像网页版的preiz,简直酷炫!贴上我的简历地址:可是没想到昨天师兄内推我说需要看懂impress.js源码,这样才能体现你学习钻研的精神。orz。。真是挖个坑坑把自己埋了==。

之前做的时候只知道impress用transition的data-x,data-y,data-z进行3D移动。但是昨晚硬着头皮把impress源码读完之后,发现收获还是挺多的。废话就不说了。我们开始剖析impress.js之旅

一. impress.js整体的设计思想是什么?

这里和大家分享一个我个人分析问题的小技巧。(我是前端菜鸟,真正学习时间也不到3个月时间,有说错的地方还请大家多多指正) 这个技巧就是用浏览器自带的审查元素功能。我们打开impress官网的demo.我们通过审查元素,发现每次变化的过程中

图片描述

1.发现一个ppt从左滑动到右边 对应的translate3d(0px,1500px,0px)变化到translate3d(-1000px,1500px,0px)
说明整个ppt的变化是通过translate3d()这个css3属性完成的。

2.我们打开index.html页面源码,发现div上只有如下的代码

<div id="bored" class="step slide" data-x="-1000" data-y="-1500" style="background-color:#ddd;">
.....
</div>

说明我们查看最终效果的div style是js动态添加的。

3.style上有哪些属性呢?

定位:position: absolute;top: 50%; left: 50%;
变化圆心:transform-origin: left top 0px;
移动translate:transition: all 0ms ease-in-out 0ms;
-webkit-transition: all 0ms ease-in-out 0ms;
3d变化样式:transform-style: preserve-3d; //子元素保留其3d位置
变化的透视样式:transform: perspective(14797.6878612717px)//可以近大 远小的效果
缩放:scale(0.067578125);

也就是说明这些是impress.js能实线prezi绚丽ppt效果的核心css,也都是css3新增的属性,推荐大家在慕课网上温习一遍 十天精通CSS3

4.我们在index.html页面中可以看到有data-xdata-ydata-z等属性。而我们一般做impress的时候就是只改变这些参数来达到变换的目的,在上文中我们通过浏览器的调试已经发现了这些参数和最终加载在div上的style样式是有关系的。
data-x对应为translateX;data-y对应translateY;data-z对应translateZ

5.我们可以很”肤浅“得出结论:impress的水平移动是改变了translateX坐标,垂直移动是改变translateY坐标,而突然变小又变大的绚丽效果是改变translateZ的坐标。而这些转化样式,事件监听是通过js来实现的。

二. impress.js具体的技术实现?

1.源码阅读从data-* 属性入手

这个是html5新增api。目的是可以用户自定义数据,定义好的数据又是怎样被拿出来的呢,通过dataset()的方法。我们来看一段源码(line307)

var data = el.dataset, //el是通过getElememtById()获得的元素
            step = {   //定义了一个step对象。里面有4个属性,分别是咱们上文分析过的impress变化相关的css样式。
                translate: {
                    x: toNumber(data.x),
                    y: toNumber(data.y),
                    z: toNumber(data.z)
                },//toNumber()是一个函数。将参数转换成数字,如果无法转换返回默认值
                rotate: {
                    x: toNumber(data.rotateX),
                    y: toNumber(data.rotateY),
                    z: toNumber(data.rotateZ || data.rotate)
                },
                scale: toNumber(data.scale, 1),
                el: el
            };

大家可以在浏览器的console处调试这段代码,你会发现 元素的dataset 得到的是一个数组,我们便可以依次取出x,y,z值。这就是为什么我们可以通过写data-x最终能够影响translateX,最终能够得到水平方向上移动的效果

2.源码的整体代码架构

看到第一个data属性案例,大家肯定觉得源码这么简单~肯定开始从github/impress.js上clone下代码,准备自己去解读源码。哈哈哈,如果你和我一样之前没有任何阅读js源码的经验的话,估计你会被虐哭的,因为源码第一行pfx()函数就够你研究半天的。所以我们必须理清一下思路,一个好的程序一定是有它的书写规范和架构。
首先源码line1-line174都在写通用函数。如果你直接研究的话会感觉莫名奇妙
那么我们大致来看一下这些通用函数都是什么功能

pfx()-----它通过检测浏览器给css3属性加上当前浏览器可用的前缀,这样就不用人工手写'Webkit" ,"Moz" 'O' ,'ms' .'Khtml'等浏览器前缀
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。用于放大和缩小

这里必须给impress.js的作者点个赞!文档写的太仔细了,很多时候你看不懂代码,但是看看注释就懂了~
很显然我们在阅读源码之初没必要逐字逐句去分析这些通用函数的语法和作用,因为通用函数就是工具。我们真正应该关心的是impress的主体架构。

从源码的223line起就是impress主函数和5大api

API:  goto(), init(), next(), prev(),initStep()

主函数: var impress = window.impress = function ( rootId ) {......}

我们可以再看index.html,它先引入impress.js,然后调用init()这个api函数

<script src="js/impress.js"></script>
<script>
impress().init();
</script>

那么我们接下来重点来研究这个init()函数

var init = function () {
    if (initialized) { return; }//初始值initialized=false;
    //第一步我们简历viewport来支持手机设备
    var meta = $("meta[name='viewport']") || document.createElement("meta");
                      //$是一个函数,本人觉得就是借鉴了jquery的源码。line104
                      //  var $ = function ( selector, context ) {
                      //context = context || document;
                      //return context.querySelector(selector);
                      //};

    meta.content = "width=device-width, minimum-scale=1, maximum-scale=1, user-scalable=no";
    if (meta.parentNode !== document.head) {//判断meta的parentNode节点是不是<head>
        meta.name = 'viewport';   //如果不是head标签,就js添加一个meta标签
        document.head.appendChild(meta);
    }

                                   //初始化配置root
                                   //  243line  : rootId = rootId || "impress";
                                    //269line:var root = byId( rootId );
        var rootData = root.dataset;//获取到初始化的root数据,即id=“impress”的div标签里的内容
        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 )
        };

        windowScale = computeWindowScale( config );

        // wrap steps with "canvas" element
        arrayify( root.childNodes ).forEach(function ( el ) {
            canvas.appendChild( el );
        });
        root.appendChild(canvas);
    //这里出现了arrayify函数,在69行。'arraify'函数能够把类数组对象转化为真正数组,
    //slice() 方法可从已有的数组中返回选定的元素。   
                     //   var arrayify = function ( a ) {
                    //return [].slice.call( a );
                    // };    

//forEach是javascript的数组循环遍历函数。
// canvas的来源:line270 var canvas = document.createElement("div");
//我们在浏览器中调试发现 root.childNodes是一个数组,是包裹在

里面 的所有div块
//因为我们的html结构是这样的<div id="impress"><div id="step-1"></div><div id="step-2"></div>..... </div>
//然后利用arrayify函数把 root.childNodes转化为小数组。再利用forEach()函数把数组遍历一遍,动态在root节点后面插入div,这个是dom操作
//还是没法理解的同学,请在浏览器中一行一行的代码敲入,观察效果 ==。js太需要一个可以断点调试的ide了!!!

document.documentElement.style.height = "100%";

        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);

        body.classList.remove("impress-disabled");
        body.classList.add("impress-enabled");

        // get and init steps
        steps = $$(".step", root);
                 // $$函数如下
                /*  var $$ = function ( selector, context ) {
                            context = context || document;
                            return arrayify( context.querySelectorAll(selector) );
                        };*/
        steps.forEach( initStep );
        //找到每一个class为”step“的元素,返回root(id=“impress”)的数组
        //forEach遍历每一个数组,给每个div用initstep()函数初始化。
        //即我们一开始分析的那个函数。主要是把data-*自定义的数据获得,附上transtion样式。
        // set a default initial state of the canvas
        currentState = {
            translate: { x: 0, y: 0, z: 0 },
            rotate:    { x: 0, y: 0, z: 0 },
            scale:     1
        };
     //当前的状态。位移为0,旋转为0,缩放为1.   
        initialized = true;
        //初始化为true,即完成初始化
        triggerEvent(root, "impress:init", { api: roots[ "impress-root-" + rootId ] });
    };

//我们遇了triggerEvent()函数,这个是自定义事件监听函数,源码如下

       /*var triggerEvent = function (el, eventName, detail) {
                var event = document.createEvent("CustomEvent");
                event.initCustomEvent(eventName, true, true, detail);
                el.dispatchEvent(event);
            };*/

//document.createEvent("CustomEvent");是自定义事件函数
// 然后初始化事件对象event.initCustomEvent(eventName, true, true, detail);
//其中,第一个参数为要处理的事件名
//第二个参数为表明事件是否冒泡
//第三个参数为表明是否可以取消事件的默认行为
//第四个参数为细节参数
//(参考https://developer.mozilla.org/en-US/docs/Web/API/Document/createEvent
//通过dispatchEvent()方法来将事件应用到特定的dom节点上,以便其支持该事件。这个dispatchEvent()事件,支持一个参数,就是你创建的event对象。

总结:初始化过程分为两个阶段,第一个阶段是运行init()函数,第二个阶段是运行绑定到impress:init上的函数。这两个阶段之间的连接非常简单,就是在init()函数的结尾触发impress:init事件,这样绑定上去的函数就会全部触发了。而这个事件是用户自定义的dom3事件

3.事件对象绑定与监听
init()函数搞清楚了,下面我们分析第二阶段:运行绑定到impress:init事件上的函数。我们 来看看impress:init事件绑定了什么函数:

  root.addEventListener("impress:init", function(){
            // STEP CLASSES
            steps.forEach(function (step) {
                step.classList.add("future");
            });
   //作者全部用的都是原生js,真是给大神跪了.         
            root.addEventListener("impress:stepenter", function (event) {
                event.target.classList.remove("past");
 //利用html5 classList属性对class类增删改查了,再也不需要jquery的addclass()等二次封装的函数了.
                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);

init是初始化事件,stepenter是进入下一步事件,stepleave是离开上一步事件。具体的函数源码如下

  var onStepEnter = function (step) {
            if (lastEntered !== step) {
                triggerEvent(step, "impress:stepenter");
                lastEntered = step;
            }
        };
  var onStepLeave = function (step) {
            if (lastEntered === step) {
                triggerEvent(step, "impress:stepleave");
                lastEntered = null;
            }
        };

一个step就是一个ppt,你按一次键盘上的left键或者right键就会切换一次step。它也把键盘事件绑定了,源码如下

document.addEventListener("keyup", function ( event ) {...}
document.addEventListener("keydown", function ( event ) {...}
document.addEventListener("click", function ( event ) {...}
window.addEventListener("resize", throttle(function () {...}
document.addEventListener("touchstart", function ( event ) {...}

分析到这里其实也差不多能够搞懂源码了,只是有点思维混乱,毕竟初次读源码,光找各种通用函数都块找哭了.
我们把这一节介绍的init函数和自定义事件的源码函数理一理,便于大家分析

impress 主函数,构造impress对象,这是一个全局对象
onStepEnter 用于触发impress:stepenter事件
onStepLeave 用于触发impress:stepleave事件
initStep 初始化给定step init 主初始化函数
getStep 获取指定step goto 切换到指定step
prev 切换到上一个step next 切换到下一个step

三. impress.js源码分析的总结

我会把impress.js源码逐字解读放在github上,稍后更新,就不在这里啰嗦了.我是前端菜鸟,希望大家一起来分析讨论.共享代码和思想.
关于总结,与其说是总结,不如说是我的一点心得体会吧.

我们也许用原生js做过单独的全屏滚动,
我们也许重写过鼠标键盘事件,
我们也许也做过自定义事件的绑定.
我们也许用过data-*的自定义数据
我们也许用过css3 transform和translate3d 做过动画
我们也许....

有很多技术我们单独实现都很简单,但是把他们综合在一起就发现好难,如何保证命名空间不污染,变量作用域,如何写出兼容性的js和css代码,如何处理好各种代码细节,这都是我们需要反思的地方.impress.js是我第一次阅读的js源码,今后我会把更多发现的问题写在这里,文章会持续更新,和大家一起讨论进步学习.


pomelo
773 声望25 粉丝

一只程序猿的自我修养