循序漸進DIY一個react(一)

鯊叔發表於2019-02-20

前言

假設我大學本科畢業論文的課題是[依據react現有的思想DIY一個react],我會怎麼實現呢?作為一個react老使用者的我,我常常有這樣的疑問。那好,現在,我就在這根據現有的react概念和思想,循序漸進地DIY一個簡單版的react。一來,為自己立下一個react研究程式的里程碑;二來,跟大家交流交流。

正文

如下,我將這個循序漸進的DIY過程細分為四個步驟:

  1. 設計virtual DOM的資料結構。
  2. 實現從virtual DOM -> real DOM的對映。
  3. 實現整樹對映過程中協調。
  4. 實現子樹對映過程的協調。

這四個步驟一路下來,肯定會遇到很多新的概念。沒關係,概念是人定義的,只要是人定義的,就可以解釋。況且,react的官方也解釋過的。不過,只不過我會根據我的理解來解釋。

一. 設計virtual DOM的資料結構

個人覺得,第一個提出“virtual DOM”概念的人想必對DOM是有十分深入的瞭解的。聰明的人一看就知道,virtual DOM這個概念肯定是相對(real)DOM這個概念而言的。所以,想要理解virtual DOM,我們不妨從理解(real)DOM開始。什麼是DOM?

javascrit犀牛書裡面是這樣說的:

文件物件模型(DOM)是表示和操作HTML和XML文件內容的基礎API。

《Javascript DOM程式設計藝術(第二版)》裡面將對定義的解釋展開成三部分裡來說。也即是說:

  • D-document

如果沒有document(文件),DOM也就無從談起。當建立了一個網頁並將它載入到web瀏覽器中時,DOM就在幕後悄然而生。它把你編寫的網頁文件轉換為一個文件物件。

  • O-object

在程式設計語言中,“物件”這個詞的含義是非常明確的。“物件”是一種自給自足的資料集合。與某個特定的物件相關聯的變數被稱為這個物件的屬性;只能通過某個特定物件去跳用的函式被稱之為這個物件的方法。

  • M-model

DOM把一份文件表示為一顆樹......更具體說,DOM把文件表示為一顆家譜樹。家譜樹本身又是一種模型。與使用“家譜樹”這個術語相比,把文件稱為“節點樹”更準確。

MDN web docs裡面是這樣說的:

The Document Object Model (DOM) is a programming interface for HTML and XML documents. It represents the page so that programs can change the document structure, style, and content. The DOM represents the document as nodes and objects. That way, programming languages can connect to the page.

根據上面的定義,結合我自己的理解,我們可以給DOM下這樣的一個定義:

DOM是把文件(一般包括HTML文件和XML文件)轉換為樹型可程式設計文件物件的一種介面。

在這裡,我們也可以把這種介面理解為規範。因為當這種介面被眾多瀏覽器所實現的時候,它無疑代表著一種行業規範。我們也可以把這種介面理解為一種從真實文件到可程式設計物件的一種對映關係。而最終完成這種對映的操作是由瀏覽器核心來完成的。而現代瀏覽器支援的指令碼語言javascript。所以,如果你覺得上面我們的定義實在太抽象的話,那麼我們可以簡單地理解為:

DOM是一種關於如何在HTML標籤javascript物件之間建立一一對應關係的規範。

為什麼說是簡單理解呢?因為標籤不一定是HTML標籤,還可以是XML標籤。物件也不一定是javascript的,也可以是python的,如下:

# Python DOM example
import xml.dom.minidom as m
doc = m.parse(r"C:\Projects\Py\chap1.xml")
doc.nodeName # DOM property of document object
p_list = doc.getElementsByTagName("para")

複製程式碼

上面所說的javascript物件在語義上說就是《Javascript DOM程式設計藝術(第二版)》中所提到的物件,也就是說傳統物件導向程式語言所說的“物件”-有自己特定的屬性和方法。這跟廣義上“javascript物件”的概念還是有區別的。更準確點,我們稱之為“原生DOM物件”。

假如,我們有以下HTML文件:

<html>
  <head>
    <title>DOM Tests</title>
  </head> 
  <body>
    <div id="test"></div>
  </body>
</html>
複製程式碼

那麼HTML標籤<div id="test"></div>所對應的原生DOM物件就可以通過document.getElementById方法來獲得。該原生DOM物件有自己的屬性和方法:

let divElement = document.getElementById('test')
let textNode = document.createTextNode('我是文字節點')

// 是個javascript物件
console.log(typeof divElement) // "object"

// 有自己的屬性,比如id
console.log(divElement.id) // "test"

/* 有自己的方法,比如appendChild()
 * 最終在文件中的表現為:<div id="test">我是文字節點</div>
*/ 
divElement.appendChild(textNode) 
複製程式碼

好,弄清楚什麼是“(real)DOM”之後,那麼“virtual DOM”就比較好理解了。顧名思義,virtual DOM無非指的就是非真正的DOM,是虛擬的。虛擬,虛擬,那擬的是誰的呢?當然是“(real)DOM”啦。在DOM的世界裡面,它們是用一個原生DOM物件來描述文件樹的一個節點的。那麼與此類比,virtual DOM的世界我們用一個pure javascript object(也就是我們平時所說的字面量物件,有點想swift的hash table)來描述一個文件節點,好像也說得通。我們說幹就幹。

 // 字面量物件
 let virtualDOM ={}
複製程式碼

原生DOM物件有nodeName屬性,代表著文件元素的標籤名稱,我們也得有。為了區別於原生,我們取名為“type”。

 // 字面量物件
 let virtualDOM ={
   type:string
 }
複製程式碼

文件元素一般都會有眾多屬性,對應到原生DOM物件中,它也有一個叫attributes的屬性。還是為了區別與原生,我們取名為“properties”,簡寫為“props”。不過相比DOM裡面的“元素屬性集合也是一個節點集合(nodeList)”,我們這裡簡化一下,也就是說還是用key-value組成的字面量物件來表示屬性集合。

 // 字面量物件
 let virtualDOM ={
   type:string,
   props:{
    prop1:value1,
    prop2:value2,
    ......
   }
 }
複製程式碼

因為文件具有天然的樹狀結構,所以一個元素與另外的一個元素必定存在這樣那樣的關係。要麼A是B的父節點,要麼C是B的兄弟節點。在DOM中,我們正是通過這種關係來遍歷整個文件樹。還是拿上面的divElement元素舉例。

循序漸進DIY一個react(一)

其實,在文件樹中,一個元素跟另外一個元素的關係無非就兩種:父子關係和兄弟關係:

divElement.parentNode  -> <body />
divElement.previousSibling -> null // divElement並沒有往下的兄弟節點
divElement.nextSibling -> null  // divElement並沒有往下的兄弟節點
divElement.childNodes  -> ['我是文字節點']
複製程式碼

那麼對比原生DOM物件,在virtual DOM物件上,我們該如何設計出對應的資料結構來體現這兩種關係呢?答案是多樣化的。你可以這樣設計:

// 字面量物件
 let virtualDOM ={
   type:string,
   props:{
    prop1:value1,
    prop2:value2,
    ......,
    children:[child1,child2,......]
   }
 }
複製程式碼

上面的這種設計,就是讓children也成為了一個屬性,只不過使用的時候,不能把它當作一個真正的屬性來用。

你也可以這樣設計:

// 字面量物件
 let virtualDOM ={
   type:string,
   props:{
    prop1:value1,
    prop2:value2,
    ......
   },
   children:[
        child1,
        child2,
        ......
    ]
 }
複製程式碼

這種設計相比前者,把children屬性放在與props屬性平級的層級上,個人感覺從結構角度上來評價,後者更好理解。但是不知道為啥,react官方採用了前者的設計方案。無論哪種設計方案,都是在virtual DOM節點中儲存一個指向它的所有子節點的引用來體現父子關係的。而陣列中相鄰的兩項則是兄弟關係的體現。

至此,我們通過理解DOM,並運用依樣畫葫蘆的手法,我們完成了virtual DOM物件的資料結構的設計。這是十分基礎且重要的一步。如果說,現實世界是由原子堆疊而成的話,那麼我們也可以說,文件就是由節點(node,node又分為elecment node,text node和 attribute node)堆疊而成,DOM就是由原生DOM物件堆疊而成,virtual DOM就是由virtual DOM物件堆疊而成的。在react的世界裡面,virtual DOM物件是構建應用的基本單元。我們正是通過把[對virtual DOM的更新]對映[為對real DOM的更新],再通過瀏覽器把[對real DOM的更新]對映為[對文件的更新]來完成介面的更新的。

循序漸進DIY一個react(一)

下一篇文章,我就來談談如何使用javascript來實現這種“對映”。 更具體地說,就是如何將

{
  type:'div',
  props:{
    id:'test',
    children:['我是文字節點']
  }
}
複製程式碼

最終“對映”為真正文件上的一個元素節點:

<div id="test">我是文字節點</div>
複製程式碼

下篇:循序漸進DIY一個react(二)

相關文章