上文 我们做到让一个文字虚拟节点上树 但子节点显然还 没有完成
那这次我们继续
递归的话 我们需要换个思路
我们将 src下的入口文件 index.js代码改成这样
import h from "./snabbdom/h";
import patch from "./snabbdom/patch";
const container = document.getElementById("container");
const vnode = h("h1", {}, "你好");
patch( container, vnode)
我们还是像上次一样 先插入一个节点 但要换一种方式
createElement.js 代码修改如下
//创建节点
//函数接收两个参数 vnode 需要插入的虚拟节点 pivot 需要插入到哪一个元素之前
export default function(vnode) {
//先用虚拟节点的sel 标签名选择器 创建一个真实的dom节点
let domNode = document.createElement(vnode.sel);
//如果 能判断到虚拟节点下的 text 文本属性 而且 拿不到children子节点 或者 children 子节点 没有长度
if(vnode.text && (!vnode.children || !vnode.children.length)){
//这个条件达到 表示 他的内容是一个文章 没有子节点
//直接将虚拟节点的 text 文本内容 通过innerText写入到新节点中
domNode.innerText = vnode.text;
// 将新生成孤儿节点 domNode 保存在虚拟节点的elm属性上
vnode.elm = domNode;
//判断 如果 children 是个数组 说明 存在子节点
}else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
//递归创建子节点
}
//将函数用虚拟节点创建的孤儿节点返回
return vnode.elm
}
条件上的话 我们先加入如果children是个数组 且有长度的判断 但主要改的还是 得到孤儿节点后 不再直接在这个文件中插入了 我们将得到的孤儿节点保存在了虚拟节点的elm属性
最后 返回vnode的elm字段
那么 这里不插入节点 在哪里插入呢?
对 patch.js
patch.js 参考代码如下
import vnode from "./vnode";
import createElement from "./createElement";
export default function(oldVnode, newVnode) {
//先判断oldVnode 第一个参数 是一个虚拟节点 还是一个真实的dom节点
if(!oldVnode.sel) {
//如果oldVnode中 拿不到sel 表示这是一个真实的节点 因为虚拟节点中 sel 代表标签类型选择器
//那么 我们就要将虚拟节点转为真实的节点
//这里 我们通过vnode 将真实节点转为真实节点
/*
第一个参数sel 标签选择器 我们通过tagName.toLowerCase() 获取dom节点的 标签类型
第二个参数data 标签的数据 dom老节点肯定是没有的 我们直接{}
第三个参数 children 子集集合 我们这里肯定是没有的 直接 [] 或者 undefined
第四个参数 text 文本内容 我们这里 没有直接undefined
最后一个 elm 他上树的节点 我们直接给他自己 oldVnode
*/
oldVnode = vnode(oldVnode.tagName.toLowerCase(),{},[],undefined,oldVnode);
}
//判断 oldVnode 和 newVnode 是不是同一个节点
//判断的条件是 sel 标签选择器 和 key 元素表示是否都相同
if(oldVnode.sel == newVnode.sel && oldVnode.key == newVnode.key) {
} else {
//else 走进来 表示两者并不是同一个节点
//那就执行 暴力拆除旧的 插入新的
//先通过createElement 将虚拟节点变成一个个人节点
let newVnodeElm = createElement(newVnode);
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
}
}
这里 我们定义一个 newVnodeElm 接一下 createElement的返回值 就是会返回vnode.elm 刚才生成的孤儿节点
然后 直接在这里 通过oldVnode.elm进行插入
我们运行项目
这样 我们就将逻辑进行了一个改造
改造是为了递归渲染子节点做铺垫 那么 铺垫都做好了 就下一步
先将
src下的 index.js 代码改成这样
import h from "./snabbdom/h";
import patch from "./snabbdom/patch";
const container = document.getElementById("container");
const vnode = h("ul", {}, [
h("li", {}, "1A"),
h("li", {}, "2B"),
h("li", {}, "3C"),
h("li", {}, "4D")
]);
patch( container, vnode)
这里我们将vnode又改成了一个多层嵌套的虚拟节点 然后 关键文件就还是 createElement.js
然后 createElement.js 代码修改如下
//创建节点
//函数接收两个参数 vnode 需要插入的虚拟节点 pivot 需要插入到哪一个元素之前
export default function createElement(vnode) {
//先用虚拟节点的sel 标签名选择器 创建一个真实的dom节点
let domNode = document.createElement(vnode.sel);
//如果 能判断到虚拟节点下的 text 文本属性 而且 拿不到children子节点 或者 children 子节点 没有长度
if(vnode.text && (!vnode.children || !vnode.children.length)){
//这个条件达到 表示 他的内容是一个文章 没有子节点
//直接将虚拟节点的 text 文本内容 通过innerText写入到新节点中
domNode.innerText = vnode.text;
//判断 如果 children 是个数组 说明 存在子节点
}else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
//循环遍历当前虚拟节点的所有 children 子节点
vnode.children.map(item => {
//通过调用自己createElement 将每个子集的虚拟节点也都变成真实的孤儿节点
let chDom = createElement(item);
//将孤儿节点 插入到上面创建的domNode上 因为 domNode 是通过当前调用createElement的虚拟节点创建的孤儿节点
domNode.appendChild(chDom);
})
}
// 将新生成孤儿节点 domNode 保存在虚拟节点的elm属性上
vnode.elm = domNode;
//将函数用虚拟节点创建的孤儿节点返回
return vnode.elm
}
这里 我们判断 如果有子节点children就循环 children
一次一次 通过子节点去调用createElement 将字节点的虚拟dom变成孤儿节点 然后将孤儿节点插入到domNode下面
这个就比较奇妙 因为
例如 我们 div下 两个 h1 h1下面又有 带文本的三个p
那么 第一次 div虚拟节点进入 函数这次接到的vnode参数是div虚拟节点domNode是一个div虚拟节点创建的div孤儿节点
然后 我们进入到 vnode.children循环 然后 两个子h1虚拟节点 再去调用createElement
这里分为两个逻辑
首先 h1虚拟节点进入到 createElement 这次 接到的 vnode 就是h1虚拟节点
然后 domNode 就是h1 虚拟节点创建的一个 h1孤儿节点
然后 走到vnode.children循环 三个带文本的p虚拟节点进去createElement
然后 createElement 接到的 vnode 就是p虚拟节点
接到的 domNode 就是p 虚拟节点创建的一个p孤儿节点
然后就会走进vnode.text && (!vnode.children || !vnode.children.length)
文本被写入孤儿节点 然后 每一个 p的createElement 都将创建好的孤儿p节点返回
然后 还是在h1节点调用的createElement 中走到domNode.appendChild(chDom);
chDom是单个p调用createElement返回的p孤儿节点 然后被appendChild插入到domNode后面
domNode 是 h1 调用createElement时的domNode
然后 h1又返回孤儿节点
再到最外面div的createElement 也是一样的逻辑 domNode.appendChild(chDom);
chDom是h1调用createElement 返回的孤儿节点 domNode是当前div节点创建的孤儿节点
最终 div的createElement 返回 走到patch.js的
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);
将整个节点挂上去
运行代码 我们的子节点就上去了
我们可以测试一下它有多级的时候
将src下的 index.js 代码更改如下
import h from "./snabbdom/h";
import patch from "./snabbdom/patch";
const container = document.getElementById("container");
const vnode = h("ul", {}, [
h("li", {}, "1A"),
h("li", {}, "2B"),
h("li", {}, "3C"),
h("li", {}, [
h("div", {}, "SRM"),
])
]);
patch( container, vnode)
跑起来也是没有任何问题
但 目前 patch.js 还少一个逻辑 我们一直说 插入新节点 删除旧节点
所以patch.js最后面加一句话
oldVnode.elm.parentNode.removeChild(oldVnode.elm);
然后 我们试着还原一下 之前写的 更改dom 的功能
我们将 src下的 index.js 代码更爱如下
import h from "./snabbdom/h";
import patch from "./snabbdom/patch";
const container = document.getElementById("container");
const vnode = h("ul", {}, [
h("li", {}, "1A"),
h("li", {}, "2B"),
h("li", {}, "3C"),
h("li", {}, [
h("div", {}, "SRM"),
])
]);
patch( container, vnode)
const btn = document.getElementById("btn");
const vnode1 = h("h1", {}, [
h("p", {}, "你好"),
h("p", {}, "我很好"),
]);
btn.onclick = function(){
patch( vnode, vnode1)
}
这里 我们就有写上了 通过document.getElementById(“btn”)获取按钮节点 然后定义一个它的点击事件
里面的逻辑就是 用vnode1替换vnode
我们运行项目
然后点击一下 更改dom
可以看到 我们的节点替换就也好了