做者:鬍子大哈
原文連接:http://huziketang.com/blog/posts/detail?postId=58aea515204d50674934c3achtml
轉載請註明出處,保留原文連接和做者信息。前端
1 前言react
2 一切從點贊提及git
3 實現可複用性github
3.1 結構複用算法
3.2 生成 DOM 元素而且添加事件app
4 爲何不暴力一點?dom
4.1 狀態改變 -> 構建新的 DOM 元素函數
4.2 從新插入新的 DOM 元素組件化
5 抽象出 Component 類
6 總結
本文會教你如何在 50 行代碼內,不依賴任何第三方的庫,用純 JavaScript 實現一個 React.js 。
本文的目的是:揭開對初學者看起來很很難理解的 React.js 的組件化形式的外衣,讓你有更多的精力和注意力去學習 React.js 精髓的地方。若是你剛開始學習 React.js 而且感受很迷茫,那麼看完這篇文章之後就可以解除一些疑惑。
另外注意,本文所實現的代碼只用於說明教學展現,並不適用於生產環境。代碼託管這個 倉庫 。心急如焚的同窗能夠先去看代碼,但本文會從最基礎的內容開始解釋。
接下來全部的代碼都會從一個基本的點贊功能開始演化,你會逐漸看到,文章代碼慢慢地愈來愈像 React.js 的組件代碼。而在這個過程裏面,你們須要只須要跟着文章的思路,就能夠在代碼的演化當中體會到組件化形式。
假設如今咱們須要實現一個點贊、取消點讚的功能。
[image:B4B41FF2-519A-4A7C-8035-0D5CD4EE8FFA-86900-00013723B2CAE361/8D274601-162D-4B36-B1E0-9C65FB0C494F.png]
若是你對前端稍微有一點了解,你就順手拈來:
HTML:
<body> <div class='wrapper'> <button class='like-btn'> <span class='like-text'>點贊</span> <span>?</span> </button> </div> </body>
爲了現實當中的實際狀況,因此這裏特易把這個 button
的 HTML 結構搞得稍微複雜一些。有了這個 HTML 結構,如今就給它加入一些 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)
功能和實現都很簡單,按鈕已經能夠提供點贊和取消點讚的功能。這時候你的同事跑過來了,說他很喜歡你的按鈕,他也想用你寫的這個點贊功能。你就會發現這種實現方式很致命:你的同事要把整個 button
和裏面的結構複製過去,還有整段 JavaScript 代碼也要複製過去。這樣的實現方式沒有任何可複用性。
因此如今咱們來想辦法解決這個問題,讓這個點贊功能具備較好的可複用的效果,那麼你的同事們就能夠輕鬆自在地使用這個點贊功能。
如今咱們來從新編寫這個點贊功能。此次咱們先寫一個類,這個類有 render 方法,這個方法裏面直接返回一個表示 HTML 結構的字符串:
class LikeButton { render () { return ` <button id='like-btn'> <span class='like-text'>贊</span> <span>?</span> </button> ` } }
而後能夠用這個類來構建不一樣的點贊功能的實例,而後把它們插到頁面中。
const wrapper = document.querySelector('.wrapper') const likeButton1 = new LikeButton() wrapper.innerHTML = likeButton1.render() const likeButton2 = new LikeButton() wrapper.innerHTML += likeButton2.render()
[image:4AEFC6B6-F913-440E-9306-CCC454A7A30C-87312-00013B98FB6F8354/4555573C-8435-4079-9D64-C76913AB6E40.png]
這裏很是暴力地使用了 innerHTML ,把兩個按鈕粗魯地插入了 wrapper 當中。雖然你可能會對這種實現方式很是不滿意,但咱們仍是勉強了實現告終構的複用。咱們後面再來優化它。
你必定會發現,如今的按鈕是死的,你點擊它它根本不會有什麼反應。由於根本沒有往上面添加事件。可是問題來了,LikeButton
類裏面是雖說有一個 button
,可是這玩意根本就是在字符串裏面的。你怎麼能往一個字符串裏面添加事件呢?DOM 事件的 API 只有 DOM 結構才能用。
咱們須要 DOM 結構,準確地來講:咱們須要這個點贊功能的 HTML 字符串表明的 DOM 結構。假設咱們如今有一個函數 createDOMFromString
,你往這個函數傳入 HTML 字符串,可是它會把相應的 DOM 元素返回給你。這個問題就能夠額解決了。
// ::String => ::Document const createDOMFromString = (domString) => { // TODO }
先不用管這個函數應該怎麼實現,先知道它是幹嗎的。拿來用就好,這時候用它來改寫一下 LikeButton
類:
class LikeButton { render () { this.el = createDOMFromString(` <button class='like-button'> <span class='like-text'>點贊</span> <span>?</span> </button> `) this.el.addEventListener('click', () => console.log('click'), false) return this.el } }
如今 render()
返回的不是一個 html 字符串了,而是一個由這個 html 字符串所生成的 DOM。在返回 DOM 元素以前會先給這個 DOM 元素上添加事件在返回。
由於如今 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())
如今你點擊這兩個按鈕,每一個按鈕都會在控制檯打印 click
,說明事件綁定成功了。可是按鈕上的文本仍是沒有發生改變,只要稍微改動一下 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-button'> <span class='like-text'>點贊</span> <span>?</span> </button> `) this.el.addEventListener('click', this.changeLikeText.bind(this), false) return this.el } }
這裏的代碼稍微長了一些,可是仍是很好理解。只不過是在給 LikeButton
類添加了構造函數,這個構造函數會給每個 LikeButton
的實例添加一個對象 state
,state
裏面保存了每一個按鈕本身是否點讚的狀態。還改寫了原來的事件綁定函數:原來只打印 click
,如今點擊的按鈕的時候會調用 changeLikeText
方法,這個方法會根據 this.state
的狀態改變點贊按鈕的文本。
若是你如今還能跟得上文章的思路,那麼你留意下,如今的代碼已經和 React.js 的組件代碼有點相似了。但其實咱們根本沒有講 React.js 的任何內容,咱們一心一意只想怎麼作好「組件化」。
如今這個組件的可複用性已經很不錯了,你的同事們只要實例化一下而後插入到 DOM 裏面去就行了。
仔細留意一下 changeLikeText
函數,這個函數包含了 DOM 操做,如今看起來比較簡單,那是由於如今只有 isLiked
一個狀態。但想一下,由於你的數據狀態改變了你就須要去更新頁面的內容,因此若是你的組件包含了不少狀態,那麼你的組件基本所有都是 DOM 操做。一個組件包含不少狀態的狀況很是常見,因此這裏還有優化的空間:如何儘可能減小這種手動 DOM 操做?
這裏要提出的一種解決方案:一旦狀態發生改變,就從新調用 render
方法,構建一個新的 DOM 元素。這樣作的好處是什麼呢?好處就是你能夠在 render
方法裏面使用最新的 this.state
來構造不一樣 HTML 結構的字符串,而且經過這個字符串構造不一樣的 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
函數裏面的 HTML 字符串會根據 this.state
不一樣而不一樣(這裏是用了 ES6 的字符串特性,作這種事情很方便)。
新增一個 setState
函數,這個函數接受一個對象做爲參數;它會設置實例的 state
,而後從新調用一下 render
方法。
當用戶點擊按鈕的時候, changeLikeText
會構建新的 state
對象,這個新的 state
,傳入 setState
函數當中。
這樣的結果就是,用戶每次點擊,changeLikeText
都會調用改變組件狀態而後調用 setState
;setState
會調用 render
方法從新構建新的 DOM 元素;render
方法會根據 state
的不一樣構建不一樣的 DOM 元素。
也就是說,你只要調用 setState
,組件就會從新渲染。咱們順利地消除了不必的 DOM 操做。
上面的改進不會有什麼效果,由於你仔細看一下就會發現,其實從新渲染的 DOM 元素並無插入到頁面當中。因此這個組件以外,你須要知道這個組件發生了改變,而且把新的 DOM 元素更新到頁面當中。
從新修改一下 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
都會調用 onStateChange
方法,而這個方法是實例化之後時候被設置的,因此你能夠自定義 onStateChange
的行爲。這裏作的事是,每當 setState
的時候,就會把插入新的 DOM 元素,而後刪除舊的元素,頁面就更新了。這裏已經作到了進一步的優化了:如今不須要再手動更新頁面了。
非通常的暴力。不過沒有關係,這種暴力行爲能夠被 Virtual-DOM 的 diff 策略規避掉,但這不是本文章所討論的範圍。
這個版本的點贊功能很不錯,我能夠繼續往上面加功能,並且還不須要手動操做DOM。可是有一個很差的地方,若是我要從新另外作一個新組件,譬如說評論組件,那麼裏面的這些 setState
方法要從新寫一遍,其實這些東西均可以抽出來。
爲了讓代碼更靈活,能夠寫更多的組件,我把這種模式抽象出來,放到一個 Component 類當中:
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' }))
有沒有發現你寫的代碼已經和 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> ` } }
簡單好用,完整的代碼能夠在這裏找到: 倉庫
噢,忘了,還有一個神祕的 createDOMFromString
,其實它更簡單:
const createDOMFromString = (domString) => { const div = document.createElement('div') div.innerHTML = domString return div }
你到底從文章能從文章中獲取到什麼?
好吧,我認可我標題黨了,這個 40 行不到的代碼實際上是一個殘廢並且智障版的 React.js,沒有 JSX ,沒有組件嵌套等等。它只是 React.js 組件化表現形式的一種實現而已。它根本沒有觸碰到 React.js 的精髓。
其實 React.js 的最最精髓的地方可能就在於它的 Virtual DOM 算法,而它的 setState
、props
等等都只不過是一種形式,而不少初學者會被它這種形式做迷惑。本篇文章其實就是揭露了這種組件化形式的實現原理。若是你正在學習或者學習 React.js 過程很迷茫,那麼看完這篇文章之後就可以解除一些疑惑。
本文並無涉及到 Virtual DOM 的任何內容,有須要的同窗能夠參考一下這篇博客 ,介紹的很詳盡。有興趣的同窗能夠把二者結合起來,把 Virtual DOM 替代本文暴力處理的 mount
中的實現,真正實現一個 React.js。
若是你對本文的內容有疑惑,能夠關注個人知乎專欄而且評論或者給我知乎發私信。
我最近正在寫一本《React.js 小書》,對 React.js 感興趣的童鞋,歡迎指點。