现在,我们拥有了基于实体dom的ToyReact,可喜可贺
✿✿ヽ(°▽°)ノ✿
但是,react的核心是虚拟dom,毕竟有了虚拟dom,我们的组件刷新才更高效嘛
那么,我们来实现这个功能吧
1. 实现虚拟dom树的创建及渲染
要支持虚拟dom,我们需要重构ElementWrapper、TextWrapper以及Component,毕竟它们通过root这一实际dom进行渲染和更新
1.1 ElementWrapper的vdom实现
在ElementWrapper中我们增加vdom的get
get vdom () {
return {
type: this.type,
props: this.props,
// 拿到每个child的vdom
children: this.children.map(child => child.vdom)
}
}
在setAtrribute方法中,我们存储了this.props,而在appendChild中,我们存储了this.children。这两种方法的逻辑,和Component有重合,所以ElementWrapper和TextWrapper可以继承Component
1.2 TextWrapper的vdom实现
get vdom () {
return {
type: "#text",
content: this.content
}
}
1.3 Component的vdom实现
get vdom () {
return this.render().vdom
}
1.4 vdom到实体dom的patch
我们之前在ElementWrapper中设置的setAtrribute和appendChild方法,实际创建了需要渲染的dom,而实现vdom到实体dom的patch的过程,这两个方法可以删除,其逻辑可以合并到[RENDER_TO_DOM]方法中。
因为基于vdom,所以保存实际dom的this.root属性可以删去,实际dom只在[RENDER_TO_DOM]方法中存在
在删除setAtrribute和appendChild方法之后,因为 get vdom 的操作只返回了对象,并没有返回方法,无法重绘,所以ElementWrapper和TextWrapper的get vdom的正解应是:
// ElementWrapper
get vdom () {
this.vchildren = this.children.map(child => child.vdom)
return this
}
// TextWrapper
get vdom () {
return this
}
那么,vdom的对象属性,就是ElementWrapper和TextWrapper中的属性。个人理解,这里将数据和行为进一步解耦。
1.4.1 [RENDER_TO_DOM]方法中patch的实现
// ElementWrapper
[RENDER_TO_DOM] (range) {
range.deleteContents()
// 创建实体dom,root
let root = document.createElement(this.type)
// props内容抄写,setAttribute逻辑的实现
for (let name in this.props) {
let value = this.props[name]
if (name.match(/^on([\s\S]+)/)){
root.addEventListener(RegExp.$1.replace(/^[\s\S]/, c => c.toLowerCase()), value)
} else{
if (name === "className"){
root.setAttribute("class", value)
} else {
root.setAttribute(name, value)
}
}
}
// children的处理
for (let child of this.children) {
let childRange = document.createRange()
// 将新增的元素置于range末尾
childRange.setStart(root, root.childNodes.length)
childRange.setEnd(root, root.childNodes.length)
child[RENDER_TO_DOM](childRange)
}
//挂载 root
range.insertNode(root)
}
1.5 修复bug
现在,我们的vdom树已经可以建起来啦~
不过还是留了一些尾巴,特别在于vchildren的处理上,这部分还需要优化,需要去掉Component的vchildren getter
对于ElementWrapper和TextWrapper,我们需要在在[RENDER_TO_DOM]方法中保存之前的range
// TextWrapper
[RENDER_TO_DOM] (range) {
this._range = range
let root = document.createTextNode(this.content)
range.deleteContents()
range.insertNode(root)
}
由于ElementWrapper和TextWrapper的[RENDER_TO_DOM]依旧采用先删除range,再渲染this.root的方式,会导致空range被吞,所以我们要优化[RENDER_TO_DOM]。而[RENDER_TO_DOM]的逻辑在不同位置都有使用,所以我们单独封装一个repalceContent方法
function replaceContent (range, node) {
// 将node插入range,此时node在range的最前位置
range.insertNode(node)
// range挪到node之后
range.setStartAfter(node)
// 清空range
range.deleteContents()
// 重设range的位置
range.setStartBefore(node)
range.setEndAfter(node)
}
现在我们来改进[RENDER_TO_DOM]中range的处理
// TextWrapper
[RENDER_TO_DOM] (range) {
let root = document.createTextNode(this.content)
this._range = range
replaceContent(range, root)
}
// ElementWrapper
[RENDER_TO_DOM] (range) {
this._range = range
// 通过replaceContent代替初始时range.deleteContents()
// range.deleteContents()
let root = document.createElement(this.type)
for (let name in this.props) {
let value = this.props[name]
if (name.match(/^on([\s\S]+)/)){
root.addEventListener(RegExp.$1.replace(/^[\s\S]/, c => c.toLowerCase()), value)
} else{
if (name === "className"){
root.setAttribute("class", value)
} else {
root.setAttribute(name, value)
}
}
}
for (let child of this.children) {
let childRange = document.createRange()
childRange.setStart(root, root.childNodes.length)
childRange.setEnd(root, root.childNodes.length)
child[RENDER_TO_DOM](childRange)
}
// 完成root的挂载
replaceContent(range, root)
}
2. vdom比对的实现
在拥有了vdom树的创建能力之后,我们的rerender函数可以退休了,代替它的是vdom更新,在Component基类中实现
在Component的[RENDER_TO_DOM]方法中,我们需要先更新vdom,再渲染
// Component
[RENDER_TO_DOM] (range) {
// 保存range和vdom
this._range = range
// 由于this.vdom是getter,所以会重新调用组件的render方法,返回新的vdom,实现vdom更新
this._vdom = this.vdom
// 渲染旧的vdom
this._vdom[RENDER_TO_DOM](range)
}
2.1 vdom更新
在Component的unpdate方法中,我们实现vdom的更新
update () {
let update = (oldNode, newNode) => {
// 更新为newNode
}
// 保存新的vdom
let vdom = this.vdom
// 对比vdom
update(this._vdom, vdom)
// 重新赋值
this._vdom = vdom
}
那么在实现更新vdom功能前,我们需要对比根节点,细化vdom更新范围
2.2 简单的 dom diff 算法
我们的节点更新逻辑如下:
- 根节点的type是否一致,不一致,则更新为新的根节点
- 根节点的props是否一致,不一致,则更新为新的根节点
- 根节点的children是否一致,不一致,则更新为新的根节点
- 对于文本节点,content不一致,则更新为新节点
我们采用最土,最傻瓜最简练的方式更新dom
- 只对比相同位置的vdom
- 采用直接替换的方式更新节点
update () {
let isSameNode = (oldNode, newNode) => {
// type不同,则为不同节点
if (oldNode.type !== newNode.type) {
return false
}
// props不同,则为不同节点
for ( let name in newNode.props) {
// 属性值要相同
if (newNode.props[name] !== oldNode.props[name]) {
return false
}
}
// props的长度不相同,节点不相同
if (Object.keys(oldNode.props).length > Object.keys(newNode.props)) {
return false
}
// 文本节点,比对content
if (newNode.type === "#text") {
if (newNode.content !== oldNode.content) {
return false
}
}
return true
}
let update = () => {}
// 保存新的vdom
let vdom = this.vdom
// 对比vdom
update(this._vdom, vdom)
// 重新赋值
this._vdom = vdom
}
2.3 newNode更新
update () {
let isSameNode = (oldNode, newNode) => {
... ...
}
let update = (oldNode, newNode) => {
// 根节点不同,则全部重新渲染
if (!isSameNode(oldNode, newNode)) {
// 替换oldNode
newNode[RENDER_TO_DOM](oldNode._range)
return
}
newNode._range = oldNode._range
// children的处理
// 因为children属性是实体dom,所以我们要拿到vchildren
let newChildren = newNode.vchildren
let oldChildren = oldNode.vchildren
if (!newChildren || !newChildren.length) {
return
}
// 记录oldChildren的尾部位置
let tailRange = oldChildren[oldChildren.length - 1]._range
// 两个数组一起循环,所以不用 for of循环
for (let i = 0; i < newChildren.length; i++) {
let newChild = newChildren[i]
let oldChild = oldChildren[i]
if (i < oldChildren.length) {
update(oldChild, newChild)
} else {
// 如果newChild比oldChild元素多,我们需要在newChild进行节点插入
// 创建一个需要插入的range
let range = document.createRange()
range.setStart(tailRange.endContainer, tailRange.endOffset)
range.setEnd(tailRange.endContainer, tailRange.endOffset)
newChild[RENDER_TO_DOM](range)
tailRange = range
}
}
}
let vdom = this.vdom
// 更新vdom
update(this._vdom, vdom)
// 重新赋值, 此时的“旧vdom”是vdom
this._vdom = vdom
}
之后在setState中调用update,即可实现虚拟dom的更新
至此,塑料版react toyReact就完成啦
✿✿ヽ(°▽°)ノ✿