impress.js 源碼分析
來自:http://hao.jser.com/archive/8126/
前言:
之前做簡歷用到了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-x,data-y,data-z等屬性。而我們一般做impress的時候就是只改變這些參數來達到變換的目的,在上文中我們通過瀏覽器的調試已經發現了這些參數和最終加載在div上的style樣式是有關系的。
即data-x對應為translateX;data-y對應translateY;data-z對應translateZ
5.我們可以很”膚淺“得出結論:impress的水平移動是改變了translateX坐標,垂直移動是改變translateY坐標,而突然變小又變大的絢麗效果是改變translateZ的坐標。而這些轉化樣式,事件監聽是通過js來實現的。
二。impress.js具體的技術實現?
1.源碼閱讀從data-* 屬性入手
</blockquote>這個是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.源碼的整體代碼架構
</blockquote>看到第一個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 ) {......}</pre>
我們可以再看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 ); // };</pre> <p>//forEach是javascript的數組循環遍歷函數。<br />
//canvas的來源:line270 var canvas = document.createElement(“div”);
//我們在瀏覽器中調試發現root.childNodes是一個數組,是包裹在 </p>里面 的所有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 ] }); };</pre> <p>//我們遇了triggerEvent()函數,這個是自定義事件監聽函數,源碼如下 </p>
/*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事件
</blockquote>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);</pre> <blockquote>
init是初始化事件,stepenter是進入下一步事件,stepleave是離開上一步事件。具體的函數源碼如下
</blockquote>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對象,這是一個全局對象
</blockquote>
onStepEnter 用于觸發impress:stepenter事件
onStepLeave 用于觸發impress:stepleave事件
initStep 初始化給定step init 主初始化函數
getStep 獲取指定step goto 切換到指定step
prev 切換到上一個step next 切換到下一個step三.impress.js源碼分析的總結
我會把impress.js源碼逐字解讀放在github上,稍后更新,就不在這里啰嗦了.我是前端菜鳥,希望大家一起來分析討論.共享代碼和思想.
關于總結,與其說是總結,不如說是我的一點心得體會吧.我們也許用原生js做過單獨的全屏滾動,
</blockquote>
我們也許重寫過鼠標鍵盤事件,
我們也許也做過自定義事件的綁定.
我們也許用過data-*的自定義數據
我們也許用過css3 transform和translate3d 做過動畫
我們也許….有很多技術我們單獨實現都很簡單,但是把他們綜合在一起就發現好難,如何保證命名空間不污染,變量作用域,如何寫出兼容性的js和css代碼,如何處理好各種代碼細節,這都是我們需要反思的地方.impress.js是我第一次閱讀的js源碼,今后我會把更多發現的問題寫在這里,文章會持續更新,和大家一起討論進步學習.