假如你的專案使用了React,你知道怎麼做效能優化嗎?
你知道為什麼React讓你寫shouldComponentUpdate或者React.PureComponent嗎?
你知道為什麼React讓你寫Immutable Data Structures嗎?
你知道為什麼React讓你在渲染列表時,一定要給每個子項加一個key嗎?
你知道為什麼React讓你在條件渲染時,不寫if而寫&&操作符或三元操作符嗎?
一切的答案都在Virtual DOM上!
只要你跟著我完成了這個手寫Virtual DOM的系列,上面的所有問題你都將得到解答,從此進入react高手的陣營!
上集回顧
從零開始手把手教你實現一個Virtual DOM(一)
上一集我們介紹了什麼是VDOM,為什麼要用VDOM,以及我們要怎樣來實現一個VDOM。我們再來看一下這張藍圖,今天我們要實現的是這張圖的左半部分。
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "name": "vdom", "version": "1.0.0", "description": "", "scripts": { "compile": "babel index.js --out-file compiled.js" }, "author": "", "license": "", "devDependencies": { "babel-cli": "^6.23.0", "babel-plugin-transform-react-jsx": "^6.23.0" } } |
這裡主要主要兩點:
devDependencies
中依賴babel-cli和babel-plugin-transform-react-jsx這兩個庫,前者提供Babel的命令列功能,後者主要幫我們把jsx轉化成js。scripts
中我們指定了一條命令:complile
,每次當我們在當前目錄下的命令列中敲npm run compile
時,babal就會將我們的index.js
轉化後新建一個compile.js
檔案。
完成後,在命令列中輸入npm install
安裝下依賴。
.babelrc
1 2 3 4 5 6 7 |
{ "plugins": [ ["transform-react-jsx", { "pragma": "h" // default pragma is React.createElement }] ] } |
在babel的配置檔案中,我們指定transform-react-jsx
這個外掛將轉化後的函式名設定為h
。預設的函式名是React.createElement
,我們不依賴react,所以顯然換個自己的名字更合適。這裡不清楚h
是幹什麼的不要緊,等會看到程式碼你就知道了。
index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>VDOM</title> <style> body { margin: 0; font-size: 24; font-family: sans-serif } .list { text-decoration: none } .list .main { color: red } </style> </head> <body> <script src="compiled.js"></script> <div id="app"></div> <script> var app = document.getElementById('app') render(app) </script> </body> </html> |
這個HTML還是很直觀的,類似React,我們有一個根節點id是app。然後我們render函式最終生成的DOM會插入到app這個根節點裡。注意我們引用的compile.js檔案是babel根據等會要寫的index.js檔案自動生成的。
index.js
首先,我們用JSX來編寫“模板”:
1 2 3 4 5 6 7 |
{ "plugins": [ ["transform-react-jsx", { "pragma": "h" // default pragma is React.createElement }] ] } |
接下來,我們要將JSX編譯成js, 也就是hyperscript。我們先用Babel編譯一下,看這段JSX轉成js會是什麼樣子,開啟命令列,輸入npm run compile
,得到的compile.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function view() { return h( "ul", { id: "filmList", className: "list" }, h( "li", { className: "main" }, "Detective Chinatown Vol 2" ), h( "li", null, "Ferdinand" ), h( "li", null, "Paddington 2" ) ); } |
可以看出h
函式接收的引數,第一個引數是node的型別,比如ul
,li
,第二個引數是node的屬性,之後的引數是node的children,假如child又是一個node的話,就會繼續呼叫h
函式。
清楚了Babel會將我們的JSX編譯成什麼樣子後,接下來我們就可以繼續在index.js中來寫h
函式了。
1 2 3 4 5 6 7 8 9 10 11 |
function flatten(arr) { return [].concat(...arr) } function h(type, props, ...children) { return { type, props: props || {}, children: flatten(children) } } |
我們的h
函式主要的工作就是返回我們真正需要的hyperscript物件,只有三個引數,第一個引數是節點型別,第二個引數是屬性物件,第三個是子節點的陣列。
這裡主要用了ES6的rest, spread引數,不清楚程式碼中兩個...
分別是什麼意思的可以先去看我的介紹ES6文章30分鐘掌握ES6/ES2015核心內容(上)。簡單來說,rest就是上面的...children
,它將函式多餘的引數放到一個陣列裡,所以children此時變成了一個陣列。而spread則是rest的逆運算,也就是上面的...arr
,它將一個陣列轉為用逗號分隔的引數序列。
flatten(children)
這個操作是因為children這個陣列裡的元素有可能也是個陣列,那樣就成了一個二維陣列,所以我們需要將陣列拍平成一維陣列。[].concat(...arr)
是ES6寫法,傳統的寫法是[].concat.apply([], arr)
我們現在可以先來看一下h
函式最終返回的物件長什麼樣子。
1 2 3 |
function render() { console.log(view()) } |
我們在render函式中列印出執行完view()的結果,再npm run compile後,用瀏覽器開啟我們的index.html,看控制檯輸出的結果。
可以,很完美!這個物件就是我們的VDOM了!
下面我們就可以根據VDOM, 來渲染真實DOM了。先改寫render函式:
1 2 3 |
function render(el) { el.appendChild(createElement(view(0))) } |
createElement函式生成DOM,然後再插入到我們在index.html中寫的根節點app。注意render函式式在index.html中被呼叫的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
function createElement(node) { if (typeof(node) === 'string') { return document.createTextNode(node) } let { type, props, children } = node const el = document.createElement(type) setProps(el, props) children.map(createElement) .forEach(el.appendChild.bind(el)) return el } function setProp(target, name, value) { if (name === 'className') { return target.setAttribute('class', value) } target.setAttribute(name, value) } function setProps(target, props) { Object.keys(props).forEach(key => { setProp(target, key, props[key]) }) } |
我們來仔細看下createElement函式。假如說node,即VDOM的型別是文字,我們直接返回一個建立好的文字節點。否則的話,我們取出node中型別,屬性和子節點, 先根據型別建立相應的目標節點,然後再呼叫setProps
函式依次設定好目標節點的屬性,最後遍歷子節點,遞迴呼叫createElement方法,將返回的子節點插入到剛剛建立的目標節點裡。最後返回這個目標節點。
還需要注意的一點是,jsx中class的寫成了className,所以我需要特殊處理一下。
大功告成,complie後瀏覽器開啟index.html看看結果吧。
今天我們成功的完成了藍圖的左半部分,將JSX轉化成hyperscript,再轉化成VDOM,最後根據VDOM生成DOM,渲染到頁面。明天,我們迎接挑戰,開始處理資料變動引起的重新渲染,我們要如何DIFF新舊VDOM,生成補丁,修改DOM。