banner
这篇文章是 制作你自己的React系列的其中一篇

#DOM
在开始之前,我们先看看要用到的DOM API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Get an element by id
const domRoot = document.getElementById("root");
// Create a new element given a tag name
const domInput = document.createElement("input");
// Set properties
domInput["type"] = "text";
domInput["value"] = "Hi world";
domInput["className"] = "my-class";
// Listen to events
domInput.addEventListener("change", e => alert(e.target.value));
// Create a text node
const domText = document.createTextNode("");
// Set text node content
domText["nodeValue"] = "Foo";
// Append an element
domRoot.appendChild(domInput);
// Append a text node (same as previous)
domRoot.appendChild(domText);

DOM.js hosted with ❤ by GitHub

注意我们是设定DOM元素的properties而不是attributes。也就是说只有有效的属性可以被支持。

Didact 元素

我们将使用普通的JS对象来表示我们将要渲染的东西。我将它们称为Didact元素。这些元素有连个必须的属性:propstypetype可以是字符串或者函数,我们先只允许字符串式的type,等之后引入组件的时候才允许函数作为typeprops可能会有一个children属性,他应该是包含Didact元素的一个数组。

Didact元素是最常用的元素,所以从现在开始我将它称为元素。不要把他和HTML元素混淆,我们将HTML元素称为DOM元素,或者只是称他们为DOM(就像preact那样)

打个比方,一个元素就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
const element = {
type: "div",
props: {
id: "container",
children: [
{ type: "input", props: { value: "foo", type: "text" } },
{ type: "a", props: { href: "/bar" } },
{ type: "span", props: {} }
]
}
};

element.js hosted with ❤ by GitHub

最后会编译成这样的DOM

1
2
3
4
5
<div id="container">
<input value="foo" type="text">
<a href="/bar"></a>
<span></span>
</div>

element.html hosted with ❤ by GitHub

Didact元素和React元素非常相似。但是通常你不会在用React的时候直接创建JS对象作为React元素,一般你会用JSX或者骚一点createElement。Didact也一样,但是我留在了下一篇文章里。

#渲染DOM元素

下一步把一个元素和他的子元素渲染成DOM。我们将使用render函数(相当于ReactDOM.render),接收一个元素和一个DOM容器。这个函数创建有元素定义的DOM子树并将其渲染到DOM容器中:

1
2
3
4
5
6
7
function render(element, parentDom) {
const { type, props } = element;
const dom = document.createElement(type);
const childElements = props.children || [];
childElements.forEach(childElement => render(childElement, dom));
parentDom.appendChild(dom);
}

render.js hosted with ❤ by GitHub

我们还没有给元素加上properties和事件监听。我们通过Object.keys函数遍历props来添加他们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function render(element, parentDom) {
const { type, props } = element;
const dom = document.createElement(type);

const isListener = name => name.startsWith("on");
Object.keys(props).filter(isListener).forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, props[name]);
});

const isAttribute = name => !isListener(name) && name != "children";
Object.keys(props).filter(isAttribute).forEach(name => {
dom[name] = props[name];
});

const childElements = props.children || [];
childElements.forEach(childElement => render(childElement, dom));

parentDom.appendChild(dom);
}

render.js hosted with ❤ by GitHub

渲染DOM中的文字节点

我们的render还没有支持文字节点。先定义一下文字节点的数据结构。打个比方一个<span>Foo</span>在React中这样被定义:

1
2
3
4
5
6
const reactElement = {
type: "span",
props: {
children: ["Foo"]
}
};

react-text-element.js hosted with ❤ by GitHub

注意这个元素的props.children数组中不是另一个Didact元素而是一个字符串。这违背了之前对Didact元素的定义。如果我们遵循这个规则未来会少很多if else。所以Didact 的Text元素将会有一个值为”TEXT ELEMENT”的type属性,文字将会在nodeValue属性中。就像这样:

1
2
3
4
5
6
7
8
9
10
11
const textElement = {
type: "span",
props: {
children: [
{
type: "TEXT ELEMENT",
props: { nodeValue: "Foo" }
}
]
}
};

didact-text-element.js hosted with ❤ by GitHub

现在我们定义了文本元素的数据格式,普通元素和文本元素的区别是,创建文本元素时候我们要使用createTextNode而不是createElementnewValue将会设置为文本元素的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function render(element, parentDom) {
const { type, props } = element;

// Create DOM element
const isTextElement = type === "TEXT ELEMENT";
const dom = isTextElement
? document.createTextNode("")
: document.createElement(type);

// Add event listeners
const isListener = name => name.startsWith("on");
Object.keys(props).filter(isListener).forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, props[name]);
});

// Set properties
const isAttribute = name => !isListener(name) && name != "children";
Object.keys(props).filter(isAttribute).forEach(name => {
dom[name] = props[name];
});

// Render children
const childElements = props.children || [];
childElements.forEach(childElement => render(childElement, dom));

// Append to parent
parentDom.appendChild(dom);
}

render.js hosted with ❤ by GitHub

#总结

我们实现了render函数,让我们把一个元素渲染到DOM里。下一步是我们需要一个简单的创建元素的方法,在下一篇文章中,我们将会在Didact中引入JSX。
如果你想试试目前为止的代码,可以在codepen中查看。你可以看看 diff from the github repo