一步一步帶你實現virtual dom(一)

一步一步帶你實現virtual dom(一)
一步一步帶你實現virtual dom(二)--Props和事件html

要寫你本身的虛擬DOM,有兩件事你必須知道。你甚至都不用翻看React的源代碼,或者其餘的基於虛擬DOM的代碼。他們代碼量都太大,太複雜。然而要實現一個虛擬DOM的主要部分只須要大約50行的代碼。50行代碼!!node

下面就是那兩個你要知道的事情:react

  • 虛擬DOM和真實DOM的有某種對應關係
  • 咱們在虛擬DOM樹的更改會生成另一個虛擬DOM樹。咱們會用一種算法來比較兩個樹有哪些不一樣,而後對真實的DOM作最小的更改。

下面咱們就來看看這兩條是如何實現的。web

生成虛擬DOM樹

首先咱們須要在內存裏存儲咱們的DOM樹。只要使用js就能夠達到這個目的。假設咱們有這樣的一個樹:算法

<ul class="list">
  <li>item 1</li>
  <li>item 2</li>
</ur>

看起來很是簡單對吧。咱們怎麼用js的對象來對應到這個樹呢?babel

{ type: 'ul', props: {'class': 'list}, children: [
  {type: 'li', props: {}, children: ['item 1']},
  {type: 'li', props: {}, children: ['item 2']}
]}

這裏咱們會注意到兩件事:app

  • 咱們使用這樣的對象來對應到真實的DOM上:{type: '...', props: {...}, children: [...]}
  • DOM的文本節點會對應到js的字符串上。
    可是若是用這個方法來對應到巨大的DOM樹的話那將是很是困難的。因此咱們來寫一個helper方法,這樣結構上也就容易理解一些:
function h(type, props, ...children) {
  return {type, props, children};
}

如今咱們能夠這樣生成一個虛擬DOM樹:dom

h('ul', {'class': 'list'},
  h('li', {}, 'item 1'),
  h('li', {}, 'item 2'),
)

這樣看起來就清晰了不少。可是咱們還能夠作的更好。你應該據說過JSX對吧。是的,咱們也要用那種方式。可是,這個應該如何下手呢?spa

若是你讀過Babel的JSX文檔的話,你就會知道這些都是Babel的功勞。Babel會把下面的代碼轉碼:.net

<ul className="list">
  <li>item 1</li>
  <li>item 2</li>
</ul>

轉碼爲:

React.createElement('ul', {className: 'list'}),
 React.createElement('li', {}, 'item 1'),
 React.createElement('li', {}, 'item 2')
);

你注意到多類似了嗎?若是把React.createElement(...)體換成咱們本身的h方法的話,那咱們也已使用相似於JSX的語法。咱們只須要在咱們的文件最頂端加這麼一句話:

/** @jsx h */
<ul className="list">
  <li>item 1</li> 
  <li>item 2</li>
</ul>

這一行/** @jsx h */就是在告訴Babel「大兄弟,按照jsx的方式轉碼,可是不要用React.createElement, 使用h。你可使用任意的東西來代替h。

那麼把上面咱們說的總結一下,咱們會這樣寫咱們的虛擬DOM:

/** @jsx h */
const a = {
  <ul className="list">
    <li>item 1</li>
    <li>item 2</li>
  </ul>
};

而後Babel就會轉碼成這樣:

const a = {
  h('ul', {className: 'list'},
    h('li', {}, 'item 1'),
    h('li', {}, 'item 2'),
  )
};

當方法h執行的時候,它就會返回js的對象--咱們的虛擬DOM樹。

const a = (
  { type: ‘ul’, props: { className: ‘list’ }, children: [
    { type: ‘li’, props: {}, children: [‘item 1’] },
    { type: ‘li’, props: {}, children: [‘item 2’] }
  ] }
);

JSFiddle裏運行一下試試

應用咱們的DOM展現

如今咱們的DOM樹用純的JS對象來表明了。很酷了。可是咱們須要根據這些建立實際的DOM。由於咱們不能只是把虛擬節點轉換後直接加載DOM裏。

首先咱們來定義一些假設和一些術語:

  • 實際的DOM都會使用$開頭的變量來表示。因此$parent是一個實際的DOM。
  • 虛擬DOM使用node變量表示
  • 和React同樣,你只能夠有一個根節點。其餘的節點都在某個根節點裏。

咱們來寫一個方法:createElement(),這個方法能夠接收一個虛擬節點以後返回一個真實的DOM節點。先不考慮propschildren,這個以後會有介紹。

function createElement(node) {
  if(typeof node === 'string') {
    return document.createTextNode(node);
  }
  return document.createElement(node.type);
}

由於咱們不只須要處理文本節點(js的字符串),還要處理各類元素(element)。這些元素都是想js的對象同樣的:

{ type: '-', props: {...}, children: [...]}

咱們能夠用這個結構來處理文本節點和各類element了。

