1、前言
到目前为止,我们只实现了向DOM添加内容,所以接下来的目标我们实现更新和删除节点;
当执行更新时,我们要对比两棵fiber树,对有变化的DOM进行更新;
关于协调的原理篇请移步这里;
2、实现步骤
2.1 新增变量
新增 currentRoot 变量,保存根节点更新前的fiber树,添加alternate属性到每一个fiber,关联老的fiber,老fiber是我们上一次提交阶段提交给DOM的fiber;
// 更新前的根节点fiber树let currentRoot = nullfunction render (element, container) {wipRoot = {// 省略alternate: currentRoot}// 省略}function commitRoot () {commitWork(wipRoot.child)currentRoot = wipRootwipRoot = 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 = fiberwhile (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]// 创建fiberconst 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 = newFiberindex++}}
2.3 对比新旧fiber
添加循环条件oldFiber,将newFiber赋值为null;
function reconcileChildren(wipFiber, elements) {// 省略// 上一次渲染的fiberlet oldFiber = wipFiber.alternate && wipFiber.alternate.child// 省略while (index < elements.length || oldFiber != null) {// 省略const newFiber = null// 省略}// 省略}
新旧fiber进行对比,看看是否需要对 DOM 应用进行更改;
function reconcileChildren(wipFiber, elements) {// 省略// 上一次渲染的fiberlet 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) {// 省略// 类型相同只更新propsif (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 = nullfunction 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/indeximport 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
