從零開始手把手教你實現一個Virtual DOM(一)
上一集咱們介紹了什麼是VDOM,爲何要用VDOM,以及咱們要怎樣來實現一個VDOM。咱們再來看一下這張藍圖,今天咱們要實現的是這張圖的左半部分。html
{ "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" } }
這裏主要主要兩點:node
devDependencies
中依賴babel-cli和babel-plugin-transform-react-jsx這兩個庫,前者提供Babel的命令行功能,後者主要幫咱們把jsx轉化成js。scripts
中咱們指定了一條命令:complile
,每次當咱們在當前目錄下的命令行中敲npm run compile
時,babal就會將咱們的index.js
轉化後新建一個compile.js
文件。完成後,在命令行中輸入npm install
安裝下依賴。react
{ "plugins": [ ["transform-react-jsx", { "pragma": "h" // default pragma is React.createElement }] ] }
在babel的配置文件中,咱們指定transform-react-jsx
這個插件將轉化後的函數名設置爲h
。默認的函數名是React.createElement
,咱們不依賴react,因此顯然換個本身的名字更合適。這裏不清楚h
是幹什麼的沒關係,等會看到代碼你就知道了。npm
<!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文件自動生成的。json
首先,咱們用JSX來編寫「模板」:segmentfault
function view() { return <ul id="filmList" className="list"> <li className="main">Detective Chinatown Vol 2</li> <li>Ferdinand</li> <li>Paddington 2</li> </ul> }
接下來,咱們要將JSX編譯成js, 也就是hyperscript。咱們先用Babel編譯一下,看這段JSX轉成js會是什麼樣子,打開命令行,輸入npm run compile
,獲得的compile.js:數組
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
函數了。babel
function flatten(arr) { return [].concat(...arr) } function h(type, props, ...children) { return { type, props: props || {}, children: flatten(children) } }
咱們的h
函數主要的工做就是返回咱們真正須要的hyperscript對象,只有三個參數,第一個參數是節點類型,第二個參數是屬性對象,第三個是子節點的數組。app
這裏主要用了ES6的rest, spread參數,不清楚代碼中兩個...
分別是什麼意思的能夠先去看個人介紹ES6文章30分鐘掌握ES6/ES2015核心內容(上)。簡單來講,rest就是上面的...children
,它將函數多餘的參數放到一個數組裏,因此children此時變成了一個數組。而spread則是rest的逆運算,也就是上面的...arr
,它將一個數組轉爲用逗號分隔的參數序列。
flatten(children)
這個操做是由於children這個數組裏的元素有可能也是個數組,那樣就成了一個二維數組,因此咱們須要將數組拍平成一維數組。[].concat(...arr)
是ES6寫法,傳統的寫法是[].concat.apply([], arr)
咱們如今能夠先來看一下h
函數最終返回的對象長什麼樣子。
function render() { console.log(view()) }
咱們在render函數中打印出執行完view()的結果,再npm run compile後,用瀏覽器打開咱們的index.html,看控制檯輸出的結果。
能夠,很完美!這個對象就是咱們的VDOM了!
下面咱們就能夠根據VDOM, 來渲染真實DOM了。先改寫render函數:
function render(el) { el.appendChild(createElement(view(0))) }
createElement函數生成DOM,而後再插入到咱們在index.html中寫的根節點app。注意render函數式在index.html中被調用的。
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。