那麼子節點如何處理呢,他們也基本是文本節點或者各類元素。這些子節點也能夠用createElement()方法來處理。父節點和子節點都使用這個方法,看到了麼?其實這就是遞歸處理了。咱們能夠調用createElement方法來建立子節點,而後用appendChild方法來把他們添加到根節點上。

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

看起來還不錯,咱們先不考慮節點的props。要理解虛擬節點的概念並不須要這些東西卻會增長不少的複雜度。

處理修改

咱們能夠把虛擬節點轉化爲真實的DOM了。如今該考慮比較咱們的虛擬樹了。基本上咱們須要寫一點算法了。虛擬樹的比較須要用到這個算法,比較以後只作必要的修改。

如何比較樹的不一樣?

  • 若是新節點的子節點增長了,那麼咱們就須要調用appendChild方法來添加。
//new 
<ul>
  <li>item 1</li>
  <li>item 2</li>
</ul>
 
//old
<ul>
  <li>item 1</li>
</ul>
  • 新節點比舊節點的子節點少,那麼就須要調用removeChild方法來刪除掉多餘的子節點。
//new
<ul>
  <li>item 1</li>
</ul>

//old
<ul>
  <li>item 1</li>
  <li>item 2</li>  // 這個要被刪掉
</ul>
  • 新舊節點的某個子節點不一樣,也就是某個節點上發生了修改。那麼,咱們就調用replaceChild方法。
//new
<div>
  <p>hi there!</p>
  <p>hello</p>
</div>


//old
<div>
  <p>hi there!</p>
  <button>click it</button>   //發生了修改,變成了new裏的<p />節點
</div>
  • 各節點都同樣。那麼咱們就須要作進一步的比較
//new
<ul>
  <li>item 1</li>
  <li> //*
    <span>hello</span>
    <span>hi!</span> 
  </li>
</ul>

//old
<ul>
  <li>item 1</li>
  <li> //*
    <span>hello</span>
    <div>hi!</div>
  </li>
</ul>

加醒的兩個節點能夠看到都是<li>,是相等的。可是它的子節點裏面卻有不一樣的節點。

咱們來寫一個方法updateElement,它接收三個參數:$parentnewNodeoldNode$parent是真的DOM元素。它是咱們虛擬節點的父節點。如今咱們來看看如何處理上面提到的所有問題。

沒有舊節點

這個問題很簡單:

function updateElement($parent, newNode, oldNode) {
  if(!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  }
}

沒有新節點

若是當前沒有新的虛擬節點,咱們就應該把它從真的DOM裏刪除掉。可是,如何作到呢?咱們知道父節點(做爲參數傳入了方法),那麼咱們就能夠調用$parent.removeChild方法,並傳入真DOM的引用。可是咱們沒法獲得它,若是咱們知道的節點在父節點的位置,就能夠用$parent.childNodes[index]來獲取它的引用。index就是節點的位置。

假設index也做爲參數傳入了咱們的方法,咱們的方法就能夠這麼寫:

function updateElement($parent, newNode, oldNode, index = 0) {
  if(!oldNode) {
    $parent.appendChild(
      createElement(newNode);
    );
  } else if(!newNode) {
    $parent.removeChild(
      $parent.childNodes[index];
    );
  }
}

節點改變

首先寫一個方法來比較兩個節點(新的和舊的)來區分節點是否發生了改變。要記住,節點能夠是文本節點,也能夠是元素(element):

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

如今有了當前節點的index了,index就是當前節點在父節點的位置。這樣能夠很容易用新建立的節點來代替當前節點了。

function updateElement($parent, newNode, oldNode, index = 0) {
  if(!oldNode) {
    $parent.appendChild(
      createElement(newNode);
    );
  } else if(!newNode) {
    $parent.removeChild(
      $parent.childNOdes[index];
    );
  } else if(chianged(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  }
}

對比子節點的不一樣

最後,須要遍歷新舊節點的子節點,並比較他們。能夠在每一個節點上都使用updateElement方法。是的,遞歸。

可是在開始代碼以前須要考慮一些問題:

  • 只有在節點是一個元素(element)的時候再去比較子節點(文本節點不可能有子節點)。
  • 當前節點做爲父節點傳入方法中。
  • 咱們要一個一個的比較子節點,即便會遇到undefined的狀況。沒有關係,咱們的方法能夠處理。
  • index,當前節點在直接父節點中的位置。
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) {
    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
      );
    }
  }
}

JSFiddle裏看看代碼把!

結語

祝賀你!咱們搞定了。咱們寫出了虛擬節點的實現。從上面的例子中你已經能夠理解虛擬節點的概念了,也大致能夠知道React是如何運做的了。

當時還有不少須要講述的內容,其中包括:

  • 設置節點的屬性(props)和比較、更新他們
  • 處理事件,在元素上添加事件監聽器
  • 讓咱們的節點像React的Component那樣運做
  • 獲取實際DOM的引用
  • 虛擬節點和其餘的庫一塊兒使用來修改真實的DOM,這些庫有jQuery等其餘的相似的庫。
  • 更多。。

原文地址:https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060

相關文章
相關標籤/搜索