在绑定的事件中,如果在修改了某些变量,那么视图也应该相对应的发生改变,在 react 中,除了标签本身以外的所有内容都看作 props,所以只需要针对props进行更新即可。
更新props就是针对新旧两棵dom树进行遍历对比。
对比的规则就是判断新旧dom树对应的节点tag是否一样的,如果一样,则表示更新,如果不一样则表示删除、修改。
在对比之前需要考虑这样几个问题:
- 如何获取新的dom树
- 如何找到旧的节点
- 如何进行diff props对比
如何获取新的dom树
在考虑如何获取新的dom树之前,可以想到dom树的获取是通过render函数拿到了根节点的vdom,然后赋值给 nextUnitOfFier 后启动 perfromFiberOfUnit 函数,然后根据相应的规则创建了链表。
同理,在更新的时候可以给 nextUnitOfFier 重新赋值后再次启动 perfromFiberOfUnit 函数,然后就可以获取新的dom树。
// 更新时可以调用update方法。
function update() {
nextUnitOfFier = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot, // 表示旧的节点。
};
root = nextUnitOfFier;
}
在update方法中,我们并不能像render函数一样给他传递el和container,所以可以先将el存储起来。
在commitRoot方法中通过commitWork递归拿到了最终的dom结构,所以可以在这里新建一个变量将root给存储起来,作为update中的el。
function commitRoot() {
commitWork(root.child);
currentRoot = root; // 新建currentRoot变量存储root
root = null;
}
至此,当调用update时,就可以重启perfromFiberOfUnit并拿到新的dom树
如何找到旧的节点
新的dom树创建完成后,我们可以通过添加一个字段 alternate(备用)来表示旧的节点,以此来做一一对应关系,如下图所示:
如何在新旧dom树种建立起这种关系呢?
在处理dom树的时候,我们将dom树转化成了链表,所以可以在处理链表时将alternate关系给表示出来。如下图所示:
所以在创建链表的时候处理:
// 转化成链表,做好指针指向
function initChildren(fiber, children) {
// 先获取旧的节点
let oldFiber = fiber.alternate?.child;
let prevChild = null;
children.forEach((child, index) => {
// 判断旧的tag与新tag是否相同
// true:更新节点
// false:创建、删除节点
const isSameTag = oldFiber && oldFiber.type === child.type;
let newFiber = null;
if (isSameTag) {
// 更新
newFiber = {
type: child.type,
props: child.props,
child: null,
sibling: null,
parent: fiber,
dom: oldFiber.dom, // 更新props,dom没有变化,所以可以使用旧dom
effectTag: "update", // 更新标识
alternate: oldFiber, // 将旧的节点给指向到alternate上建立关系
};
} else {
// 新增
newFiber = {
type: child.type,
props: child.props,
child: null,
sibling: null,
parent: fiber,
dom: null,
effectTag: "placement", // 其他标识
};
}
// 按照创建链表的逻辑,child处理完后应该再处理sibling,所以要将sibling返回
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
// ...
});
}
至此,新旧dom的对应关系就建立完成。
如何进行diff props对比
更新props是在处理props 即函数 updateProps 中进行的,在更新props时可以看做有三种情况:
- new没有 old有 => 表示删除
- new有 old没有 => 表示新增
- new有 old有 => 表示修改
在这三种情况中可以看做有两种处理方式,1 ⇒ 删除属性 2,3 ⇒ 更新属性
/**
*
* @param {*} dom
* @param {*} nextProps 新的props
* @param {*} prevProps 旧的props
*/
function updateProps(dom, nextProps, prevProps) {
// 1. new没有 old有 删除
Object.keys(prevProps).forEach((key) => {
if (!(key in nextProps)) {
if (key !== "children") {
dom.removeAttribute(key);
}
}
});
// 2. new有 old没有 新增
// 3. new有 old有 更新
Object.keys(nextProps).forEach((key) => {
if (nextProps[key] !== prevProps[key]) {
if (key !== "children") {
if (key.startsWith("on")) {
const eventType = key.slice(2).toLocaleLowerCase();
// 在添加事件之前,要把之前的事件给注销掉,否则会重复触发以前的事件
dom.removeEventListener(eventType, prevProps[key]);
dom.addEventListener(eventType, nextProps[key]);
}
dom[key] = nextProps[key];
}
}
});
}
这样就可以在修改变量时调用update方法,实现视图更新。