本系列文章是分享我自己一步一步编写整个框架的过程,有兴趣的xdm可以参考源代码阅读。git仓库:github.com/sullay/art-…
自定义组件
通过前面努力,我们已经成功完成了dom渲染,并且借助jsx语法简化了我们虚拟dom的创建工作。这篇文中我们将开始构建自己的自定义组件。
首先要了解一件事情,就是jsx是如何处理自定义组件的。这里我直接说结论,对自定义组件h函数的第一个参数type应该是该组件对应的构造器。所以我们只需要判断出type是否为自定义组件的构造器,然后使用new type()便可以创建出组件实例。
首先我写一个组件类Component,后面所有自定义组件都继承这个类。并且在vNode中标识出来。
// 自定义组件父类
export class Component {
}
export class vNode {
constructor(type = '', allProps = {}, children = []) {
// 标识自定义组件
this.isComponent = (type.prototype instanceof Component);
this.type = type;
this.props = {};
...
}
}
然后我们规定自定义组件都要包含一个render方法,用于自定义组件虚拟dom的创建。例如:
class App extends Component {
render() {
return (
<div>
<p style="color: red;">11111111111111111111</p>
<p>222222222222222222</p>
</div>
)
}
}
再次改造render方法,区分自定义组件。
function render(node, parentDom) {
if (!vNode.isVNode(node)) throw new Error("渲染元素类型有误");
const { type, props, events, children } = node;
if (node.isComponent) {
let newComponent = new type(props, events, children).render();
render(newComponent, parentDom);
} else {
// 根据元素类型创建对应的dom
const dom = vTextNode.isVNode(node) ? document.createTextNode('') : document.createElement(type);
// 设置属性
for (let key in props) dom[key] = props[key];
// 渲染子元素
for (let child of children) render(child, dom);
// 监听事件
for (let event in events) dom.addEventListener(event, events[event])
// 绑定到父元素
parentDom.appendChild(dom);
}
}
此时我们已经可以正常渲染出页面。但是考虑到自定义组件的数据更新的话就会发现,上面的做法完全没办法更新组件数据。
整改代码
整改代码之前我们先梳理一下上面代码中存在的问题:
- 虚拟dom树上没有自定义组件实例,无法实现更新
- 每次渲染时都要去重新创建dom
- render中每次都是使用appendChild追加dom
新增根节点
新增根节点,用来追踪整棵虚拟dom树
import { vNode } from './VNode'
const ROOT = Symbol('root');
class Root extends vNode {
}
if (!window[ROOT]) {
window[ROOT] = new Root();
}
export default window[ROOT];
整改vNode
vNode类中新增了getDom、createDom两个方法,帮助我们获取和创建对应的dom元素。
新增vComponentNode类。
- $instance用来引用自定义组件的实例,组件实例中也可以通过$vNode获取到对应的虚拟dom节点
- vComponentNode的组件实例render渲染出来的虚拟dom树作为vComponentNode的子节点,并且vComponentNode并没有自己的dom只是其子节点的$dom的引用。
// 普通元素
export class vNode {
constructor(type = '', allProps = {}, children = []) {
this.$type = type;
this.$props = {};
this.$events = {};
for (let prop in allProps) {
if (isEvent(prop)) {
// 从props中过滤出事件监听
this.$events[getEventName(prop)] = allProps[prop];
} else {
this.$props[prop] = allProps[prop];
}
}
// 处理子元素中的文字类元素
this.$children = children.map(child => {
return vNode.isVNode(child) ? child : new vTextNode(child);
});
}
// 判断是否属于虚拟Dom元素
static isVNode(node) {
return node instanceof this;
}
getDom() {
if (!this.$dom) this.createDom();
return this.$dom;
}
createDom() {
this.$dom = document.createElement(this.$type);
// 设置属性
for (let key in this.$props) this.$dom[key] = this.$props[key];
// 监听事件
for (let event in this.$events) this.$dom.addEventListener(event, this.$events[event]);
}
}
// 文字元素
export class vTextNode extends vNode {
constructor(text) {
super(vTextNode.type, { nodeValue: text })
}
static type = Symbol('TEXT_ELEMENT');
createDom() {
this.$dom = document.createTextNode('');
// 设置属性
for (let key in this.$props) this.$dom[key] = this.$props[key];
}
}
export class vComponentNode extends vNode {
constructor(type = '', allProps = {}, slots = []) {
super(type, allProps);
this.$isComponent = true;
this.$slots = slots;
this.$instance = new type(this.$props, this.$events, slots);
this.$instance.$vNode = this;
}
createDom() {
let child = this.$instance.render();
this.$children = [child];
this.$dom = child.getDom();
}
}
整改render
render方法中创建根节点。 renderDomTree方法:
- getDom来获取虚拟Dom节点对应的真实dom。
- 渲染的过程中赋值虚拟dom的父节点,使得整个dom树挂载到根节点上。
- 增加第三个参数oldDom,后续更新节点时传入旧的dom节点进行替换。
// 渲染domTree
export function renderDomTree(node, parentNode, oldDom) {
if (!vNode.isVNode(node)) throw new Error("渲染元素类型有误");
// 设置父节点
node.$parentNode = parentNode;
let parentDom = parentNode.getDom();
let dom = node.getDom();
// 判断是否为自定义组件
if (node.$isComponent) {
for (let child of node.$children[0].$children) renderDomTree(child, node.$children[0]);
} else {
for (let child of node.$children) renderDomTree(child, node);
}
// 绑定到父元素
if (oldDom) {
parentDom.replaceChild(dom, oldDom)
} else {
parentDom.appendChild(dom);
}
}
// 渲染方法
export function render(node, parentDom) {
$root.$dom = parentDom;
$root.$children = [node];
renderDomTree(node, $root);
}
// 创建元素
export function h(type, props, ...children) {
if (type.prototype instanceof Component) {
return new vComponentNode(type, props, children);
} else {
return new vNode(type, props, children);
}
}
整改完毕,经过上述的整改后,我们的框架从一个demo变成了一个真正的框架,可以更好的支持我们后续功能扩展。
下一篇文章中将完成框架数据更新,感兴趣的xdm可以关注我后续的更新。