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

上集回憶

【React進階系列】從零最先手把手教你完成一個Virtual DOM(二)

上集我們完成了初次襯着從JSX=>Hyperscript=>VDOM=>DOM的歷程,本日我們來看一下當數據更改的時刻怎樣更新DOM,也就是下圖的右半邊部份。
《【React進階系列】從零開始手把手教你完成一個Virtual DOM(三)》

改寫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。

改寫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是當前計數值。

tick函數順次做了這幾件事:

  1. 挪用diff函數,對照新舊兩個VDOM,依據二者的差別獲得須要修正的補丁
  2. 將補丁patch到實在DOM上
  3. 當計數器小於即是5的時刻,將count加1,再繼承下一次tick
  4. 當計數器大於5的時刻,完畢

下面我們來完成diff函數和patch函數。

我們先列出來新舊兩個VDOM對照,會有哪些差別。在index.js文件的最前面聲明一下幾個常量。

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是不是有更改的要領很簡單,

  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。

  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就悉數完成了。系列初我提出的那幾個題目不知道你如今是不是有了答案。有答案的童鞋能夠在文章批評區將你的看法跟人人分享一下。剖析周全且正確的會收到我的特別嘉獎。😁😁😁😁

    原文作者:zach5078
    原文地址: https://segmentfault.com/a/1190000014641724
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