1. 比较新旧节点
(1)想要比较新旧节点,前提是需要获得最新的Dom树,那么如何得到?
1. 我们定义的render函数里边的全局变量root其实就是一个根Dom树,但是root的使用处太广泛了,它还牵扯到了我们在执行提交代码将链表结构递归触发渲染视图逻辑并且处理完后将root重置为null了。所以我们需要重新定义一个新的全局变量currentRoot,在我们执行commitRoot方法中递归处理完所有dom树并渲染视图后,将root的值赋值给currentRoot,这样currentRoot就是一个最新的Dom树。
2. 想要生成最新的dom树,逻辑其实和定义的render函数逻辑是一样的,我们copy并未其重命名为update函数,但是更新逻辑上由于此时我们的dom是最新的dom树,所以我们无需接收el和container,可以直接获取我们定义的全局变量currentRoot的dom以及props。继而对root最后更新为最新的一颗Dom树。
3. 那么接下来就是如何找到老的节点?假如我们要处理新Dom树上的div,那么在老Dom树上可以通过笨方法从root根节点往下依次遍历直至找到div,比较更新,太过麻烦,影响性能。那么其实我们在将Dom树转化为链表的时候,可以创建一个属性去指向老的链表上的节点,这样就可以依次进行对比了,这样性能就会得到提升。
4. 这一操作可以在Dom树转化链表的过程中进行实现,设置一个alternate(交替的意思)属性代表一个指针,去指向老的节点,那么后续处理后边的节点时,我们可以通过child去指向下一个节点,当后续没有child节点时,我们可以寻找它的兄弟sibling节点,并且指向他,依次进行操作。这里我画了一个图便于大家理解:
5. 所以我们要在initChildren转换链表,设置指针的这个方法去做新旧节点的对比。第一次对比Dom树孩子节点App,我们通过alternate.child取到旧Dom树与之对应的节点进行对比判断。对比的策略就是标签是否发生改变,如果没有改变则是更新,反之则是创建删除。那么我们通过这个条件进行判断在遍历children里边,并且我们要为更新和创建里边的newFiber进行标识的一个区分,以及在更新时的newFiber里的dom属性设置为oldFiber的dom,因为在更新时我们是无需重新创建Dom的,并未其添加alternate属性将其指针指向与之对应的旧Dom树的节点,这一步是关键,并且在最后我们需要考虑到如果他有多个child节点,index等于0时处理的是第一个child节点,index=1的时候就要处理第二个,此时我们的指针应该指向第一个child的兄弟节点。上方之所以加标识就是为了在commitWork提交代码渲染视图时根据这个标识判断是添加还是更新。
代码如下:
function initChildren(fiber,children) {
let oldFiber = fiber.alternate?.child
let prevChild = null
children.forEach((child,index) => {
const isSameType = oldFiber && oldFiber.type === child.type
let newFiber;
if(isSameType) {
// update
newFiber = {
type:child.type,
props:child.props,
child:null,
parent:fiber,
sibling:null,
dom:oldFiber.dom,
effectTag:"update",
alternate:oldFiber,
}
} else {
/*
由于我们要给孩子节点定义父亲等属性,所以定义新的对象,
这样可以不直接破坏之前vdom的结构
*/
newFiber = {
type:child.type,
props:child.props,
child:null,
parent:fiber,
sibling:null,
dom:null,
effectTag:"placement"
}
}
// 将指针指向child的兄弟节点
if(oldFiber) {
oldFiber = oldFiber.sibling
}
if(index == 0) {
fiber.child = newFiber
} else {
prevChild.sibling = newFiber
}
prevChild = newFiber
});
}
2. 更新props
(1)那么更新props的逻辑应该在commitWork里的渲染视图前进行判断,通过我们定义的标识判断是更新props还是添加。
(2)更新props里,之前我们定义的只是实现了初始化的过程,那么这一次我们对比新旧props的情况。有三种:1.老的存在,新的不存在,删除 2.新的存在,老的不存在,创建 3.新的存在,老的也存在,更新。其实仔细想想我们会发现第二种和第三种完全可以合为一种去实现,都是通过对比props,如果不同,直接赋值即可。
代码如下:
function updateProps(dom,nextProps,prevProps) {
// 1. 老的存在,新的不存在 删除
if(prevProps) {
Object.keys(prevProps).forEach(key => {
if(key !== 'children') {
if(!(key in nextProps)) {
dom.removeAttribute(key)
}
}
})
}
// 2. 新的存在,老的不存在,创建
// 3. 新的存在,老的也存在,更新
if(nextProps) {
Object.keys(nextProps).forEach(key => {
if(key !== 'children') {
if(nextProps[key] !== prevProps[key]) {
if(key.startsWith("on")) {
// 获取事件类型
let eventType = key.slice(2).toLocaleLowerCase()
// 删除上一次的事件
dom.removeEventListener(eventType,prevProps[key])
dom.addEventListener(eventType,nextProps[key])
} else {
dom[key] = nextProps[key]
}
}
}
})
}
}
(3)此时我们可以去触发一下我们的更新,在App.jsx里,写一些测试然后通过调用我们定义的update函数,当然大家可能开发时并没有使用过update,它就是后期的useState,我们采用小步骤的方法后期再一点一点实现。这次的代码实现比较绕,如果写的步骤中,有不理解的地方,可以通过打断点或者画图理清思路。
3. 重构代码
(1)重命名我们定义的root变量,它代表其实是一个正在工作中的根节点(work in progress),将其命名为wipRoot,以及在render和update函数中,将对象直接创建在wipRoot上会更合适一些。
function render(el,container) {
wipRoot = {
dom:container,
props:{
children:[el]
}
}
nextWorkOfUnit = wipRoot
}
function update() {
wipRoot = {
dom:currentRoot.dom,
props:currentRoot.props,
alternate:currentRoot
}
nextWorkOfUnit = wipRoot
}
(2)由于我们的initChildren函数多了很多新的逻辑,这里我们采用react源码的命名,将其改为reconcileChildren,更加的有语义化。