How to wirte your own Virtual DOM(譯)

前言

React在處理界面更新的時候,是經過對比虛擬DOM(js對象)之間的差別來更新UI的.這種方式能必定程度上的減小對DOM的操做.經過虛擬DOM這個中間層結合多平臺的renderer使React實現了跨平臺.本文梳理了在medium上關於Virtual DOM的兩篇文章來介紹如何簡單實現一個Virtual DOM.前端

爲何虛擬DOM

  1. UI = F(data) Virtual DOM使數據,操做,屬性能夠集中在一塊兒,這種方式能必定程度上下降項目長期維護的複雜性.
  2. 頁面性能 經過虛擬DOM的對比,進行差別的更新能提高頁面的性能.

如何實現虛擬DOM

虛擬DOM能夠理解是真實DOM的映射,如何實現虛擬DOM主要須要考慮一下幾點:node

  1. 如何描述虛擬DOM(create)
  2. 如何繪製虛擬DOM(render)
  3. 如何差別化的更新虛擬DOM而且更新UI(update)

下面主要從上面的三點來逐步實現一個簡易版的虛擬DOM實現.git

建立Virtual DOM

DOM的節點能夠經過type(節點類型),props(styles, event), children(子元素)來描述.能夠經過下面的函數來建立虛擬節點.github

// 建立虛擬節點
function h(type, props, children) {
  return { type, props: props || [], children: children || [] };
}
const root = h('ul', { name: 100, onClick: () => { console.log(1); } }, [
  h('li', {}, ['sss'])
])
複製代碼

virtualDom

繪製Virtual DOM

在下面的實現中,以$開頭的元素指代真實的DOM節點,node指代虛擬節點.算法

繪製元素

在繪製元素的時候,若是節點的內容是文本,就直接建立文本節點.不然就建立當前類型的DOM節點而且遍歷它的children節點遞歸的調用自身而且添加到建立的節點.性能優化

