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

作者: 梁杰_numbbbbb | 来源:发表于2014-05-27 22:35 被阅读2139次

    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,夹在两个事件中间的就是css的动画效果。也就是说,先触发stepleave事件,然后运行css动画,然后触发stepenter。这两个事件的作用主要就是设定一些标志位和变量,比如设置当前活跃step。

    流程分析

    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上,移动应用在canvas上
        // 大家还记得元素的结构吗?root下面是canvas,canvas下面是所有step
        // 所以缩放root的时候其实就是缩放canvas
        // 至于为什么分开可以更逼真,请看最后一节的代码详解
    
        // 这里是把缩放应用到root上
        css(root, {
            transform: perspective( config.perspective / targetScale ) + scale( targetScale ),
            transitionDuration: duration + "ms",
            transitionDelay: (zoomin ? delay : 0) + "ms"
        });
        
        // 这里就是把移动应用到canvas上
        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,感兴趣的朋友可以看看:
    代码详解

    相关文章

      网友评论

      • 0db4d95fbd61:@GitHub不完全装B指南 十分感谢!我觉得讲得挺具有操作性。我会继续努力。共勉!
      • 梁杰_numbbbbb:@luobo_tang 这个还好,去掉注释空行什么的估计几百行吧,比较大的库看起来就很痛苦了。。
      • 梁杰_numbbbbb:@青争
        首先要给你个赞,我刚入门的时候是绝对不会看这样的文章的。。。

        进一步提升的话,我说一个大致的路线,你可以参考一下:

        1、首先要搞清楚JS。我大概明白你现在所处的阶段,刚开始时候我是从w3school上面学的JS,那个教程快的话一天也就看完了。不过这只是个开头,JS是非常特别(或者说坑很多)的一门语言,一定要找一本经典教程去学一遍,有一些太深奥的东西看不懂也没关系,先把语言搞懂。

        2、学会使用现有的插件。GitHub上面有非常非常多JS相关的插件,你可以自己做几个项目来练手,保证对于常用插件比如jQuery很熟悉,给你一个新库,照着文档可以很快上手。

        3、其实大部分人都处在第2阶段,我本人也不例外。不过我现在也在努力向下一个阶段发展,所以有一些方法可以分享出来。其实说白了就三个字——读源码。当你可以很快上手一个库之后,自己就会感觉到瓶颈。说强吧也不强,说不强吧很多库都会用。这时候就要去看源码,学习那些优秀库是怎么写出来的。这一步和第一步是对照的,很多东西你看教程是看不懂的,比如闭包、原型链这样的东西,怎么看都是一知半解。看代码的过程中其实就是加深理解的过程,你真正看到别人是如何使用这些东西,再结合书上的讲解,就比较容易搞懂。注意我说的是“比较容易”,实际上看代码是很枯燥的一件事。。。这个展开来讲可以写一篇文章,就不多说了。反正读代码的关键就是一定要坚持!一遍读不懂就读两遍,两遍不行就三遍,直到读懂为止。

        总体来说就是这三步吧,我们可以互相交流互相学习~
      • 0db4d95fbd61:感谢!不得不说我是十分费劲得读完的,很多还是一知半解。我JS刚入门,但是了解的都是与网页效果有关的浅显知识,读起这个来还是有些难度。不好意思请问您,进一步提升JS水平,有什么门道吗?
      • NARUTO_86:能认认真真把源码读完,了不起!
      • 秋纫:太赞!最近在做这个,可我不懂JS。

      本文标题:开胃小菜——impress.js代码详解

      本文链接:https://www.haomeiwen.com/subject/moedtttx.html