用原生js寫一個(gè)"多動(dòng)癥"的簡(jiǎn)歷
最近在知乎上看到@方應(yīng)杭用vue寫了一個(gè)會(huì)動(dòng)的簡(jiǎn)歷墨缘,覺(jué)得挺好玩的撮躁,研究一下其實(shí)現(xiàn)思路尘奏,決定試試用原生js來(lái)實(shí)現(xiàn)马昨。
會(huì)動(dòng)的簡(jiǎn)歷實(shí)現(xiàn)思路
這張會(huì)
動(dòng)
的簡(jiǎn)歷,就好像一個(gè)打字員在不斷地錄入文字虽界,頁(yè)面呈現(xiàn)動(dòng)態(tài)效果汽烦。又好像一個(gè)早已經(jīng)錄制好影片,而我們只是坐在放映機(jī)前觀看莉御。
原理分兩個(gè)部分
- 頁(yè)面能看見(jiàn)的不斷跳動(dòng)著的增加的文字撇吞,由innerHTML控制
- 頁(yè)面的布局效果由藏在"背后的"
style
標(biāo)簽完成
想象一下你要往一張網(wǎng)頁(yè)每間隔0.1秒增加一個(gè)啊
字俗冻,是不是開(kāi)個(gè)定時(shí)器,間斷地往body里面塞啊
梢夯,就可以啊晴圾!沒(méi)錯(cuò)颂砸,做到這一步就完成了原理的第一部分
再想象一下,在往頁(yè)面里面塞啊
的時(shí)候死姚,我還想改變啊字的字體顏色以及網(wǎng)頁(yè)背景顏色人乓,那應(yīng)該怎么做呢,是不是執(zhí)行下面的代碼就可以呢都毒,沒(méi)錯(cuò)色罚,只不過(guò)更改字體和背景色不是突然改變的,而是也是開(kāi)個(gè)定時(shí)器账劲,間斷地往style
標(biāo)簽中塞入以下代碼戳护,這樣就完成了原理的第二步,是不是好簡(jiǎn)單 ??????瀑焦, 接下來(lái)讓我們一步步完成它
.xxx{
color: blue;
background: red;
}
項(xiàng)目搭建
在這個(gè)項(xiàng)目中我們
- 使用webpack2來(lái)完成項(xiàng)目的構(gòu)建
- 使用yarn來(lái)處理依賴包的管理
- 使用es6的寫法
- 使用部分原生dom操作api
- standard.js(代碼風(fēng)格約束利器)
目錄結(jié)構(gòu)如下
最重要的幾個(gè)模塊分別是resumeEditor(簡(jiǎn)歷編輯模塊)
腌且、 stylesEditor(簡(jiǎn)歷樣式編輯模塊)
、 以及vQuery(封裝的dom操作模塊)
最后app.js(入口模塊)
再將幾個(gè)模塊的功能結(jié)合起來(lái)完成整個(gè)項(xiàng)目榛瓮。
vQuery(封裝的dom操作模塊)
因?yàn)楹竺娴膸讉€(gè)模塊都要依賴這個(gè)小模塊铺董,所以我們先簡(jiǎn)單的看下。
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)
}
可以看出它做的事就是封裝一個(gè)構(gòu)造函數(shù)Vquery禀晓,它的實(shí)例會(huì)有一些簡(jiǎn)單的dom操作方法精续,最后為了能夠像jQuery那樣使用$().funcName的形式去使用,我們導(dǎo)出了一個(gè)匿名函數(shù)粹懒,在匿名函數(shù)中去new Vquery
stylesEditor(簡(jiǎn)歷樣式編輯模塊)
簡(jiǎn)歷所展現(xiàn)的布局效果都是由這個(gè)模塊完成的,核心方法是showStyles重付。
const showStyles = (num, callback) => {
let style = styles[num]
let length
let prevLength
if (!style) {
return
}
length = styles.filter((item, i) => { // 計(jì)算數(shù)組styles前n個(gè)元素的長(zhǎng)度
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) { // 數(shù)組styles前n個(gè)元素已經(jīng)全部塞入,則關(guān)閉定時(shí)器凫乖,并且執(zhí)行外面?zhèn)鬟M(jìn)來(lái)的回調(diào)堪夭,進(jìn)而執(zhí)行下一步操作
clearInterval(timer)
callback && callback()
} else {
let top = $stylePre.height() - MAX_HEIGHT
if (top > 0) { // 當(dāng)塞入的內(nèi)容已經(jīng)超過(guò)了容器的高度,我們需要設(shè)置一下滾動(dòng)距離才方便演示接下來(lái)的內(nèi)容
goBottom(top)
}
$style.html(currentStyle)
$stylePre.html(Prism.highlight(currentStyle, Prism.languages.css))
}
}, delay)
}
stylesEditor(簡(jiǎn)歷樣式編輯模塊)
簡(jiǎn)歷編輯模塊用來(lái)展示簡(jiǎn)歷內(nèi)容拣凹,主要會(huì)經(jīng)歷由markdown格式往html頁(yè)面形式的轉(zhuǎn)換森爽。
const markdownToHtml = (callback) => {
$resumeMarkdown.css({
display: 'none'
})
$resumeWrap.addClass(iClass)
$resumetag.html(marked(resumeMarkdown)) // 借助marked工具將markdown轉(zhuǎn)化為html
callback && callback() // 執(zhí)行后續(xù)的回調(diào)
}
const showResume = (callback) => { // 原理基本上同stylesEditor, 不斷地往簡(jiǎn)歷編輯的容器中塞入事先準(zhǔn)備好的簡(jiǎn)歷內(nèi)容嚣镜,當(dāng)全部塞入的時(shí)候再關(guān)閉定時(shí)器爬迟,并執(zhí)行后續(xù)的回調(diào)操作
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(入口模塊)
最后由app入口模塊將以上幾個(gè)模塊整合完成項(xiàng)目的功能,我們找出其中的核心代碼來(lái), ??菊匿,你沒(méi)看錯(cuò)付呕,傳說(shuō)中的回調(diào)地獄计福,亮瞎了我的狗眼啊。想必大家和我一樣都是不愿意看到這坨惡心的代碼的徽职,但對(duì)于處理異步問(wèn)題象颖,回調(diào)又的確是一直以來(lái)的解決方案之一。
因?yàn)槎〞r(shí)器的操作是異步行為姆钉,而我們的簡(jiǎn)歷生成過(guò)程會(huì)涉及到多個(gè)異步操作说订,所以為了看到如首頁(yè)預(yù)覽鏈接的效果,必須等前一個(gè)步驟完成之后潮瓶,才能執(zhí)行下一步步驟陶冷,這里首先使用的回調(diào)函數(shù)的解決方案,大家可以從github上拉取代碼毯辅,分別切換以下幾個(gè)分支來(lái)查看不同的解決方案
- master(使用回調(diào)函數(shù)處理)
- promise(使用promise處理)
- generator-thunk(使用generator + thunk函數(shù)處理)
- generator-promise(使用generator + promise處理)
- async(使用async處理)
showStyles(0, () => {
showResume(() => {
showStyles(1, () => {
markdownToHtml(() => {
showStyles(2)
})
})
})
})
解決回調(diào)地獄之promise
回調(diào)方式能夠解決異步操作問(wèn)題埂伦,但是代碼寫起來(lái)非常的不美觀,可讀性差思恐,代碼呈橫向發(fā)展趨勢(shì)...偉大的程序員們開(kāi)疆?dāng)U土發(fā)明了promise的解決方案沾谜。我們來(lái)看一下promise分支中app模塊最終的寫法
showStylesWrap(0)
.then(showResumeWrap)
.then(showStylesWrap.bind(null, 1))
.then(markdownToHtmlWrap)
.then(showStylesWrap.bind(null, 2))
可以看到,代碼清爽了很多胀莹,縱向發(fā)展类早,應(yīng)用第一步第二步第三步...一眼就能夠看出來(lái),當(dāng)然實(shí)現(xiàn)的邏輯是將原來(lái)的相關(guān)的模塊用Promise包裝起來(lái)嗜逻,并且在原來(lái)回調(diào)函數(shù)執(zhí)行的地方resolve即可涩僻,詳細(xì)實(shí)現(xiàn),歡迎查看項(xiàng)目源碼
解決回調(diào)地獄之generator-thunk栈顷,generator-promise
兩種方式比較類似逆日,都要用到es6中的generator。關(guān)于什么是generator萄凤,thunk函數(shù)室抽,可以查看軟大神關(guān)于ECMAScript 6 入門,這里簡(jiǎn)要地講述一下,其如何處理異步操作問(wèn)題使得可以將異步行為寫起來(lái)如同步般爽靡努。
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()
上面的代碼在過(guò)了200毫秒會(huì)log出2222坪圾,過(guò)了1秒鐘之后log出1111
這,要??了惑朦,你不是說(shuō)generator寫起來(lái)同步可以解決異步問(wèn)題嗎兽泄,為毛這里timeOut2沒(méi)有在timeOut1之后執(zhí)行呢,畢竟gen函數(shù)中看起來(lái)是希望這樣的嘛漾月。
其實(shí)不然病梢,timeOut2啥時(shí)候執(zhí)行取決于
g.next()
g.next()
試想兩個(gè)函數(shù)幾乎同時(shí)執(zhí)行,那在定時(shí)器中當(dāng)然是200毫秒后的timeOut2先打印出2222來(lái),但是有沒(méi)有辦法蜓陌,讓timeOut2在timeOut1后執(zhí)行呢觅彰?答案是有的
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()
可以看到我們?cè)趖imeOut1執(zhí)行完成之后,再將指針指向下一個(gè)位置钮热,即timeOut2再去執(zhí)行填抬,這樣的結(jié)果就和gen函數(shù)中兩個(gè)yield的寫起來(lái)同步感覺(jué)一樣了。但是含有一個(gè)問(wèn)題隧期,如果涉及到很多個(gè)異步操作飒责,我們是很難通過(guò)上面的方式將異步流程管理起來(lái)的。于是我們需要做下面一件事
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();
}
內(nèi)部的next函數(shù)就是 thunk 的回調(diào)函數(shù)读拆。next函數(shù)先將指針移到 generator 函數(shù)的下一步(gen.next方法)擅憔,然后判斷 generator 函數(shù)是否結(jié)束(result.done屬性)鸵闪,如果沒(méi)結(jié)束,就將next函數(shù)再傳入 thunk 函數(shù)(result.value屬性)暑诸,否則就直接退出蚌讼。
最后我們?cè)诳匆幌峦ㄟ^(guò)co函數(shù)的寫法完成上面的例子
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()
})
解決回調(diào)地獄之a(chǎn)sync
async其實(shí)就是generator函數(shù)的語(yǔ)法糖个榕。大家如果把generator弄明白了篡石,使用它一定不再話下,關(guān)于這個(gè)項(xiàng)目的用法西采,歡迎查看async分支源代碼凰萨,這里不再贅述。
尾述
本文中可能存在闡述不當(dāng)?shù)牡胤叫倒荩瑲g迎大家指正胖眷。??????,最后點(diǎn)個(gè)贊霹崎,點(diǎn)個(gè)star好不好呀珊搀。
源碼地址