function createElement(node) {
  if(typeof node === 'string') {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children.map(createElement).forEach($el.appendChild.bind($el));
  return $el;
}
const root = h("ul", { name: '111', className: 'test' }, [
  h('li', { name: 'child' }, ['text'])
]);
// app是頁面中已經存在的容器節點
const container = document.getElementById('app');
container.appendChild(createElement(root)); // 已經能繪製到頁面
複製代碼

添加屬性

在添加屬性的時候,有如下的節點須要注意:bash

  • 對DOM節點上不存在的屬性名字進行轉換,例如ClassName
  • 布爾屬性值的設置
  • 增長屬性過濾功能來實現特有的實現
實現
//設置布爾屬性
function setBooleanProp($target, name, value) {
  if(value) {
    $target.setAttribute(name, value);
    $target[name] = value;
  } else {
    $target[name] = false;
  }
}
// 屬性過濾
function isCustomProp(name) {
  return false;
}
// 設置全部屬性的入口
function setProps($target, props) {
  Object.keys(props).forEach(name => {
    setProp($target, name, props[name]);
  });
}
// 對單一屬性的設置,實現過濾,轉換
function setProp($target, name, value) {
  if(isCustomProp(name)) {
    return;
  } else if(name === 'className') {
    $target.setAttribute('class', value);
  } else if(typeof value === 'boolean') {
    setBooleanProp(name, value);
  } else {
    $target.setAttribute(name, value);
  }
}
複製代碼

經過將設置屬性的操做加入到以前的createElement函數中,來實現DOM屬性的添加.微信

function createElement(node) {
  if(typeof node === 'string') {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  setProps($el, node.props);
  node.children.map(createElement).forEach($el.appendChild.bind($el));
  return $el;
} 
複製代碼

經過運行以前的代碼,發現屬性已經添加到DOM中了.
domWithPropapp

添加事件

在對事件的添加上,因爲具體的事件也是在建立Virtual DOM的時候添加到props的,若是不想經過以前聲明的setProps函數進行事件的處理,就須要將這些屬性過濾出來,具體實現以下:dom

//判斷是不是event屬性
function isEventProp(name) {
  return /^on/.test(name);
}
// 獲取屬性的後綴  例如 onClick => click
function extractEventName(name) {
  return name.slice(2).toLowerCase();
}
// 修改以前的過濾屬性函數,加入對event屬性的過濾
function isCustomProp(name) {
  return isEventProp(name)
}
// 添加屬性函數
function addEventListeners($target, props) {
  Object.keys(props).forEach(name => {
    if(isEventProp(name)) {
      $target.addEventListener(extractEventName(name), props[name]);
    }
  })
}
複製代碼

將添加事件的函數增長到createElement函數中,完成對事件的添加.

function createElement(node) {
  if(typeof node === 'string') {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  setProps($el, node.props);
  addEventListeners($el, node.props);
  node.children.map(createElement).forEach($el.appendChild.bind($el));
  return $el;
}
複製代碼

對比差別更新UI

若是徹底的對比兩個樹形結構的差別,時間複雜度是O(n^3)的.爲了必定的性能優化,能夠有如下的假設:

  1. 節點的類型變動,兩個DOM的結構就是不一樣的.這種狀況能夠直接進行替換操做.
  2. 不多存在跨層級的節點移動
  3. 同一類型的節點的DOM結構是相同的

經過對上面假設的分析,在更新Virtual DOM的時候,主要有如下幾種狀況

  1. 對比兩個node的類型不一樣,直接替換
  2. 最新的node中沒有元素和屬性,須要刪除對應的節點的屬性
  3. 最新的node中增長了元素和屬性,須要添加對應的屬性和節點
  4. 節點類型相同,對子節點實現1 2 3的操做

更新節點

// 判斷兩個node是不是同一個節點
function changed(node1, node2) {
  return typeof node1 !== typeof node2 || typeof node1 === 'string' && node1 !== node2 || node1.type !== node2.type;
}

function updateElement($parent, newNode, oldNode, index = 0) {
  // index是子元素的位置
  if(!oldNode) {
    // 若是不存在oldNode進行添加操做
    $parent.appendChild(createElement(newNode));
  } else if(!newNode) {
    // 在新的節點中不村存在移除
    $parent.removeChild($parent.childNodes[index]);
  } else if(changed(newNode, oldNode)) {
    // 若是節點類型改變進行替換
    $parent.replaceChild(
      createElement(newNode), $parent.childNodes[index]
    )
  } else if(newNode.type) {
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    // 更新子節點
    for(let i = 0; i < newLength || i < oldLength; i++) {
      updateElement($parent.childNodes[index], newNode.children[i], oldNode.children[i], i)
    }
  }
}
複製代碼

更新屬性

在更新屬性的時候跟更新節點的步驟相似

// 刪除布爾屬性
function removeBooleanProp($target, name) {
  $target.removeAttribute(name);
  $target[name] = false;
}
// 移除屬性
function removeProp($target, name, value) {
  if(isCustomProp(name)) {
    return;
  } else if(name === 'className') {
    $target.removeAttribute('class');
  } else if(typeof value === 'boolean') {
    removeBooleanProp($target, name);
  } else {
    $target.removeAttribute(name);
  }
}
// 當不存在newVal的時候,remove對應的屬性.其餘狀況進行覆蓋
function updateProp($target, name, newVal, oldVal) {
  if(!newVal) {
    removeProp($target, name, oldVal);
  } else {
    setProp($target, name, newVal);
  }
}
function updateProps($target, newProps, oldProps = {}) {
  const props = Object.assign({}, newProps, oldProps);
  Object.keys(props).forEach(name => {
    updateProp($target, name, newProps[name], oldProps[name]);
  });
}
function updateElement($parent, newNode, oldNode, index = 0) {
  if(!oldNode) {
    // 若是不存在oldNode進行添加操做
    $parent.appendChild(createElement(newNode));
  } else if(!newNode) {
    // 在新的節點中不村存在移除
    $parent.removeChild($parent.childNodes[index]);
  } else if(changed(newNode, oldNode)) {
    // 若是節點類型改變進行替換
    $parent.replaceChild(
      createElement(newNode), $parent.childNodes[index]
    )
  } else if(newNode.type) {
    // 增長屬性的更新
    updateProps($parent.childNodes[index], newNode.props, oldNode.props)
    const newLength = newNode.children.length;
    const oldLength = oldNode.children.length;
    // 更新子節點
    for(let i = 0; i < newLength || i < oldLength; i++) {
      updateElement($parent.childNodes[index], newNode.children[i], oldNode.children[i], i)
    }
  }
}
複製代碼

更新事件

函數是很差判斷是否有變化的,能夠經過一些參數來完成事件的更新(觸發從新更新,經過節點替換來完成事件的更新這樣很差)

function changed(node1, node2) {
  return typeof node1 !== typeof node2 ||
      typeof node1 === ‘string’ && node1 !== node2 ||
      node1.type !== node2.type ||
      node1.props.forceUpdate;
}
function isCustomProp(name) {
  return isEventProp(name) || name === ‘forceUpdate’;
}
複製代碼

參考

How to write your own Virtual DOM

Write your Virtual DOM 2: Props & Events
深度剖析:如何實現一個 Virtual DOM 算法

                                       前端小板凳
                                       歡迎你們關注個人微信公衆號,一塊兒學習
相關文章
相關標籤/搜索