一步一步帶你實現virtualdom(一)

桃子紅了吶發表於2017-11-16

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

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

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

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

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

生成虛擬DOM樹

首先我們需要在記憶體裡儲存我們的DOM樹。只要使用js就可以達到這個目的。假設我們有這樣的一個樹:

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

看起來非常簡單對吧。我們怎麼用js的物件來對應到這個樹呢?

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

這裡我們會注意到兩件事:

  • 我們使用這樣的物件來對應到真實的DOM上:{type: `...`, props: {...}, children: [...]}
  • DOM的文字節點會對應到js的字串上。
    但是如果用這個方法來對應到巨大的DOM樹的話那將是非常困難的。所以我們來寫一個helper方法,這樣結構上也就容易理解一些:

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

    現在我們可以這樣生成一個虛擬DOM樹:

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

    這樣看起來就清晰了很多。但是我們還可以做的更好。你應該聽說過JSX對吧。是的,我們也要用那種方式。但是,這個應該如何下手呢?

如果你讀過Babel的JSX文件的話,你就會知道這些都是Babel的功勞。Babel會把下面的程式碼轉碼:

<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


本文轉自張昺華-sky部落格園部落格,原文連結:http://www.cnblogs.com/sunshine-anycall/p/7897140.html,如需轉載請自行聯絡原作者


相關文章