本系列文章是分享我自己一步一步编写整个框架的过程,有兴趣的xdm可以参考源代码阅读。git仓库:github.com/sullay/art-…
数据更新
通过前面的努力我们的框架已经可以完成页面渲染的工作了,那数据更新其实只需要把数据有变更的组件更新一下就可以了。
回顾一下之前代码,对于一个vNode对象,如果没有$dom属性就会重新生成dom。
getDom() {
if (!this.$dom) this.createDom();
return this.$dom;
}
复制代码
所以我们只需要在数据更新以后将原来的$dom置空,便可以再后续的渲染中重新生成。这里我们约定Component组件的data属性用来存放所有支持动态更新到属性。
// 传入需要更新的数据,触发渲染
setData(data) {
for (const key in data) {
this.data[key] = data[key];
}
let oldDom = this.$vNode.$dom;
// 自定义组件node的$dom指向子节点的$dom,此处赋值为null是为了触发createDom
this.$vNode.$dom = null;
// 传入oldDom用作替换
renderDomTree(this.$vNode, this.$vNode.$parentNode, oldDom);
}
复制代码
通过简单的$dom置空完成了数据更新,但是这里面其实存在一些问题:
- 自定义组件的所有dom都需要重新创建
- 子组件被初始化状态没有保留
$dom、$instance复用
为了解决上述两个问题,我们就需要在createDom时尽可能使用之前创造的dom,针对vComponentNode还需要用旧node节点的$instance替换掉新node节点的$instance属性。
// vComponentNode类
createDom() {
let preChildren = this.$children;
let child = this.$instance.render();
this.$children = [child];
// 每次创建时通过diff复用之前的dom或者子组件
vNode.diffDom(this.$children, preChildren);
this.$dom = child.getDom();
}
// 旧的node中的dom根据新node重新赋值
static updateDom(newNode, preNode) {
let dom = preNode.$dom;
for (let key in preNode.$props) dom[key] = null;
for (let event in preNode.$events) dom.removeEventListener(event, preNode.$events[event]);
// 去掉子节点,调用renderDomTree方法时再组织dom结构
dom.innerHTML = null;
// 设置属性
for (let key in newNode.$props) dom[key] = newNode.$props[key];
// 监听事件
for (let event in newNode.$events) dom.addEventListener(event, newNode.$events[event]);
newNode.$dom = dom;
}
// 新旧节点对比,尽可能复用已有dom或者自定义组件
static diffDom(newNodes = [], preNodes = []) {
if (!newNodes.length || !preNodes.length) return;
// 所有旧node
let preMap = new Map();
for (const node of preNodes) {
if (!preMap.has(node.$type)) preMap.set(node.$type, []);
if (node.$dom) preMap.get(node.$type).push(node);
}
// 遍历新node,查找是否存在可以复用的node
for (const node of newNodes) {
if (preMap.has(node.$type) && preMap.get(node.$type).length) {
let preNode = preMap.get(node.$type).shift();
if (vComponentNode.isVNode(node)) {
let preChildren = preNode.$children;
// 自定义组件,复用旧的组件实例
node.$instance = preNode.$instance;
// 组件实例$vNode指向新的node
node.$instance.$vNode = node;
// 更新自定义组件虚拟dom树
let child = node.$instance.render();
node.$children = [child];
vNode.diffDom(node.$children, preChildren);
// 自定义组件$dom指向子组件的$dom
node.$dom = child.$dom;
} else {
// 普通node节点,更新后继续diff子组件
vNode.updateDom(node, preNode);
vNode.diffDom(node.$children, preNode.$children);
}
}
}
}
复制代码
上述代码目前只支持同一层级的复用,不同层级复用建议使用指定key值来实现。
props、events更新
此时我们已经可以复用之前的dom以及组件实例,但其实还存在一个隐藏的bug。先回顾一下上级篇文章的代码。
export class vComponentNode extends vNode {
constructor(type = '', allProps = {}, slots = []) {
...
this.$instance = new type(this.$props, this.$events, slots);
this.$instance.$vNode = this;
}
}
复制代码
假设我们存在父子组件,我们希望子组件的props属性可以根据父组件的data属性变更。但是子组件的props属性是在构造组件对象的时候传入的,我们数据更新时复用了原来的$instance属性,因此props属性便无法完成更新。
我们可以考虑使用类似于react的做法,通过一个额外的钩子函数来触发props的更新,这里我使用了另一种做法。
export class vComponentNode extends vNode {
constructor(type = '', allProps = {}, slots = []) {
super(type, allProps);
// 标识自定义组件
this.$isComponent = true;
// 插槽
this.$slots = slots;
// 创建组件实例
this.$instance = new type(this);
}
}
// 自定义组件类
export class Component {
constructor(node) {
// 绑定对应node节点
this.$vNode = node;
this.data = {}
}
// 所有属性都去node上面拿,复用原组件时不需要初始化可以更新数据与事件。
get props() {
return this.$vNode.$props;
}
get events() {
return this.$vNode.$events;
}
get slots() {
return this.$vNode.$slots;
}
}
复制代码
我们不把props属性传给组件类,而是把vNode对象传过去,这样props属性还是绑定在vNode对象上。组件更新时,$instance虽然被复用,但是props属性依然可以被更新。
到此为止我们已经完成了数据更新的工作,下一章我们将开始编写框架的任务调度,也是此框架最核心的功能。
xdm觉得这系列文章对你有帮助的点赞支持啊!!!