上集回顾
【React进阶系列】从零开始手把手教你实现一个Virtual DOM(二)
上集我们实现了首次渲染从JSX=>Hyperscript=>VDOM=>DOM的过程,今天我们来看一下当数据变动的时候怎么更新DOM,也就是下图的右半边部分。
改写view()
function view(count) {
const r = [...Array(count).keys()]
return <ul id="filmList" className={`list-${count % 3}`}>
{ r.map(n => <li>item {(count * n).toString()}</li>) }
</ul>
}
我们的view函数接收一个参数count,变量r表示从0到count-1的一个数组。假如count=3, r=[0, 1, 2]。ul的className的值有三种可能:list-0, list-1, list-2。li的数量取决于count。
改写render()
function render(el) {
const initialCount = 0
el.appendChild(createElement(view(initialCount)))
setTimeout(() => tick(el, initialCount), 1000)
}
function tick(el, count) {
const patches = diff(view(count + 1), view(count))
patch(el, patches)
if(count > 5) { return }
setTimeout(() => tick(el, count + 1), 1000)
}
render函数有两个修改,首先调用view()的时候传入count=0。其次,写了一个定时器,1秒后悔执行tick函数。tick函数接收两个参数,el代表节点元素,count是当前计数值。
tick函数依次做了这几件事:
- 调用diff函数,对比新旧两个VDOM,根据两者的不同得到需要修改的补丁
- 将补丁patch到真实DOM上
- 当计数器小于等于5的时候,将count加1,再继续下一次tick
- 当计数器大于5的时候,结束
下面我们来实现diff函数和patch函数。
我们先列出来新旧两个VDOM对比,会有哪些不同。在index.js文件的最前面声明一下几个常量。
const CREATE = 'CREATE' //新增一个节点
const REMOVE = 'REMOVE' //删除原节点
const REPLACE = 'REPLACE' //替换原节点
const UPDATE = 'UPDATE' //检查属性或子节点是否有变化
const SET_PROP = 'SET_PROP' //新增或替换属性
const REMOVE_PROP = 'REMOVE PROP' //删除属性
diff()
function diff(newNode, oldNode) {
if (!oldNode) {
return { type: CREATE, newNode }
}
if (!newNode) {
return { type: REMOVE }
}
if (changed(newNode, oldNode)) {
return { type: REPLACE, newNode }
}
if (newNode.type) {
return {
type: UPDATE,
props: diffProps(newNode, oldNode),
children: diffChildren(newNode, oldNode)
}
}
}
- 假如旧节点不存在,我们返回的patches对象, 类型为新增节点;
- 假如新节点不存在,表示是删除节点;
- 假如两者都存在的话,调用changed函数判断他们是不是有变动;
- 假如两者都存在,且changed()返回false的话,判断新节点是否是VDOM(根据type是否存在来判断的,因为type不存在的话,newNode要么是空节点,要么是字符串)。假如新节点是VDOM,则返回一个patches对象,类型是UPDATE,同时对props和children分别进行diffProps和diffChildren操作。
下面我们一次看一下changed, diffProps, diffChildren函数。
changed()
function changed(node1, node2) {
return typeof(node1) !== typeof(node2) ||
typeof(node1) === 'string' && node1 !== node2 ||
node1.type !== node2.type
}
检查新旧VDOM是否有变动的方法很简单,
- 首先假如数据类型都不一样,那肯定是变动了;
- 其次假如两者的类型都是纯文本,则直接比较两者是否相等;
- 最后比较两者的类型是否相等。
diffProps()
function diffProps(newNode, oldNode) {
let patches = []
let props = Object.assign({}, newNode.props, oldNode.props)
Object.keys(props).forEach(key => {
const newVal = newNode.props[key]
const oldVal = oldNode.props[key]
if (!newVal) {
patches.push({type: REMOVE_PROP, key, value: oldVal})
}
if (!oldVal || newVal !== oldVal) {
patches.push({ type: SET_PROP, key, value: newVal})
}
})
return patches
}
比较新旧VDOM的属性的变化,并返回相应的patches。
- 首先我们采用最大可能性原则,将新旧VDOM的所有属性都合并赋值给一个新的变量props
- 遍历props变量的所有Keys,依次比较新旧VDOM对于这个KEY的值
- 假如新值不存在,表示这个属性被删除了
- 假如旧值不存在,或者新旧值不同,则表示我们需要重新设置这个属性
diffChildren()
function diffChildren(newNode, oldNode) {
let patches = []
const maximumLength = Math.max(
newNode.children.length,
oldNode.children.length
)
for(let i = 0; i < maximumLength; i++) {
patches[i] = diff(
newNode.children[i],
oldNode.children[i]
)
}
return patches
}
同样采用最大可能性原则,取新旧VDOM的children的最长值作为遍历children的长度。然后依次比较新旧VDOM的在相同INDEX下的每一个child。
这里需要强烈注意一下
为了简化,我们没有引入key的概念,直接比较的是相同index下的child。所以假如说一个列表ul有5项,分别是li1, li2, li3, li4, li5; 如果我们删掉了第一项,新的变成了li2, li3, li4, li5。那么diffchildren的时候,我们会拿li1和li2比较,依次类推。这样一来,本来只是删除了li1, 而li2, li3, li4, li5没有任何变化,我们得出的diff结论却是[li替换,li2替换, li3替换, li4替换, li5删除]。所以react让大家渲染列表的时候,必须添加Key。
截止到现在,我们已经得到了我们需要的补丁。下面我们要将补丁Patch到DOM里。
patch()
function patch(parent, patches, index = 0) {
if (!patches) {
return
}
const el = parent.childNodes[index]
switch (patches.type) {
case CREATE: {
const { newNode } = patches
const newEl = createElement(newNode)
parent.appendChild(newEl)
break
}
case REMOVE: {
parent.removeChild(el)
break
}
case REPLACE: {
const {newNode} = patches
const newEl = createElement(newNode)
return parent.replaceChild(newEl, el)
break
}
case UPDATE: {
const {props, children} = patches
patchProps(el, props)
for(let i = 0; i < children.length; i++) {
patch(el, children[i], i)
}
}
}
}
- 首先当patches不存在时,直接return,不进行任何操作
- 利用childNodes和Index取出当前正在处理的这个节点,赋值为el
- 开始判断补丁的类型
- 当类型是CREATE时,生成一个新节点,并append到根节点
- 当类型是REMOVE时,直接删除当前节点el
- 当类型是REPLACE时,生成新节点,同时替换掉原节点
- 当类型是UPDATE时,需要我们特殊处理
- 调用patchProps将我们之前diffProps得到的补丁渲染到节点上
- 遍历之前diffChildren得到的补丁列表,再依次递归调用patch
最后我们再补充一下patchProps函数
patchProps
function patchProps(parent, patches) {
patches.forEach(patch => {
const { type, key, value } = patch
if (type === 'SET_PROP') {
setProp(parent, key, value)
}
if (type === 'REMOVE_PROP') {
removeProp(parent, key, value)
}
})
}
function removeProp(target, name, value) { //@
if (name === 'className') {
return target.removeAttribute('class')
}
target.removeAttribute(name)
}
这个就不用我解释了,代码很直观,setProp函数在上一集我们已经定义过了。这样一来,我们就完成了整个数据更新导致DOM更新的完整过程。
npm run compile后打开浏览器查看效果,你应该看到是一个背景颜色在不同变化,同时列表项在逐渐增加的列表。
完结撒花
至此,我们的VDOM就全部完成了。系列初我提出的那几个问题不知道你现在是否有了答案。有答案的童鞋可以在文章评论区将你的见解跟大家分享一下。分析全面且准确的会收到我的特殊奖励。😁😁😁😁
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。