1、前言
到目前为止,我们只实现了向DOM添加内容,所以接下来的目标我们实现更新和删除节点;
当执行更新时,我们要对比两棵fiber树,对有变化的DOM进行更新;
关于协调的原理篇请移步这里;
2、实现步骤
2.1 新增变量
新增 currentRoot 变量,保存根节点更新前的fiber树,添加alternate属性到每一个fiber,关联老的fiber,老fiber是我们上一次提交阶段提交给DOM的fiber;
// 更新前的根节点fiber树
let currentRoot = null
function render (element, container) {
wipRoot = {
// 省略
alternate: currentRoot
}
// 省略
}
function commitRoot () {
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
2.2 新建reconcileChildren并提取performUnitOfWork中的逻辑
提取创建新fiber的代码到reconcileChildren中;
performUnitOfWork代码更改:
/**
* 处理工作单元,返回下一个单元事件
* @param {*} fiber
*/
function performUnitOfWork(fiber) {
// 如果fiber上没有dom节点,为其创建一个
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// 获取到当前fiber的孩子节点
const elements = fiber.props.children
// 协调
reconcileChildren(fiber, elements)
// 寻找下一个孩子节点,如果有返回
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
// 如果有兄弟节点,返回兄弟节点
if (nextFiber.sibling) {
return nextFiber.sibling
}
// 否则返回父节点
nextFiber = nextFiber.parent
}
}
reconcileChildren代码:
/**
* 协调
* @param {*} wipFiber
* @param {*} elements
*/
function reconcileChildren(wipFiber,elements){
// 索引
let index = 0
// 上一个兄弟节点
let prevSibling = null
// 遍历孩子节点
while (index < elements.length) {
const element = elements[index]
// 创建fiber
const newFiber = {
type: element.type,
props: element.props,
parent: wipFiber,
dom: null,
}
// 将第一个孩子节点设置为 fiber 的子节点
if (index === 0) {
wipFiber.child = newFiber
} else if (element) {
// 第一个之外的子节点设置为第一个子节点的兄弟节点
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
2.3 对比新旧fiber
添加循环条件oldFiber,将newFiber赋值为null;
function reconcileChildren(wipFiber, elements) {
// 省略
// 上一次渲染的fiber
let oldFiber = wipFiber.alternate && wipFiber.alternate.child
// 省略
while (index < elements.length || oldFiber != null) {
// 省略
const newFiber = null
// 省略
}
// 省略
}
新旧fiber进行对比,看看是否需要对 DOM 应用进行更改;
function reconcileChildren(wipFiber, elements) {
// 省略
// 上一次渲染的fiber
let oldFiber = wipFiber.alternate && wipFiber.alternate.child
// 省略
while (index < elements.length || oldFiber != null) {
// 省略
// 类型判断
const sameType = oldFiber && element && element.type == oldFiber.type
// 类型相同需要更新
if (sameType) {
// TODO update the node
}
// 新的存在并且类型和老的不同需要新增
if (element && !sameType) {
// TODO add this node
}
// 老的存在并且类型和新的不同需要移除
if (oldFiber && !sameType) {
// TODO delete the oldFiber's node
}
// 处理老fiber的兄弟节点
if (oldFiber) {
oldFiber = oldFiber.sibling
}
// 省略
}
// 省略
}
当类型相同时,创建一个新的fiber,保留旧的fiber的dom节点,更新props,此外还加入一个effectTag属性来标识当前执行状态;
function reconcileChildren(wipFiber, elements) {
while (index < elements.length || oldFiber != null) {
// 省略
// 类型相同只更新props
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}
// 省略
}
对于元需要一个新的 DOM 节点的情况,我们用 PLACEMENT effect 标签标记新的fiber;
function reconcileChildren(wipFiber, elements) {
while (index < elements.length || oldFiber != null) {
// 省略
// 新的存在并且类型和老的不同需要新增
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
// 省略
}
对于需要删除节点的情况,没有新fiber,将 effect 标签添加到旧的fiber中,删除旧的fiber;
function reconcileChildren(wipFiber, elements) {
while (index < elements.length || oldFiber != null) {
// 省略
// 老的存在并且类型和新的不同需要移除
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
// 省略
}
设置一个数组来存储需要删除的节点;
let deletions = null
function render(element, container) {
// 省略
deletions = []
// 省略
}
渲染DOM时,遍历删除旧节点;
function commitRoot() {
deletions.forEach(commitWork)
// 省略
}
修改commitWork处理effectTag标记,处理新增节点(PLACEMENT);
function commitWork(fiber) {
// 省略
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
}
// 省略
}
处理删除节点标记;
function commitWork(fiber) {
// 省略
// 处理删除节点标记
else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}
// 省略
}
处理更新节点,加入updateDom方法,更新props属性;
function updateDom(){
}
function commitWork(fiber) {
// 省略
// 处理删除节点标记
else if (
fiber.effectTag === "UPDATE" &&
fiber.dom != null
) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
}
// 省略
}
updateDom方法根据不同的更新类型,对props更新;
const isProperty = key => key !== "children"
// 是否有新属性
const isNew = (prev, next) => key =>
prev[key] !== next[key]
// 是否是旧属性
const isGone = (prev, next) => key => !(key in next)
/**
* 更新dom属性
* @param {*} dom
* @param {*} prevProps 老属性
* @param {*} nextProps 新属性
*/
function updateDom(dom, prevProps, nextProps) {
// 移除老的属性
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// 设置新的属性
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
}
修改一下createDom方法,将更新属性逻辑修改为updateDom方法调用;
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
updateDom(dom, {}, fiber.props)
return dom
}
添加是否为事件监听,以on开头,并修改isProperty方法;
const isEvent = key => key.startsWith("on")
const isProperty = key =>
key !== "children" && !isEvent(key)
修改updateDom方法,处理事件监听,并从节点移除;
function updateDom(dom, prevProps, nextProps) {
// 移除老的事件监听
Object.keys(prevProps)
.filter(isEvent)
.filter(
key =>
!(key in nextProps) ||
isNew(prevProps, nextProps)(key)
)
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.removeEventListener(
eventType,
prevProps[name]
)
})
// 省略
}
添加新的事件监听;
function updateDom(dom, prevProps, nextProps) {
// 添加新的事件处理
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
// 省略
}
3、实现效果
修改src/index.js代码:
// src/index
import React from '../react';
const container = document.getElementById("root")
const updateValue = e => {
rerender(e.target.value)
}
const rerender = value => {
const element = (
<div>
<input onInput={updateValue} value={value} />
<h2>Hello {value}</h2>
</div>
)
React.render(element, container)
}
rerender("World")
4、本节代码
代码地址:https://github.com/linhexs/mini-react/tree/6.reconcileChildren