最近在知乎上看到@方應杭用vue寫了一個會動的簡歷,以爲挺好玩的,研究一下其實現思路,決定試試用原生js來實現。css
這張會
動
的簡歷,就好像一個打字員在不斷地錄入文字,頁面呈現動態效果。又好像一個早已經錄製好影片,而咱們只是坐在放映機前觀看。html
原理分兩個部分vue
頁面能看見的不斷跳動着的增長的文字,由innerHTML控制java
頁面的佈局效果由藏在"背後的"style
標籤完成webpack
想象一下你要往一張網頁每間隔0.1秒增長一個啊
字,是否是開個定時器,間斷地往body裏面塞啊
,就能夠啊!沒錯,作到這一步就完成了原理的第一部分git
再想象一下,在往頁面裏面塞啊
的時候,我還想改變啊字的字體顏色以及網頁背景顏色,那應該怎麼作呢,是否是執行下面的代碼就能夠呢,沒錯,只不過更改字體和背景色不是忽然改變的,而是也是開個定時器,間斷地往style
標籤中塞入如下代碼,這樣就完成了原理的第二步,是否是好簡單 ???, 接下來讓咱們一步步完成它程序員
.xxx{ color: blue; background: red; }
在這個項目中咱們es6
使用webpack2來完成項目的構建github
使用yarn來處理依賴包的管理
使用es6的寫法
使用部分原生dom操做api
standard.js(代碼風格約束利器)
目錄結構以下
最重要的幾個模塊分別是resumeEditor(簡歷編輯模塊)
、 stylesEditor(簡歷樣式編輯模塊)
、 以及vQuery(封裝的dom操做模塊)
最後app.js(入口模塊)
再將幾個模塊的功能結合起來完成整個項目。
由於後面的幾個模塊都要依賴這個小模塊,因此咱們先簡單的看下。
class Vquery { constructor (selector, context) { this.elements = getEles(selector, context) } optimizeCb (callback) { ... } get (index) { ... } html (sHtml) { ... } addClass (iClass) { ... } css (styles) { ... } height (h) { ... } scrollTop (top) { ... } } export default (selector, context) => { return new Vquery(selector, context) }
能夠看出它作的事就是封裝一個構造函數Vquery,它的實例會有一些簡單的dom操做方法,最後爲了可以像jQuery那樣使用$().funcName的形式去使用,咱們導出了一個匿名函數,在匿名函數中去new Vquery
簡歷所展示的佈局效果都是由這個模塊完成的,核心方法是showStyles。
const showStyles = (num, callback) => { let style = styles[num] let length let prevLength if (!style) { return } length = styles.filter((item, i) => { // 計算數組styles前n個元素的長度 return i <= num }).reduce((result, item) => { result += item.length return result }, 0) prevLength = length - style.length clearInterval(timer) timer = setInterval(() => { let start = currentStyle.length - prevLength let char = style.substring(start, start + 1) || '' currentStyle += char if (currentStyle.length === length) { // 數組styles前n個元素已經所有塞入,則關閉定時器,而且執行外面傳進來的回調,進而執行下一步操做 clearInterval(timer) callback && callback() } else { let top = $stylePre.height() - MAX_HEIGHT if (top > 0) { // 當塞入的內容已經超過了容器的高度,咱們須要設置一下滾動距離才方便演示接下來的內容 goBottom(top) } $style.html(currentStyle) $stylePre.html(Prism.highlight(currentStyle, Prism.languages.css)) } }, delay) }
簡歷編輯模塊用來展現簡歷內容,主要會經歷由markdown格式往html頁面形式的轉換。
const markdownToHtml = (callback) => { $resumeMarkdown.css({ display: 'none' }) $resumeWrap.addClass(iClass) $resumetag.html(marked(resumeMarkdown)) // 藉助marked工具將markdown轉化爲html callback && callback() // 執行後續的回調 } const showResume = (callback) => { // 原理基本上同stylesEditor, 不斷地往簡歷編輯的容器中塞入事先準備好的簡歷內容,當所有塞入的時候再關閉定時器,並執行後續的回調操做 clearInterval(timer) timer = setInterval(() => { currentMarkdown += resumeMarkdown.substring(start, start + 1) if (currentMarkdown.length === length) { clearInterval(timer) callback && callback() } else { $resumeMarkdown.html(currentMarkdown) start++ } }, delay) }
最後由app入口模塊將以上幾個模塊整合完成項目的功能,咱們找出其中的核心代碼來, ?,你沒看錯,傳說中的回調地獄,亮瞎了個人狗眼啊。想必你們和我同樣都是不肯意看到這坨噁心的代碼的,但對於處理異步問題,回調又的確是一直以來的解決方案之一。
由於定時器的操做是異步行爲,而咱們的簡歷生成過程會涉及到多個異步操做,因此爲了看到如首頁預覽連接的效果,必須等前一個步驟完成以後,才能執行下一步步驟,這裏首先使用的回調函數的解決方案,你們能夠從github上拉取代碼,分別切換如下幾個分支來查看不一樣的解決方案
master(使用回調函數處理)
promise(使用promise處理)
generator-thunk(使用generator + thunk函數處理)
generator-promise(使用generator + promise處理)
async(使用async處理)
showStyles(0, () => { showResume(() => { showStyles(1, () => { markdownToHtml(() => { showStyles(2) }) }) }) })
回調方式可以解決異步操做問題,可是代碼寫起來很是的不美觀,可讀性差,代碼呈橫向發展趨勢...偉大的程序員們開疆擴土發明了promise的解決方案。咱們來看一下promise分支中app模塊最終的寫法
showStylesWrap(0) .then(showResumeWrap) .then(showStylesWrap.bind(null, 1)) .then(markdownToHtmlWrap) .then(showStylesWrap.bind(null, 2))
能夠看到,代碼清爽了不少,縱向發展,應用第一步第二步第三步...一眼就可以看出來,固然實現的邏輯是將原來的相關的模塊用Promise包裝起來,而且在原來回調函數執行的地方resolve便可,詳細實現,歡迎查看項目源碼
兩種方式比較相似,都要用到es6中的generator。關於什麼是generator,thunk函數,能夠查看軟大神關於ECMAScript 6 入門,這裏簡要地講述一下,其如何處理異步操做問題使得能夠將異步行爲寫起來如同步般爽。
function timeOut1 () { setTimeout(() => { console.log(1111) }, 1000) } function timeOut2 () { setTimeout(() => { console.log(2222) }, 200) } function * gen () { yield timeOut1() yield timeOut2() } let g = gen() g.next() g.next()
上面的代碼在過了200毫秒會log出2222,過了1秒鐘以後log出1111
這,要?了,你不是說generator寫起來同步能夠解決異步問題嗎,爲毛這裏timeOut2沒有在timeOut1以後執行呢,畢竟gen函數中看起來是但願這樣的嘛。
其實否則,timeOut2啥時候執行取決於
g.next() g.next()
試想兩個函數幾乎同時執行,那在定時器中固然是200毫秒後的timeOut2先打印出2222來,可是有沒有辦法,讓timeOut2在timeOut1後執行呢?答案是有的
function timeOut1 () { setTimeout(() => { console.log(1111) g.next() }, 1000) } function timeOut2 () { setTimeout(() => { console.log(2222) }, 200) } function * gen () { yield timeOut1() yield timeOut2() } let g = gen() g.next()
能夠看到咱們在timeOut1執行完成以後,再將指針指向下一個位置,即timeOut2再去執行,這樣的結果就和gen函數中兩個yield的寫起來同步感受同樣了。可是含有一個問題,若是涉及到不少個異步操做,咱們是很難經過上面的方式將異步流程管理起來的。因而咱們須要作下面一件事
function co (fn) { var gen = fn(); function next(err, data) { var result = gen.next(data); if (result.done) return; result.value(next); // thunk和promise不一樣地方之一在這裏, promise是result.value.then(next) } next(); }
內部的next函數就是 thunk 的回調函數。next函數先將指針移到 generator 函數的下一步(gen.next方法),而後判斷 generator 函數是否結束(result.done屬性),若是沒結束,就將next函數再傳入 thunk 函數(result.value屬性),不然就直接退出。
最後咱們在看一下經過co函數的寫法完成上面的例子
function timeOut1() { return (callback) => { setTimeout(() => { console.log(1111) callback() }, 1000) } } function timeOut2() { return (callback) => { setTimeout(() => { console.log(2222) callback() }, 200) } } function co(fn) { var gen = fn(); function next(err, data) { var result = gen.next(data); if (result.done) return; result.value(next); // thunk和promise不一樣地方之一在這裏, promise是result.value.then(next) } next(); } co(function * () { yield timeOut1() yield timeOut2() })
async其實就是generator函數的語法糖。你們若是把generator弄明白了,使用它必定再也不話下,關於這個項目的用法,歡迎查看async分支源代碼,這裏再也不贅述。
本文中可能存在闡述不當的地方,歡迎你們指正。???,最後點個贊,點個star好很差呀。