【React進階系列】從零開始手把手教你實現一個Virtual DOM(二)node
上集咱們實現了首次渲染從JSX=>Hyperscript=>VDOM=>DOM的過程,今天咱們來看一下當數據變更的時候怎麼更新DOM,也就是下圖的右半邊部分。react
function view(count) { const r = [...Array(count).keys()] return <ul id="filmList" className={`list-${count % 3}`}> { r.map(n => <li>item {(count * n).toString()}</li>) } </ul> }
咱們的view函數接收一個參數count,變量r表示從0到count-1的一個數組。假如count=3, r=[0, 1, 2]。ul的className的值有三種可能:list-0, list-1, list-2。li的數量取決於count。npm
function render(el) { const initialCount = 0 el.appendChild(createElement(view(initialCount))) setTimeout(() => tick(el, initialCount), 1000) } function tick(el, count) { const patches = diff(view(count + 1), view(count)) patch(el, patches) if(count > 5) { return } setTimeout(() => tick(el, count + 1), 1000) }
render函數有兩個修改,首先調用view()的時候傳入count=0。其次,寫了一個定時器,1秒後悔執行tick函數。tick函數接收兩個參數,el表明節點元素,count是當前計數值。segmentfault
tick函數依次作了這幾件事:數組
下面咱們來實現diff函數和patch函數。瀏覽器
咱們先列出來新舊兩個VDOM對比,會有哪些不一樣。在index.js文件的最前面聲明一下幾個常量。app
const CREATE = 'CREATE' //新增一個節點 const REMOVE = 'REMOVE' //刪除原節點 const REPLACE = 'REPLACE' //替換原節點 const UPDATE = 'UPDATE' //檢查屬性或子節點是否有變化 const SET_PROP = 'SET_PROP' //新增或替換屬性 const REMOVE_PROP = 'REMOVE PROP' //刪除屬性
function diff(newNode, oldNode) { if (!oldNode) { return { type: CREATE, newNode } } if (!newNode) { return { type: REMOVE } } if (changed(newNode, oldNode)) { return { type: REPLACE, newNode } } if (newNode.type) { return { type: UPDATE, props: diffProps(newNode, oldNode), children: diffChildren(newNode, oldNode) } } }
下面咱們一次看一下changed, diffProps, diffChildren函數。函數
function changed(node1, node2) { return typeof(node1) !== typeof(node2) || typeof(node1) === 'string' && node1 !== node2 || node1.type !== node2.type }
檢查新舊VDOM是否有變更的方法很簡單,spa
function diffProps(newNode, oldNode) { let patches = [] let props = Object.assign({}, newNode.props, oldNode.props) Object.keys(props).forEach(key => { const newVal = newNode.props[key] const oldVal = oldNode.props[key] if (!newVal) { patches.push({type: REMOVE_PROP, key, value: oldVal}) } if (!oldVal || newVal !== oldVal) { patches.push({ type: SET_PROP, key, value: newVal}) } }) return patches }
比較新舊VDOM的屬性的變化,並返回相應的patches。code
function diffChildren(newNode, oldNode) { let patches = [] const maximumLength = Math.max( newNode.children.length, oldNode.children.length ) for(let i = 0; i < maximumLength; i++) { patches[i] = diff( newNode.children[i], oldNode.children[i] ) } return patches }
一樣採用最大可能性原則,取新舊VDOM的children的最長值做爲遍歷children的長度。而後依次比較新舊VDOM的在相同INDEX下的每個child。
這裏須要強烈注意一下
爲了簡化,咱們沒有引入key的概念,直接比較的是相同index下的child。因此假如說一個列表ul有5項,分別是li1, li2, li3, li4, li5; 若是咱們刪掉了第一項,新的變成了li2, li3, li4, li5。那麼diffchildren的時候,咱們會拿li1和li2比較,依次類推。這樣一來,原本只是刪除了li1, 而li2, li3, li4, li5沒有任何變化,咱們得出的diff結論倒是[li替換,li2替換, li3替換, li4替換, li5刪除]。因此react讓你們渲染列表的時候,必須添加Key。
截止到如今,咱們已經獲得了咱們須要的補丁。下面咱們要將補丁Patch到DOM裏。
function patch(parent, patches, index = 0) { if (!patches) { return } const el = parent.childNodes[index] switch (patches.type) { case CREATE: { const { newNode } = patches const newEl = createElement(newNode) parent.appendChild(newEl) break } case REMOVE: { parent.removeChild(el) break } case REPLACE: { const {newNode} = patches const newEl = createElement(newNode) return parent.replaceChild(newEl, el) break } case UPDATE: { const {props, children} = patches patchProps(el, props) for(let i = 0; i < children.length; i++) { patch(el, children[i], i) } } } }
最後咱們再補充一下patchProps函數
function patchProps(parent, patches) { patches.forEach(patch => { const { type, key, value } = patch if (type === 'SET_PROP') { setProp(parent, key, value) } if (type === 'REMOVE_PROP') { removeProp(parent, key, value) } }) } function removeProp(target, name, value) { //@ if (name === 'className') { return target.removeAttribute('class') } target.removeAttribute(name) }
這個就不用我解釋了,代碼很直觀,setProp函數在上一集咱們已經定義過了。這樣一來,咱們就完成了整個數據更新致使DOM更新的完整過程。
npm run compile後打開瀏覽器查看效果,你應該看到是一個背景顏色在不一樣變化,同時列表項在逐漸增長的列表。
至此,咱們的VDOM就所有完成了。系列初我提出的那幾個問題不知道你如今是否有了答案。有答案的童鞋能夠在文章評論區將你的看法跟你們分享一下。分析全面且準確的會收到個人特殊獎勵。😁😁😁😁