在上一篇文章中我们实现了节点创建和渲染,但是忽略组件的情况,这一篇,我们来说说组件如何渲染,并实现一个setState,来初步完成我们自己的React
React组件
在react中组件大体分为两种,一种是一个纯函数,没有生命周期的。另一个通过继承自React.Component
的类来实现。
我们先来写一个Component
类。
class Component {
constructor(props) {
this.props = props;
this.state = this.state || {};
}
setState(partialState) {
this.state = Object.assign({}, this.state, partialState);
updateComponent(this);
}
}
复制代码
我们完成了一个Component
类,同时该类的实例有一个setState
函数,用来更新该组件。updateComponent
我们下面会实现它。
createNode函数
我们前面提到过虚拟节点的概念,但是我们但是直接使用Element
下面这种形式来作为我们的虚拟节点的。
{
type: 'div'
props: {}
}
复制代码
但是但我们要更新我们的组件是,我们需要记录Element
和DOM节点之间的关系,为了不污染Element
,我们引入我们新的虚拟节点的概念,我这里称之为Node
(实际情况并不是如此,这里只是为了方便命名)。之后相关的虚拟几点命名也会采用xxxNode
的命名方式。下面我们来改造我们以前的render
函数,将其重新命名为createNode
并且只接受一个类型为Element
的参数,其返回值为一个Node
类型的节点。
function createNode(element) {
const { type, props } = element;
// 是文本节点则创建文本节点,这里创建一个空的文本节点,后面利用nodeValue直接给该节点赋值
const isTextNode = type === 'TEXT ELEMENT';
const isComponent = typeof type === 'function';
// 组件情况
if (isComponent) {
const instance = new type(props);
let childElement = null;
if (instance.render) {
// 类情况
childElement = instance.render();
} else {
// 函数情况,直接执行
childElement = type(props);
}
// 创建Node节点
const childNode = createNode(childElement);
const dom = childNode.dom;
const node = { dom, element, childNodes: childNode.childNodes || [] };
// 在实例中记录旧的node节点,以便之后进行更新
instance._internalNode = node;
return node;
}
// dom情况
const childElements = props.children || [];
const childDom = isTextNode
? document.createTextNode('')
: document.createElement(type);
const isEvent = name => name.startsWith('on');
const isAttribute = name => !isEvent(name) && name !== 'children';
// 绑定事件
Object.keys(props).filter(isEvent).forEach(name => {
const eventName = name.toLowerCase().substring(2);
childDom.addEventListener(eventName, props[name]);
});
// 添加属性
Object.keys(props).filter(isAttribute).forEach(name => {
childDom[name] = props[name];
});
// 递归创建
const childNodes = childElements.map(createNode);
// 挂载到父节点
return { dom: childDom, element, childNodes }
}
复制代码
从上面可以看到,我们的虚拟节点Node
记录我们需要的信息,如element、dom、childrenNodes等,它是一个对象,结构是:
{ dom, element, childNodes }
复制代码
render函数
有了createNode
函数,现在再写我们的render函数:
function render(element, containerDom) {
// 获取虚拟节点
const node = createNode(element);
// 获取对应的dom元素
const childDom = node.dom;
// 获取子虚拟节点
const childNodes = node.childNodes || [];
// 渲染子虚拟节点
childNodes.forEach(childNode => render(childNode.element, childDom));
// 挂载至容器dom节点
containerDom.appendChild(childDom);
}
复制代码
在render
函数中,我们所需要做的就是获取虚拟节点并直接渲染它,并且需要同时渲染其孩子节点,最后挂载到根元素就完成了我们的渲染过程。
到这里我们已经可以渲染组件了,我们还有一个setState
需要实现,实现setState
就需要我们上面提到的updateComponent
函数了。
updateComponent函数
updateComponent
接收一个组件实例,它需要做哪些事情那?我们想一下,其实很简单,它只需要拿到旧的dom节点,然后渲染新的dom节点,最后将旧的替换为新的就能够实现刷新的效果了。在这里我们上面在实例中存储的_internalNode
就能发挥作用了。它记录了旧节点的所有信息。下面来实现吧:
function updateComponent(instance) {
// 执行render函数,得到要渲染的element
const childElement = instance.render();
// 旧的虚拟节点
const internalNode = instance._internalNode;
// 获取要挂载的父亲节点
const parentDom = internalNode.dom.parentNode;
// 获取新的虚拟节点
const newNode = createNode(childElement);
// 更新虚拟几点
instance._internalNode = newNode;
// 渲染孩子节点
const newDom = newNode.dom;
(newNode.childNodes || []).forEach(childNode => render(childNode.element, newDom));
// 将旧dom节点替换为新的dom节点
parentDom.replaceChild(newDom, internalNode.dom);
}
复制代码
实现完成,现在我们已经可以更新我们的组件了。这是codepen中的实例。
在组件的实现过程中为了简化,我们去掉了组件的生命周期,它需要作为一个个钩子挂载在不同的位置。如果你使用了实例,或者自己跑过之后会发现,我们每次更新都要重新渲染整个dom树,这样代价很大,在React中使用了一种diff
算法来重用不需要更新的节点和属性,下一节我们就来实现React中的diff
算法reconciliation
。
这是github原文地址。接下来我会持续更新,欢迎star,欢迎watch。
实现React系列列表: