循序渐进DIY一个react(一)

2,073 阅读8分钟

前言

假设我大学本科毕业论文的课题是[依据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元素举例。

其实,在文档树中,一个元素跟另外一个元素的关系无非就两种:父子关系和兄弟关系:

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的更新]映射为[对文档的更新]来完成界面的更新的。

下一篇文章,我就来谈谈如何使用javascript来实现这种“映射”。 更具体地说,就是如何将

{
  type:'div',
  props:{
    id:'test',
    children:['我是文本节点']
  }
}

最终“映射”为真正文档上的一个元素节点:

<div id="test">我是文本节点</div>

下篇:循序渐进DIY一个react(二)