開胃小菜——impress.js代碼詳解

README

友情提醒寓涨,下面有大量代碼澎蛛,由于網(wǎng)頁(yè)上代碼顯示都是同一個(gè)顏色宗兼,所以推薦大家復(fù)制到自己的代碼編輯器中看躏鱼。

今天閑來(lái)無(wú)事,研究了一番impress.js的源碼殷绍。由于之前研究過(guò)jQuery染苛,看impress.js并沒(méi)有遇到太大的阻礙,讀代碼用了一個(gè)小時(shí)主到,寫這篇文章用了近三個(gè)小時(shí)茶行,果然寫文章比讀代碼費(fèi)勁多了。

個(gè)人感覺(jué)impress.js的代碼量(算上注釋一共不到1000行)和難度(沒(méi)有jQuery的各種black magic= =)都非常適合新手學(xué)習(xí)登钥,所以寫一個(gè)總結(jié)畔师,幫助大家理解源碼。

考慮到很多朋友并不喜歡深入細(xì)節(jié)牧牢,下文分為四部分:

  • 函數(shù)目錄:匯總所有函數(shù)及其作用茉唉,方便查看
  • 事件分析:了解impress.js的運(yùn)行基礎(chǔ)
  • 流程分析:了解impress.js的運(yùn)行流程
  • 消化代碼:具體到行的代碼講解

前三部分是必看的固蛾,最后一部分可以根據(jù)個(gè)人興趣選擇。由于我看代碼一向喜歡摳細(xì)節(jié)度陆,在我看來(lái)細(xì)節(jié)才是最能提高能力并且最有趣的地方艾凯,所以我會(huì)把每行代碼甚至每個(gè)變量每個(gè)表達(dá)式都講清楚,讓你真正的看懂impress.js懂傀。

由于最后一節(jié)會(huì)寫詳細(xì)解釋趾诗,所以前幾節(jié)中出現(xiàn)的代碼我不會(huì)詳細(xì)解釋,只會(huì)說(shuō)明大概的功能蹬蚁,方便大家理解恃泪。對(duì)細(xì)節(jié)感興趣的朋友可以看最后一節(jié)。

函數(shù)目錄

你可以暫時(shí)先跳過(guò)這一節(jié)或者簡(jiǎn)單瀏覽一下犀斋,后面看代碼的時(shí)候可以再來(lái)查函數(shù)作用贝乎。

函數(shù)名 函數(shù)作用
pfx 給css3屬性加上當(dāng)前瀏覽器可用的前綴
arrayify 將Array-Like對(duì)象轉(zhuǎn)換成Array對(duì)象
css 將指定屬性應(yīng)用到指定元素上
toNumber 將參數(shù)轉(zhuǎn)換成數(shù)字,如果無(wú)法轉(zhuǎn)換返回默認(rèn)值
byId 通過(guò)id獲取元素
$ 返回滿足選擇器的第一個(gè)元素
$$ 返回滿足選擇器的所有元素
triggerEvent 在指定元素上觸發(fā)指定事件
translate 將translate對(duì)象轉(zhuǎn)換成css使用的字符串
rotate 將rotate對(duì)象轉(zhuǎn)換成css使用的字符串
scale 將scale對(duì)象轉(zhuǎn)換成css使用的字符串
perspective 將perspective對(duì)象轉(zhuǎn)換成css使用的字符串
getElementFromHash 根據(jù)hash來(lái)獲取元素叽粹,hash就是URL中形如#step1的東西
computeWindowScale 根據(jù)當(dāng)前窗口尺寸計(jì)算scale因子览效,用于放大和縮小
empty 什么用都沒(méi)有的函數(shù),當(dāng)瀏覽器不支持impress的時(shí)候會(huì)用到虫几,一點(diǎn)用都沒(méi)有
impress 主函數(shù)锤灿,構(gòu)造impress對(duì)象,這是一個(gè)全局對(duì)象
onStepEnter 用于觸發(fā)impress:stepenter事件
onStepLeave 用于觸發(fā)impress:stepleave事件
initStep 初始化給定step
init 主初始化函數(shù)
getStep 獲取指定step
goto 切換到指定step
prev 切換到上一個(gè)step
next 切換到下一個(gè)step
throttle 可以延后運(yùn)行某個(gè)函數(shù)

