從零開始一步一步寫一個簡單的Virtual DOM實現 2 :Props&Event處理

第一篇地址html

第二部分原文:write-your-virtual-dom-2-props-events前端

完整的示例代碼地址node

Props

首先咱們要回顧下前文講的一個有些誤差的小點,假設咱們在JSX中只寫一個最簡單的Div:git

<div></div>

Babel會自動將該JSX轉化爲以下的DOM表達式:github

{ type: ‘’, props: null, children: [] }

注意,這裏的props默認是null,咱們在以前的文章中並無關注到這個屬性,而本部分則是要講解Virtual DOM中Props的用法。通常來講,不管在哪一種編程環境下都要儘可能避免Null的出現,所以咱們首先來改造下h函數,使得其可以默認返回一個空的Object,而不是Null:web

function h(type, props, …children) {
 return { type, props: props || {}, children };
}

Setting Props:設置Props

接觸過React的同窗對於Props確定不會陌生,而設置Props也就跟使用普通的HTML標籤屬性很相似:算法

<ul className=」list」 style=」list-style: none;」></ul>

而最終會轉化爲以下的表達式:編程

{ 
 type: ‘ul’, 
 props: { className: ‘list’, style: ’list-style: none;’ } 
 children: []
}

props對象中的每一個鍵即爲屬性名,而值爲屬性值,通常來講咱們只須要簡單的調用一個setAttribute方法來說這個Props中的鍵值對設置到DOM元素上便可:segmentfault

function setProp($target, name, value) {
 $target.setAttribute(name, value);
}

這個函數用於將單個的Prop值設置到DOM元素上,而對於props對象,咱們要作的就是依次遍歷:app

function setProps($target, props) {
 Object.keys(props).forEach(name => {
   setProp($target, name, props[name]);
 });
}

你應該還記得那個用於建立元素的createElement方法吧,咱們須要將setProps方法放置到元素成功建立以後:

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;
}

不要急,這還遠遠不夠。React的初學教程中一直強調className與class的區別,在咱們的setProps中也須要對於這些JS的保留字作一個替換,譬如:

<nav className=」navbar light」>

 <ul></ul>
</nav>

另外,還有比較常見的就是對於DOM的布爾屬性,譬如checked、disabled等等的處理:

<input type=」checkbox」 checked={false} />

在真實的DOM節點上,若是是出現了false的狀況,咱們並不但願checked屬性會出現,那麼咱們的Props函數就要能智能地進行判斷:

function setBooleanProp($target, name, value) {
 if (value) {
   $target.setAttribute(name, value);
   $target[name] = true;
 } else {
   $target[name] = false;
 }
}

最後呢,要作的就是對於自定義的,即非標準的HTML屬性進行一個過濾,這些屬性只應該出如今JS對象上,而不該該出如今真實的DOM對象上:

function isCustomProp(name) {
 return false;
}
function setProp($target, name, value) {
 if (isCustomProp(name)) {
   return;
 } else if (name === ‘className’) {
   $target.setAttribute(‘class’, value);
 } else if (typeof value === ‘boolean’) {
   setBooleanProp($target, name, value);
 } else {
   $target.setAttribute(name, value);
 }
}

總結一下,本部分完整的JSX代碼爲:

/** @jsx h */

function h(type, props, ...children) {
  return { type, props: props || {}, children };
}

function setBooleanProp($target, name, value) {
  if (value) {
    $target.setAttribute(name, value);
    $target[name] = true;
  } else {
    $target[name] = false;
  }
}

function isCustomProp(name) {
  return false;
}

function setProp($target, name, value) {
  if (isCustomProp(name)) {
    return;
  } else if (name === 'className') {
    $target.setAttribute('class', value);
  } else if (typeof value === 'boolean') {
    setBooleanProp($target, name, value);
  } else {
    $target.setAttribute(name, value);
  }
}

function setProps($target, props) {
  Object.keys(props).forEach(name => {
    setProp($target, name, props[name]);
  });
}

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;
}

//--------------------------------------------------

const f = (
  <ul style="list-style: none;">
    <li className="item">item 1</li>
    <li className="item">
      <input type="checkbox" checked={true} />
      <input type="text" disabled={false} />
    </li>
  </ul>
);

const $root = document.getElementById('root');
$root.appendChild(createElement(f));

Diffing Props:Props變化比較

如今咱們已經建立了帶有Props屬性的元素,下一個須要考慮的就是應該如何應用到咱們上文提到的Diff算法中。首先咱們要來看下如何從真實的DOM中移除某些Props:

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);
 }
}

而後咱們須要寫一個updateProp函數,來根據新舊節點的Props的變化進行恰當的真實DOM節點的修改,共有如下幾種狀況:

  • 新節點移除了某個舊節點的Prop

  • 新節點添加了某個舊節點沒有的Prop

  • 新舊節點的某個Prop的值發生了變化

