【React進階系列】從零開始手把手教你實現一個Virtual DOM(三)

上集回顧

【React進階系列】從零開始手把手教你實現一個Virtual DOM(二)node

上集咱們實現了首次渲染從JSX=>Hyperscript=>VDOM=>DOM的過程,今天咱們來看一下當數據變更的時候怎麼更新DOM,也就是下圖的右半邊部分。
圖片描述react

改寫view()

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

改寫render()

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函數依次作了這幾件事:數組

  1. 調用diff函數,對比新舊兩個VDOM,根據二者的不一樣獲得須要修改的補丁
  2. 將補丁patch到真實DOM上
  3. 當計數器小於等於5的時候,將count加1,再繼續下一次tick
  4. 當計數器大於5的時候,結束

下面咱們來實現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'  //刪除屬性

diff()

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)
     }
   }
}
  1. 假如舊節點不存在,咱們返回的patches對象, 類型爲新增節點;
  2. 假如新節點不存在,表示是刪除節點;
  3. 假如二者都存在的話,調用changed函數判斷他們是否是有變更;
  4. 假如二者都存在,且changed()返回false的話,判斷新節點是不是VDOM(根據type是否存在來判斷的,由於type不存在的話,newNode要麼是空節點,要麼是字符串)。假如新節點是VDOM,則返回一個patches對象,類型是UPDATE,同時對props和children分別進行diffProps和diffChildren操做。

下面咱們一次看一下changed, diffProps, diffChildren函數。函數

changed()

function changed(node1, node2) {
  return typeof(node1) !== typeof(node2) ||
         typeof(node1) === 'string' && node1 !== node2 ||
         node1.type !== node2.type
}

檢查新舊VDOM是否有變更的方法很簡單,spa

  1. 首先假如數據類型都不同,那確定是變更了;
  2. 其次假如二者的類型都是純文本,則直接比較二者是否相等;
  3. 最後比較二者的類型是否相等。

diffProps()

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

  1. 首先咱們採用最大可能性原則,將新舊VDOM的全部屬性都合併賦值給一個新的變量props
  2. 遍歷props變量的全部Keys,依次比較新舊VDOM對於這個KEY的值
  3. 假如新值不存在,表示這個屬性被刪除了
  4. 假如舊值不存在,或者新舊值不一樣,則表示咱們須要從新設置這個屬性

diffChildren()

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裏。

patch()

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)
      }
    }
  }
}
  1. 首先當patches不存在時,直接return,不進行任何操做
  2. 利用childNodes和Index取出當前正在處理的這個節點,賦值爲el
  3. 開始判斷補丁的類型
  4. 當類型是CREATE時,生成一個新節點,並append到根節點
  5. 當類型是REMOVE時,直接刪除當前節點el
  6. 當類型是REPLACE時,生成新節點,同時替換掉原節點
  7. 當類型是UPDATE時,須要咱們特殊處理
  8. 調用patchProps將咱們以前diffProps獲得的補丁渲染到節點上
  9. 遍歷以前diffChildren獲得的補丁列表,再依次遞歸調用patch

最後咱們再補充一下patchProps函數

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就所有完成了。系列初我提出的那幾個問題不知道你如今是否有了答案。有答案的童鞋能夠在文章評論區將你的看法跟你們分享一下。分析全面且準確的會收到個人特殊獎勵。😁😁😁😁

相關文章
相關標籤/搜索