事件分析

先明白一個(gè)基本概念——step辆脸。
step就是impress.js畫布中的基本單位但校,一個(gè)step就是一幕,你按一次鍵盤上的←鍵或者→鍵就會(huì)切換一次step啡氢。

事件是impress.js運(yùn)行的基礎(chǔ)状囱,共有三個(gè),分別是impress:init, impress:stepenterimpress:stepleave(下文將省略impress前綴)倘是。

init是初始化事件浪箭,stepenter是進(jìn)入下一步事件,stepleave是離開上一步事件辨绊。

init事件只在初始化時(shí)候觸發(fā)奶栖,且只被觸發(fā)一次,因?yàn)閕mpress.js內(nèi)部有一個(gè)initialized變量门坷,初始化之后這個(gè)變量會(huì)置True宣鄙,從而保證只初始化一次。
下一節(jié)中我們會(huì)詳細(xì)講解init事件默蚌,這里暫時(shí)跳過(guò)冻晤。

那么stepenterstepleave有什么用呢?
假設(shè)我們現(xiàn)在處在第1步绸吸,我們按一下鍵盤上的→鍵就會(huì)切換到第2步鼻弧,這背后impress.js實(shí)際上觸發(fā)了兩個(gè)事件:stepleavestepenter设江,夾在兩個(gè)事件中間的就是css的動(dòng)畫效果。也就是說(shuō)攘轩,先觸發(fā)stepleave事件叉存,然后運(yùn)行css動(dòng)畫,然后觸發(fā)stepenter度帮。這兩個(gè)事件的作用主要就是設(shè)定一些標(biāo)志位和變量歼捏,比如設(shè)置當(dāng)前活躍step。

流程分析

impress對(duì)象暴露了四個(gè)API笨篷,分別是goto(), init(), next(), prev()瞳秽。由于next()prev()都是基于goto()寫的,所以我們下面重點(diǎn)分析goto()init()率翅。

impress.js的運(yùn)行流程可以分為兩大部分——初始化過(guò)程以及step切換過(guò)程练俐,正好對(duì)應(yīng)init()goto()。就像上面說(shuō)到的冕臭。初始化過(guò)程只會(huì)被運(yùn)行一次腺晾,而切換過(guò)程可能被觸發(fā)很多次。

我們先來(lái)分析重中之重——初始化過(guò)程

初始化過(guò)程分為兩個(gè)階段浴韭,第一個(gè)階段是運(yùn)行init()函數(shù),第二個(gè)階段是運(yùn)行綁定到impress:init上的函數(shù)脯宿。這兩個(gè)階段之間的連接非常簡(jiǎn)單念颈,就是在init()函數(shù)的結(jié)尾觸發(fā)impress:init事件,這樣綁定上去的函數(shù)就會(huì)全部觸發(fā)了连霉。

來(lái)看看init()函數(shù)都干了什么:

var init = function () {
    if (initialized) { return; }
    
    // 首先設(shè)定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對(duì)象
    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 )
    };
    
    // 計(jì)算當(dāng)前scale
    windowScale = computeWindowScale( config );
    
    // 將所有step放到canvas中榴芳,再將canvas放到root中。
    // 注意這里的canvas和css3中的canvas沒(méi)關(guān)系跺撼,這里的canvas只是一個(gè)div
    arrayify( root.childNodes ).forEach(function ( el ) {
        canvas.appendChild( el );
    });
    root.appendChild(canvas);
    
    // 設(shè)置html元素的初始高度
    document.documentElement.style.height = "100%";
    
    // 設(shè)置body元素的初始屬性
    css(body, {
        height: "100%",
        overflow: "hidden"
    });
    
    // 設(shè)置根元素的初始屬性
    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 );
    
    // 設(shè)置canvas的初始狀態(tài)
    currentState = {
        translate: { x: 0, y: 0, z: 0 },
        rotate:    { x: 0, y: 0, z: 0 },
        scale:     1
    };
    
    initialized = true;
    
    // 觸發(fā)init事件
    triggerEvent(root, "impress:init", { api: roots[ "impress-root-" + rootId ] });
};