根據以上規則,咱們可知更新Prop的函數爲:

function updateProp($target, name, newVal, oldVal) {
 if (!newVal) {
   removeProp($target, name, oldVal);
 } else if (!oldVal || newVal !== oldVal) {
   setProp($target, name, newVal);
 }
}

能夠看出,更新單個Prop的函數仍是很是簡單的,就是將移除與設置結合起來使用,那麼咱們擴展到Props,就獲得以下的函數:

function updateProps($target, newProps, oldProps = {}) {

  const props = Object.assign({}, newProps, oldProps);

  Object.keys(props).forEach(name => {

    updateProp($target, name, newProps[name], oldProps[name]);

  });

}

一樣地,咱們須要將該更新函數添加到updateElement函數中:

function updateElement($parent, newNode, oldNode, index = 0) {

  ...

  } else if (newNode.type) {

    updateProps(

      $parent.childNodes[index],

      newNode.props,

      oldNode.props

    );

 

    ...

  }

}

Events

用戶交互是任何一個應用不可或缺的部分,而在這裏咱們討論下如何爲Virtual DOM添加事件處理的能力,React大概會這麼作:

<button onClick={() => alert(‘hi!’)}></button>

能夠看出,設置一個事件處理器就是添加一個Prop,只不過名稱會以on開始,那麼咱們能夠用以下函數來判斷某個Prop是否與事件相關:

function isEventProp(name) {
 return /^on/.test(name);
}

判斷是事件類型以後,咱們能夠提取出事件名:

function extractEventName(name) {
 return name.slice(2).toLowerCase();
}

看到這裏,估計你會考慮直接將事件處理也放到setProps與updateProps函數中,不過這邊就會存在一個問題,在diffProps的時候,你很難去比較兩個function:

所以咱們將全部的事件類型的Props認爲是自定義的Props,這樣咱們上面提到的isCustomProp就起做用了:

function isCustomProp(name) {
 return isEventProp(name);
}

而把事件響應函數綁定到真實的DOM節點也很簡單:

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;
}

Re-Adding Events:從新設置了事件響應

在這裏咱們暫時不考慮地很複雜,即不深刻地比較那些事件類型的Prop發生變化的狀況,做爲替代的,咱們引入一個forceUpdate屬性,即強制整個DOM進行更新:

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

最後,本文完整的JSX爲:

/** @jsx h */

function h(type, props, ...children) {
  return { type, props: props || {}, children };
}

function setBooleanProp($target, name, value) {
  if (value) {
    $target.setAttribute(name, value);
    $target[name] = true;
  } else {
    $target[name] = false;
  }
}

function removeBooleanProp($target, name) {
  $target.removeAttribute(name);
  $target[name] = false;
}

function isEventProp(name) {
  return /^on/.test(name);
}

function extractEventName(name) {
  return name.slice(2).toLowerCase();
}

function isCustomProp(name) {
  return isEventProp(name) || name === 'forceUpdate';
}

function setProp($target, name, value) {
  if (isCustomProp(name)) {
    return;
  } else if (name === 'className') {
    $target.setAttribute('class', value);
  } else if (typeof value === 'boolean') {
    setBooleanProp($target, name, value);
  } else {
    $target.setAttribute(name, value);
  }
}

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);
  }
}

function setProps($target, props) {
  Object.keys(props).forEach(name => {
    setProp($target, name, props[name]);
  });
}

function updateProp($target, name, newVal, oldVal) {
  if (!newVal) {
    removeProp($target, name, oldVal);
  } else if (!oldVal || newVal !== oldVal) {
    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 addEventListeners($target, props) {
  Object.keys(props).forEach(name => {
    if (isEventProp(name)) {
      $target.addEventListener(
        extractEventName(name),
        props[name]
      );
    }
  });
}

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;
}

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

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!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 log(e) {
  console.log(e.target.value);
}

const f = (
  <ul style="list-style: none;">
    <li className="item" onClick={() => alert('hi!')}>item 1</li>
    <li className="item">
      <input type="checkbox" checked={true} />
      <input type="text" onInput={log} />
    </li>
    {/* this node will always be updated */}
    <li forceUpdate={true}>text</li>
  </ul>
);

const g = (
  <ul style="list-style: none;">
    <li className="item item2" onClick={() => alert('hi!')}>item 1</li>
    <li style="background: red;">
      <input type="checkbox" checked={false} />
      <input type="text" onInput={log} />
    </li>
    {/* this node will always be updated */}
    <li forceUpdate={true}>text</li>
  </ul>
);

const $root = document.getElementById('root');
const $reload = document.getElementById('reload');

updateElement($root, f);
$reload.addEventListener('click', () => {
  updateElement($root, g, f);
});

到這裏咱們就完成了一個最簡單的Virtual DOM算法,不過其與真正可以投入實戰的Virtual DOM算法仍是有很大距離,進一步閱讀推薦:

相關文章
相關標籤/搜索