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:stepenter
和impress: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ò)冻晤。
那么stepenter
和stepleave
有什么用呢?
假設(shè)我們現(xiàn)在處在第1步绸吸,我們按一下鍵盤上的→鍵就會(huì)切換到第2步鼻弧,這背后impress.js實(shí)際上觸發(fā)了兩個(gè)事件:stepleave
和stepenter
设江,夾在兩個(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浓体,感興趣的朋友可以看看:
代碼詳解