init()函數(shù)搞清楚了,下面我們分析第二階段:運(yùn)行綁定到impress:init事件上的函數(shù)歉井。
來(lái)看看impress:init事件上面都綁定了什么函數(shù):

root.addEventListener("impress:init", function(){
    // 改變step當(dāng)前狀態(tài)
    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相關(guān)操作
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);

// 綁定鍵盤事件柿祈、觸摸事件和點(diǎn)擊事件
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);
    
    // 綁定鏈接點(diǎn)擊事件
    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);
    
    // 綁定對(duì)象點(diǎn)擊事件
    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);

    // 綁定頁(yè)面resize事件
    window.addEventListener("resize", throttle(function () {
        api.goto( document.querySelector(".step.active"), 500 );
    }, 250), false);
    
}, false);

我們來(lái)梳理一遍,初始化過(guò)程做了什么事:

  • init()函數(shù)中主要初始化畫布哩至、step以及impress對(duì)象內(nèi)部用到的一些狀態(tài)
  • 綁定到impress:init事件上的函數(shù)把其他需要綁定的事件都進(jìn)行了綁定躏嚎,讓impress可以正常工作

接下來(lái)我們分析step切換過(guò)程,來(lái)看看goto函數(shù)都干了什么

什么菩貌?你有點(diǎn)累了卢佣?加把勁,一定要看完goto

var goto = function ( el, duration ) {
    
    if ( !initialized || !(el = getStep(el)) ) {
        //如果沒(méi)初始化或者el不是一個(gè)step就返回
        return false;
    }
    
    // 為了避免載入時(shí)候?yàn)g覽器滾動(dòng)箭阶,手動(dòng)滾動(dòng)到0虚茶,0
    window.scrollTo(0, 0);
    
    var step = stepsData["impress-" + el.id];
    
    // 清理當(dāng)前活躍step上面的標(biāo)記
    if ( activeStep ) {
        activeStep.classList.remove("active");
        body.classList.remove("impress-on-" + activeStep.id);
    }
    // 給el加活躍標(biāo)記
    el.classList.add("active");
    
    body.classList.add("impress-on-" + el.id);
    
    // 計(jì)算canvas相對(duì)于當(dāng)前step的變換參數(shù)
    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就是當(dāng)前活躍step戈鲁,重新計(jì)算scale
    if (el === activeStep) {
        windowScale = computeWindowScale(config);
    }
    
    var targetScale = target.scale * windowScale;
    
    // 觸發(fā)stepleave事件
    if (activeStep && activeStep !== el) {
        onStepLeave(activeStep);
    }
    
    // 這里就是最核心的部分,設(shè)置css來(lái)實(shí)現(xiàn)動(dòng)畫效果
    // 需要注意的是嘹叫,動(dòng)畫效果有兩類:縮放和移動(dòng)
    // 為了讓效果看起來(lái)更逼真婆殿,這兩類動(dòng)畫是分開實(shí)現(xiàn)的
    // 縮放應(yīng)用在root上,移動(dòng)應(yīng)用在canvas上
    // 大家還記得元素的結(jié)構(gòu)嗎待笑?root下面是canvas鸣皂,canvas下面是所有step
    // 所以縮放root的時(shí)候其實(shí)就是縮放canvas
    // 至于為什么分開可以更逼真,請(qǐng)看最后一節(jié)的代碼詳解

    // 這里是把縮放應(yīng)用到root上
    css(root, {
        transform: perspective( config.perspective / targetScale ) + scale( targetScale ),
        transitionDuration: duration + "ms",
        transitionDelay: (zoomin ? delay : 0) + "ms"
    });
    
    // 這里就是把移動(dòng)應(yīng)用到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;
    }
    
    // 存儲(chǔ)當(dāng)前狀態(tài)
    currentState = target;
    activeStep = el;

    // 動(dòng)畫執(zhí)行完畢后觸發(fā)stepenter事件
    window.clearTimeout(stepEnterTimeout);
    stepEnterTimeout = window.setTimeout(function() {
        onStepEnter(activeStep);
    }, duration + delay);
    
    return el;
};

