作者:胡子大哈
******目錄**
1 前言
2 一切從點贊說起
3 實現(xiàn)可復(fù)用性
3.1 結(jié)構(gòu)復(fù)用
3.2 生成 DOM 元素并且添加事件
4 為什么不暴力一點蠢终?
4.1 狀態(tài)改變 -> 構(gòu)建新的 DOM 元素
4.2 重新插入新的 DOM 元素
5 抽象出 Component 類
6 總結(jié)
1澎怒、前言
本文會教你如何在 40 行代碼內(nèi)啥酱,不依賴任何第三方的庫订晌,用純 JavaScript 實現(xiàn)一個 React.js 甥温。
本文的目的是:揭開對初學(xué)者看起來很很難理解的 React.js 的組件化形式的外衣调鬓。如果你剛開始學(xué)習(xí) React.js 并且感覺很迷茫卦尊,那么看完這篇文章以后就能夠解除一些疑惑。
另外注意报亩,本文所實現(xiàn)的代碼只用于說明教學(xué)展示浴鸿,并不適用于生產(chǎn)環(huán)境。代碼托管這個 倉庫弦追。心急如焚的同學(xué)可以先去看代碼岳链,但本文會從最基礎(chǔ)的內(nèi)容開始解釋。
2劲件、一切從點贊說起
接下來所有的代碼都會從一個基本的點贊功能開始演化掸哑,你會逐漸看到,文章代碼慢慢地越來越像 React.js 的組件代碼零远。而在這個過程里面苗分,大家需要只需要跟著文章的思路,就可以在代碼的演化當(dāng)中體會到組件化形式遍烦。
假設(shè)現(xiàn)在我們需要實現(xiàn)一個點贊俭嘁、取消點贊的功能。
如果你對前端稍微有一點了解服猪,你就順手拈來:
HTML:
<body> <div class='wrapper'> <button class='like-btn'> <span class='like-text'>點贊</span> <span>??</span> </button> </div> </body>
為了現(xiàn)實當(dāng)中的實際情況供填,所以這里特意把這個 button 的 HTML 結(jié)構(gòu)搞得稍微復(fù)雜一些拐云。有了這個 HTML 結(jié)構(gòu),現(xiàn)在就給它加入一些 JavaScript 的行為:
JavaScript:
const button = document.querySelector('.like-btn') const buttonText = button.querySelector('.like-text') let isLiked = false button.addEventListener('click', function () { isLiked = !isLiked if (isLiked) { buttonText.innerHTML = '取消' } else { buttonText.innerHTML = '點贊' } }, false)
功能和實現(xiàn)都很簡單近她,按鈕已經(jīng)可以提供點贊和取消點贊的功能叉瘩。這時候你的同事跑過來了,說他很喜歡你的按鈕粘捎,他也想用你寫的這個點贊功能薇缅。你就會發(fā)現(xiàn)這種實現(xiàn)方式很致命:你的同事要把整個 button 和里面的結(jié)構(gòu)復(fù)制過去,還有整段 JavaScript 代碼也要復(fù)制過去攒磨。這樣的實現(xiàn)方式?jīng)]有任何可復(fù)用性泳桦。
3、實現(xiàn)可復(fù)用性
所以現(xiàn)在我們來想辦法解決這個問題娩缰,讓這個點贊功能具有較好的可復(fù)用的效果灸撰,那么你的同事們就可以輕松自在地使用這個點贊功能。
3.1 結(jié)構(gòu)復(fù)用
現(xiàn)在我們來重新編寫這個點贊功能拼坎。這次我們先寫一個類浮毯,這個類有 render 方法,這個方法里面直接返回一個表示 HTML 結(jié)構(gòu)的字符串:
class LikeButton { render () { return <button class='like-btn'> <span class='like-text'>贊</span> <span>??</span> </button>
} }
然后可以用這個類來構(gòu)建不同的點贊功能的實例泰鸡,然后把它們插到頁面中债蓝。
const wrapper = document.querySelector('.wrapper') const likeButton1 = new LikeButton() wrapper.innerHTML = likeButton1.render() const likeButton2 = new LikeButton() wrapper.innerHTML += likeButton2.render()
這里非常暴力地使用了 innerHTML ,把兩個按鈕粗魯?shù)夭迦肓?wrapper 當(dāng)中盛龄。雖然你可能會對這種實現(xiàn)方式非常不滿意饰迹,但我們還是勉強了實現(xiàn)了結(jié)構(gòu)的復(fù)用。我們后面再來優(yōu)化它讯嫂。
3.2 生成 DOM 元素并且添加事件
你一定會發(fā)現(xiàn)蹦锋,現(xiàn)在的按鈕是死的,你點擊它它根本不會有什么反應(yīng)欧芽。因為根本沒有往上面添加事件。但是問題來了葛圃,LikeButton 類里面是雖然說有一個 button千扔,但是這玩意根本就是在字符串里面的。你怎么能往一個字符串里面添加事件呢库正?DOM 事件的 API 只有 DOM 結(jié)構(gòu)才能用曲楚。
我們需要 DOM 結(jié)構(gòu),準確地來說:我們需要這個點贊功能的 HTML 字符串代表的 DOM 結(jié)構(gòu)褥符。假設(shè)我們現(xiàn)在有一個函數(shù) createDOMFromString 龙誊,你往這個函數(shù)傳入 HTML 字符串,但是它會把相應(yīng)的 DOM 元素返回給你喷楣。這個問題就可以額解決了趟大。
// ::String => ::Documentconst createDOMFromString = (domString) => { // TODO }
先不用管這個函數(shù)應(yīng)該怎么實現(xiàn)鹤树,先知道它是干嘛的。拿來用就好逊朽,這時候用它來改寫一下 LikeButton 類:
class LikeButton { render () { this.el = createDOMFromString(<button class='like-btn'> <span class='like-text'>點贊</span> <span>??</span> </button>
) this.el.addEventListener('click', () => console.log('click'), false) return this.el } }
現(xiàn)在 render() 返回的不是一個 html 字符串了罕伯,而是一個由這個 html 字符串所生成的 DOM。在返回 DOM 元素之前會先給這個 DOM 元素上添加事件再返回叽讳。
因為現(xiàn)在 render 返回的是 DOM 元素追他,所以不能用 innerHTML 暴力地插入 wrapper。而是要用 DOM API 插進去岛蚤。
const wrapper = document.querySelector('.wrapper') const likeButton1 = new LikeButton() wrapper.appendChild(likeButton1.render()) const likeButton2 = new LikeButton() wrapper.appendChild(likeButton2.render())
現(xiàn)在你點擊這兩個按鈕邑狸,每個按鈕都會在控制臺打印 click,說明事件綁定成功了涤妒。但是按鈕上的文本還是沒有發(fā)生改變推溃,只要稍微改動一下 LikeButton 的代碼就可以完成完整的功能:
class LikeButton { constructor () { this.state = { isLiked: false } } changeLikeText () { const likeText = this.el.querySelector('.like-text') this.state.isLiked = !this.state.isLiked if (this.state.isLiked) { likeText.innerHTML = '取消' } else { likeText.innerHTML = '點贊' } } render () { this.el = createDOMFromString(<button class='like-btn'> <span class='like-text'>點贊</span> <span>??</span> </button>
) this.el.addEventListener('click', this.changeLikeText.bind(this), false) return this.el } }
這里的代碼稍微長了一些,但是還是很好理解届腐。只不過是在給 LikeButton 類添加了構(gòu)造函數(shù)铁坎,這個構(gòu)造函數(shù)會給每一個 LikeButton 的實例添加一個對象 state,state 里面保存了每個按鈕自己是否點贊的狀態(tài)犁苏。還改寫了原來的事件綁定函數(shù):原來只打印 click硬萍,現(xiàn)在點擊的按鈕的時候會調(diào)用 changeLikeText 方法,這個方法會根據(jù) this.state 的狀態(tài)改變點贊按鈕的文本围详。
如果你現(xiàn)在還能跟得上文章的思路朴乖,那么你留意下,現(xiàn)在的代碼已經(jīng)和 React.js 的組件代碼有點類似了助赞。但其實我們根本沒有講 React.js 的任何內(nèi)容买羞,我們一心一意只想怎么做好“組件化”。
現(xiàn)在這個組件的可復(fù)用性已經(jīng)很不錯了雹食,你的同事們只要實例化一下然后插入到 DOM 里面去就好了畜普。
4、為什么不暴力一點群叶?
仔細留意一下 changeLikeText 函數(shù)吃挑,這個函數(shù)包含了 DOM 操作,現(xiàn)在看起來比較簡單街立,那是因為現(xiàn)在只有 isLiked 一個狀態(tài)舶衬。但想一下,因為你的數(shù)據(jù)狀態(tài)改變了你就需要去更新頁面的內(nèi)容赎离,所以如果你的組件包含了很多狀態(tài)逛犹,那么你的組件基本全部都是 DOM 操作。一個組件包含很多狀態(tài)的情況非常常見,所以這里還有優(yōu)化的空間:如何盡量減少這種手動 DOM 操作虽画?
4.1 狀態(tài)改變 -> 構(gòu)建新的 DOM 元素
這里要提出的一種解決方案:一旦狀態(tài)發(fā)生改變舞蔽,就重新調(diào)用 render 方法,構(gòu)建一個新的 DOM 元素狸捕。這樣做的好處是什么呢喷鸽?好處就是你可以在 render 方法里面使用最新的 this.state 來構(gòu)造不同 HTML 結(jié)構(gòu)的字符串,并且通過這個字符串構(gòu)造不同的 DOM 元素灸拍。頁面就更新了做祝!聽起來有點繞,看看代碼怎么寫:
class LikeButton { constructor () { this.state = { isLiked: false } } setState (state) { this.state = state this.el = this.render() } changeLikeText () { this.setState({ isLiked: !this.state.isLiked }) } render () { this.el = createDOMFromString(<button class='like-btn'> <span class='like-text'>${this.state.isLiked ? '取消' : '點贊'}</span> <span>??</span> </button>
) this.el.addEventListener('click', this.changeLikeText.bind(this), false) return this.el } }
其實只是改了幾個小地方:
render 函數(shù)里面的 HTML 字符串會根據(jù) this.state 不同而不同(這里是用了 ES6 的字符串特性鸡岗,做這種事情很方便)混槐。
新增一個 setState 函數(shù),這個函數(shù)接受一個對象作為參數(shù)轩性;它會設(shè)置實例的 state声登,然后重新調(diào)用一下 render 方法。
當(dāng)用戶點擊按鈕的時候揣苏, changeLikeText 會構(gòu)建新的 state 對象悯嗓,這個新的 state ,傳入 setState 函數(shù)當(dāng)中卸察。
這樣的結(jié)果就是脯厨,用戶每次點擊,changeLikeText 都會調(diào)用改變組件狀態(tài)然后調(diào)用 setState坑质;setState 會調(diào)用 render 方法重新構(gòu)建新的 DOM 元素合武;render 方法會根據(jù) state 的不同構(gòu)建不同的 DOM 元素。
也就是說涡扼,你只要調(diào)用 setState稼跳,組件就會重新渲染。我們順利地消除了沒必要的 DOM 操作吃沪。
4.2 重新插入新的 DOM 元素
上面的改進不會有什么效果汤善,因為你仔細看一下就會發(fā)現(xiàn),其實重新渲染的 DOM 元素并沒有插入到頁面當(dāng)中巷波。所以這個組件之外萎津,你需要知道這個組件發(fā)生了改變,并且把新的 DOM 元素更新到頁面當(dāng)中抹镊。
重新修改一下 setState 方法:
... setState (state) { const oldEl = this.el this.state = state this.el = this.render() if (this.onStateChange) this.onStateChange(oldEl, this.el) }...
使用這個組件的時候:
const likeButton = new LikeButton()wrapper.appendChild(likeButton.render()) // 第一次插入 DOM 元素component.onStateChange = (oldEl, newEl) => { wrapper.insertBefore(newEl, oldEl) // 插入新的元素 wrapper.removeChild(oldEl) // 刪除舊的元素}
這里每次 setState 都會調(diào)用 onStateChange 方法,而這個方法是實例化以后時候被設(shè)置的荤傲,所以你可以自定義 onStateChange 的行為垮耳。這里做的事是,每當(dāng) setState 的時候,就會把插入新的 DOM 元素终佛,然后刪除舊的元素俊嗽,頁面就更新了。這里已經(jīng)做到了進一步的優(yōu)化了:現(xiàn)在不需要再手動更新頁面了铃彰。
非一般的暴力绍豁。不過沒有關(guān)系,這種暴力行為可以被 Virtual-DOM 的 diff 策略規(guī)避掉牙捉,但這不是本文章所討論的范圍竹揍。
這個版本的點贊功能很不錯,我可以繼續(xù)往上面加功能邪铲,而且還不需要手動操作DOM芬位。但是有一個不好的地方,如果我要重新另外做一個新組件带到,譬如說評論組件昧碉,那么里面的這些 setState 方法要重新寫一遍,其實這些東西都可以抽出來揽惹。
5被饿、抽象出 Component 類
為了讓代碼更靈活,可以寫更多的組件搪搏,我把這種模式抽象出來狭握,放到一個 Component 類當(dāng)中:
class Component { constructor (props = {}) { this.props = props } setState (state) { const oldEl = this.el this.state = state this.el = this.renderDOM() if (this.onStateChange) this.onStateChange(oldEl, this.el) } renderDOM () { this.el = createDOMFromString(this.render()) if (this.onClick) { this.el.addEventListener('click', this.onClick.bind(this), false) } return this.el } }
還有一個額外的 mount 的方法,其實就是把組件的 DOM 元素插入頁面慕嚷,并且在 setState 的時候更新頁面:
const mount = (wrapper, component) => { wrapper.appendChild(component.renderDOM()) component.onStateChange = (oldEl, newEl) => { wrapper.insertBefore(newEl, oldEl) wrapper.removeChild(oldEl) } }
這樣的話我們重新寫點贊組件就會變成:
class LikeButton extends Component { constructor (props) { super(props) this.state = { isLiked: false } } onClick () { this.setState({ isLiked: !this.state.isLiked }) } render () { return <button class='like-btn'> <span class='like-text'>${this.props.word || ''} ${this.state.isLiked ? '取消' : '點贊'}</span> <span>??</span> </button>
} } mount(wrapper, new LikeButton({ word: 'hello' }))
有沒有發(fā)現(xiàn)你寫的代碼已經(jīng)和 React.js 的組件寫法很相似了哥牍?而且還是可以正常運作的代碼,而且我們從頭到尾都是用純的 JavaScript喝检,沒有依賴任何第三方庫嗅辣。(注意這里加入了上面沒有提到過點 props,可以給組件傳入配置屬性挠说,跟 React.js 一樣)澡谭。
只要有了上面那個 Component 類和 mount 方法加起來不足40行代碼就可以做到組件化。如果我們需要寫另外一個組件损俭,只需要像上面那樣蛙奖,簡單地繼承一下 Component 類就好了:
class RedBlueButton extends Component { constructor (props) { super(props) this.state = { color: 'red' } } onClick () { this.setState({ color: 'blue' }) } render () { return <div style='color: ${this.state.color};'>${this.state.color}</div>
} }
簡單好用,完整的代碼可以在這里找到: React.js in 40
噢杆兵,忘了雁仲,還有一個神秘的 createDOMFromString,其實它更簡單:
const createDOMFromString = (domString) => { const div = document.createElement('div') div.innerHTML = domString return div }
6琐脏、總結(jié)
你到底能從文章中獲取到什么攒砖?
好吧缸兔,我承認我標題黨了,這個 40 行不到的代碼其實是一個殘廢而且智障版的 React.js吹艇,沒有 JSX 惰蜜,沒有組件嵌套等等。它只是 React.js 組件化表現(xiàn)形式的一種實現(xiàn)而已受神。
React 的 setState 抛猖、props 等等都只不過是一種形式,而很多初學(xué)者會被它這種形式作迷惑鼻听。本篇文章其實就是揭露了這種組件化形式的實現(xiàn)原理财著。如果你正在學(xué)習(xí)或者學(xué)習(xí) React.js 過程很迷茫,那么看完這篇文章以后就能夠解除一些疑惑精算。
本文并沒有涉及到 Virtual DOM 的任何內(nèi)容瓢宦,有需要的同學(xué)可以參考一下這篇博客 ,介紹的很詳盡灰羽。有興趣的同學(xué)可以把兩者結(jié)合起來驮履,把 Virtual DOM 替代本文暴力處理的 mount 中的實現(xiàn),真正實現(xiàn)一個 React.js廉嚼。
京程一燈玫镐,夢起的地方,我們始終相信通過努力怠噪,可以改變自己的命運恐似。
我們始終相信,通過堅持不懈傍念,可以為大家解決更多的前端技術(shù)問題矫夷。
我們始終相信,時間可以證明憋槐,我們可以為廣大IT從業(yè)者解決前端學(xué)習(xí)路線双藕。
HTML5,CSS3阳仔,Web前端忧陪,jquery,javascript近范,前端學(xué)習(xí)路線嘶摊,各類問題,我們都可以為你解決评矩。
更多技術(shù)好文叶堆,前端問題,面試技巧斥杜,請關(guān)注京程一燈(原一燈學(xué)堂)