大家好,我是微微笑的蜗牛,🐌。
今天将会开启一个新的系列,如何打造自己的 React 框架。包括如下几部分:
- dom 节点描述与创建
- jsx
- virtual dom
- component
这一篇文章主要讲 dom 节点描述与创建。
dom api
我们首先来看一下如何使用 dom api 来创建节点。节点分为两种类型:元素和文字。
对于元素来说,使用 createElement
创建,参数传入类型。如下所示,创建了一个 input 节点,document
是一个全局对象。
const domInput = document.createElement("input");
可通过元素 id 获取元素:
const domRoot = document.getElementById("root");
可设置元素属性:
domInput["type"] = "text";
domInput["value"] = "Hi world";
domInput["className"] = "my-class";
可监听事件:
domInput.addEventListener("change", e => alert(e.target.value));
对于文字来说,使用 createTextNode
创建,文字内容用属性 nodeValue
填充。如下所示:
// Create a text node
const domText = document.createTextNode("");
// Set text node content
domText["nodeValue"] = "Foo";
// Append an element
domRoot.appendChild(domInput);
在了解这些 api 后,我们接下来就可以着手设计自己框架中的节点描述格式了。我将这个框架称之为 SLReact,当然你也可以用你喜欢的名字。
节点描述
我们将使用 js 对象来描述一个节点信息。节点信息包括类型 type 和属性 props。
- type 用来描述节点类型,是个字符串,比如
div/span
。 - props 是节点属性信息。如果它有子节点的话,则会包含 children 字段。children 是一个数组,同样包含描述信息。
举个栗子:
const element = {
type: "div",
props: {
id: "container",
children: [
{ type: "input", props: { value: "foo", type: "text" } },
{ type: "a", props: { href: "/bar" } },
{ type: "span", props: {} }
]
}
};
上面这段信息,描述了如下的 dom 结构。div 节点中包含了 3 个子节点,input、a、span
。
<div id="container">
<input value="foo" type="text">
<a href="/bar"></a>
<span></span>
</div>
render
在有了节点描述信息之后,下一步就是如何将其转换为真正的 dom 节点,并添加到 dom 树上。这里我们将会实现自己的 render
方法。
元素节点
有了节点类型,很容易通过 createElement
这个 api 来创建 dom。
const { type, props } = element;
const dom = document.createElement(type);
若有子节点的话,递归调用 render 方法即可。如下所示:
const childElements = props.children || [];
childElements.forEach(childElement => render(childElement, dom));
但是需要注意的是,还有属性需要处理。props 中包含属性,同时也会包含事件信息。比如:
const element = {
type: "div",
props: {
onChange: () => {},
children: [],
other: 'xx'
}
};
- 以
on
开头的是事件监听,它的值是个方法。比如 onChange,表明我们想监听 change 事件。 -
children
是子节点信息。 - 其余就是普通属性。
事件监听
将以 on
开头的属性过滤出来,以全小写的方式取出事件名称,最后使用 addEventListener
来监听事件。
如下所示:
const isListener = name => name.startsWith("on");
Object.keys(props).filter(isListener).forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, props[name]);
});
添加属性
在上一步,我们处理了事件监听的属性。这里再来处理普通属性,过滤掉以 on
开头的属性和 children
即可,然后将属性添加到 dom 中。
如下所示:
const isAttribute = name => !isListener(name) && name != "children";
Object.keys(props).filter(isAttribute).forEach(name => {
dom[name] = props[name];
});
文本节点
先来看下 createTextNode
函数的说明。其中参数 data 为文本内容,它指定了节点属性 nodeValue
的值。下面会用到这个知识点。
/**
* Creates a text string from the specified value.
* @param data String that specifies the nodeValue property of the text node.
*/
createTextNode(data: string): Text;
文本的描述结构同元素,但是请注意:文本内容将被当做一个子节点。这跟《听说你想写个渲染引擎》中的处理是一样的。
比如一段文本:<span>Foo</span>
。在 React 中,它的描述结构如下:
const reactElement = {
type: "span",
props: {
children: ["Foo"]
}
};
其中,文本内容 Foo
被当成了子节点,但它是一个字符串。
上面我们提到,children 中的结构也是一段描述信息。为了统一处理,这里将文本内容信息修改同样的结构,使用 type + props 的描述方式。文本内容使用 nodeValue
属性来描述。
如下所示:
const textElement = {
type: "span",
props: {
children: [
{
type: "text",
props: { nodeValue: "Foo" }
}
]
}
};
这样,当我们遇到类型是 text
的节点时,便认为它是文本,而属性 nodeValue
的值就是文本内容。所以将属性塞给 dom 就好。
const { type, props } = element;
// 创建节点
const isTextElement = type === "text";
const dom = isTextElement
? document.createTextNode("")
: document.createElement(type);
完整的 render 方法如下所示:
function render(element, parentDom) {
const { type, props } = element;
// 创建节点
const isTextElement = type === "text";
const dom = isTextElement
? document.createTextNode("")
: document.createElement(type);
// 处理事件监听
const isListener = (name) => name.startsWith("on");
Object.keys(props)
.filter(isListener)
.forEach((name) => {
const eventType = name.toLocaleLowerCase().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((child) => render(child, dom));
// 添加父节点
parentDom.appendChild(dom);
}
测试代码
function importFromBlow() {
function render(element, parentDom) {
// 省略实现
}
return { render };
}
const SLReact = importFromBlow();
const stories = [
{ name: "part1", url: "http://bit.ly/2pX7HNn" },
{ name: "part2", url: "http://bit.ly/2qCOejH" },
{ name: "part3", url: "http://bit.ly/2qGbw8S" },
{ name: "part4", url: "http://bit.ly/2q4A746" },
{ name: "part5", url: "http://bit.ly/2rE16nh" },
];
const appElement = {
type: "div",
props: {
children: [
{
type: "ul",
props: {
children: stories.map(storyElement),
},
},
],
},
};
// 生成 element 结构
function storyElement({ name, url }) {
const likes = Math.ceil(Math.random() * 100);
const buttonElement = {
type: "button",
props: {
children: [
{ type: "text", props: { nodeValue: likes } },
{ type: "text", props: { nodeValue: " 🐶" } },
],
},
};
const linkElement = {
type: "a",
props: {
href: url,
children: [{ type: "text", props: { nodeValue: name } }],
},
};
return {
type: "li",
props: {
children: [buttonElement, linkElement],
},
};
}
SLReact.render(appElement, document.getElementById("root"));
看看最后一句,是不是就很像 React 中的用法了?在 render 方法中传入节点描述信息和 dom 根节点即可。
最后的效果图如下所示(有自定义 css):
image完整的代码在此:https://github.com/silan-liu/slreact/tree/master/part1。
总结
这一篇文章主要介绍了如何定义节点描述信息,以及实现自己的 render 方法,来完成 dom 节点的创建和属性设置。感谢阅读~
下一篇文章将会讲述 jsx 的实现,敬请期待。
网友评论