好了暮蹂,下面簡(jiǎn)單看看prev和next函數(shù):

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

很簡(jiǎn)單吧寞缝?他們都是基于goto寫的,所以核心的goto搞懂了也就明白prev和next了仰泻。

消化代碼

非常感謝你能看到這里——或者是直接跳到這里——這篇文章大概是我寫過(guò)的最長(zhǎng)的文章了荆陆,如果你覺(jué)得不錯(cuò)的話請(qǐng)點(diǎn)個(gè)“喜歡”點(diǎn)個(gè)“分享”吧!

本來(lái)想都寫到簡(jiǎn)書里的集侯,但是寫到這里的話會(huì)讓本來(lái)就很長(zhǎng)的文章變得更長(zhǎng)被啼。。棠枉。所以就把代碼詳解寫成了一個(gè)Gist浓体,感興趣的朋友可以看看:
代碼詳解

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市辈讶,隨后出現(xiàn)的幾起案子命浴,更是在濱河造成了極大的恐慌,老刑警劉巖贱除,帶你破解...
    沈念sama閱讀 211,561評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件生闲,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡月幌,警方通過(guò)查閱死者的電腦和手機(jī)碍讯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)扯躺,“玉大人捉兴,你說(shuō)我怎么就攤上這事÷加铮” “怎么了轴术?”我有些...
    開封第一講書人閱讀 157,162評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)钦无。 經(jīng)常有香客問(wèn)我逗栽,道長(zhǎng),這世上最難降的妖魔是什么失暂? 我笑而不...
    開封第一講書人閱讀 56,470評(píng)論 1 283
  • 正文 為了忘掉前任彼宠,我火速辦了婚禮鳄虱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘凭峡。我一直安慰自己拙已,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評(píng)論 6 385
  • 文/花漫 我一把揭開白布摧冀。 她就那樣靜靜地躺著倍踪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪索昂。 梳的紋絲不亂的頭發(fā)上建车,一...
    開封第一講書人閱讀 49,806評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音椒惨,去河邊找鬼缤至。 笑死,一個(gè)胖子當(dāng)著我的面吹牛康谆,可吹牛的內(nèi)容都是我干的领斥。 我是一名探鬼主播,決...
    沈念sama閱讀 38,951評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼沃暗,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼月洛!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起孽锥,我...
    開封第一講書人閱讀 37,712評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤嚼黔,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后忱叭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體隔崎,經(jīng)...
    沈念sama閱讀 44,166評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡今艺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評(píng)論 2 327
  • 正文 我和宋清朗相戀三年韵丑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片虚缎。...
    茶點(diǎn)故事閱讀 38,643評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡撵彻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出实牡,到底是詐尸還是另有隱情陌僵,我是刑警寧澤,帶...
    沈念sama閱讀 34,306評(píng)論 4 330
  • 正文 年R本政府宣布创坞,位于F島的核電站碗短,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏题涨。R本人自食惡果不足惜偎谁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評(píng)論 3 313
  • 文/蒙蒙 一总滩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧巡雨,春花似錦闰渔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至正蛙,卻和暖如春督弓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背跟畅。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工咽筋, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人徊件。 一個(gè)月前我還...
    沈念sama閱讀 46,351評(píng)論 2 360
  • 正文 我出身青樓奸攻,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親虱痕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子睹耐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容