在上一篇文章中咱们实现了节点建立和渲染,可是忽略组件的状况,这一篇,咱们来讲说组件如何渲染,并实现一个setState,来初步完成咱们本身的Reactnode
在react中组件大致分为两种,一种是一个纯函数,没有生命周期的。另外一个经过继承自React.Component
的类来实现。react
咱们先来写一个Component
类。git
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
咱们下面会实现它。github
咱们前面提到过虚拟节点的概念,可是咱们可是直接使用Element
下面这种形式来做为咱们的虚拟节点的。算法
{
type: 'div'
props: {}
}
复制代码
可是但咱们要更新咱们的组件是,咱们须要记录Element
和DOM节点之间的关系,为了避免污染Element
,咱们引入咱们新的虚拟节点的概念,我这里称之为Node
(实际状况并非如此,这里只是为了方便命名)。以后相关的虚拟几点命名也会采用xxxNode
的命名方式。下面咱们来改造咱们之前的render
函数,将其从新命名为createNode
而且只接受一个类型为Element
的参数,其返回值为一个Node
类型的节点。app
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
{ dom, element, childNodes }
复制代码
有了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
函数中,咱们所须要作的就是获取虚拟节点并直接渲染它,而且须要同时渲染其孩子节点,最后挂载到根元素就完成了咱们的渲染过程。post
到这里咱们已经能够渲染组件了,咱们还有一个setState
须要实现,实现setState
就须要咱们上面提到的updateComponent
函数了。ui
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系列列表: