9.update props

这节课我们来实现一下mini-react中的更新props。

第一步. 我们需要得到新的Dom树

  • 更新想要获得Dom树可以和初始化一样通过render函数

  • 但是这里更新,需要知道自动root和container,不需要用户传进来

    • 通过增加currentRoot来得到root
tsx 复制代码
let currentRoot = null
function commitRoot() {
    commitWork(root.child)
    currentRoot = root
    root = null
}
​
function update() {
    nextWorkOfUnit = {
        dom: currentRoot.dom,
        props: currentRoot.props
    }
    root = nextWorkOfUnit
}

第二步. 找到老的节点

  • 笨办法,依次去遍历树
  • 通过altermate指针建立对比关系,考虑到复杂的问题目前值做更新,删除,增加之后再做
  • 我们先按照如果标签一样就是更新来处理
tsx 复制代码
let currentRoot = null
​
function commitRoot() {
    commitWork(root.child)
    currentRoot = root
    root = null
}
​
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 {
            newFiber = {
                type: child.type,
                props: child.props,
                child: null,
                parent: fiber,
                sibling: null,
                dom: null,
                effectTag: "placement"
            }
        }
​
        if(oldFilber) {
            oldFiber = oldFiber.sibling
        }
​
        if (index === 0) {
            fiber.child = newFiber
        } else {
            prevChild.sibling = newFiber
        }
        prevChild = newFiber
    })
}
​
function update() {
    nextWorkOfUnit = {
        dom: currentRoot.dom,
        props: currentRoot.props,
        alternate: currentRoot
    }
    root = nextWorkOfUnit
}
​
  • 我们来用菜市场买菜的例子来解释一下上述代码,让它更容易理解:

🥦 菜市场故事背景:

  1. :菜摊老板(相当于 React 的协调器)
  2. 你的菜摊 :当前展示给顾客的摊位(对应 fiber
  3. 旧进货单 :昨天的蔬菜摆放记录(对应 oldFiber = fiber.alternate?.child
  4. 新进货单 :今天要摆放的蔬菜清单(对应 children 数组)
  5. 蔬菜位置牌 :每个蔬菜的位置标识(对应 newFiber

📜 代码步骤的故事化解释:

tsx 复制代码
function initChildren(fiber, children) {
    // 拿出昨天的进货单(旧蔬菜摆放记录)
    let oldFiber = fiber.alternate?.child;
    // 手里还没拿任何蔬菜位置牌
    let prevChild = null;

故事 :今天开摊前,你拿出昨天的进货单(oldFiber),准备按照今天的进货清单(children)重新摆放蔬菜。

tsx 复制代码
    // 按照今天的进货清单逐一处理
    children.forEach((child, index) => {
        // 检查今天要摆的蔬菜和昨天的是否同一品种
        const isSameType = oldFiber && oldFiber.type === child.type;

故事:你拿起清单上第一个蔬菜(比如西红柿),看看昨天的进货单:昨天这个位置放的是不是也是西红柿?

tsx 复制代码
        let newFiber;
        if (isSameType) {
            // 更新:同一品种蔬菜
            newFiber = {
                type: child.type,       // 还是西红柿
                props: child.props,     // 但可能有新属性(比如价格变了)
                dom: oldFiber.dom,      // 用昨天的同一块位置牌
                effectTag: "update",    // 标记"需要更新价格牌"
                alternate: oldFiber     // 记录昨天的西红柿位置
            };
        } else {
            // 新增:不同品种蔬菜
            newFiber = {
                type: child.type,       // 新品种(比如今天改卖黄瓜)
                props: child.props,     // 新属性(黄瓜的价格)
                dom: null,             // 需要新的位置牌
                effectTag: "placement" // 标记"需要新位置"
            };
        }

故事

  • 如果是同品种蔬菜(都是西红柿):你拿出昨天的西红柿位置牌,贴上"更新"标签(因为价格可能变了),并记下昨天的位置
  • 如果是新品种蔬菜(昨天西红柿→今天黄瓜):你准备一个空白位置牌,贴上"新位置"标签
ini 复制代码
        // 翻到进货单下一页(准备检查下一个蔬菜)
        if (oldFiber) {
            oldFiber = oldFiber.sibling;
        }

故事:处理完一个蔬菜后,你把昨天的进货单翻到下一页(准备检查下一个位置昨天放的是什么菜)

tsx 复制代码
        // 摆放蔬菜位置牌
        if (index === 0) {
            fiber.child = newFiber;  // 第一块牌子挂在摊位入口
        } else {
            prevChild.sibling = newFiber; // 后续牌子挨着前一个挂
        }
        prevChild = newFiber;  // 记住当前挂的牌子
    });
}

故事

  • 第一个蔬菜(西红柿)的位置牌挂在摊位最前面(fiber.child
  • 第二个蔬菜(黄瓜)的位置牌挂在西红柿牌子旁边(prevChild.sibling
  • 第三个蔬菜(土豆)的牌子挂在黄瓜牌子旁边(以此类推)

🌟 完整故事流程:

假设今天进货单是:[西红柿, 黄瓜, 土豆]

  1. 处理西红柿

    • 查看昨天第一个位置:也是西红柿(isSameType=true
    • 拿出昨天的西红柿位置牌,贴上"更新"标签
    • 把牌子挂在摊位入口处
    • 把昨天的进货单翻到第二页(准备看昨天第二个位置是什么)
  2. 处理黄瓜

    • 查看昨天第二个位置:假设是胡萝卜(类型不同!)
    • 准备新位置牌,贴上"新位置"标签
    • 把黄瓜牌子挂在西红柿牌子旁边
    • 进货单翻到第三页
  3. 处理土豆

    • 昨天第三个位置:假设是空的
    • 准备新位置牌,贴上"新位置"标签
    • 把土豆牌子挂在黄瓜牌子旁边

📌 关键点总结:

代码概念 菜市场比喻 作用
oldFiber 昨天的进货单 参考之前的布局
children 今天的进货清单 当前要展示的布局
isSameType 检查是否同一蔬菜品种 决定复用还是新建
effectTag "更新"或"新位置"标签 标记后续如何处理这个位置
alternate 记录昨天的对应位置 保存旧状态用于比较
fiber.child 摊位第一个位置牌 建立父节点与第一个子节点的链接
prevChild.sibling 相邻蔬菜的位置牌挨着挂 建立兄弟节点之间的链表关系

老板最后得到一套完整的位置牌链:

scss 复制代码
摊位入口 → [西红柿] → [黄瓜] → [土豆]
           (更新)    (新位置) (新位置)

等顾客来了(渲染时),你根据这些标签:

  • 更新"标签的:只换价格牌内容
  • "新位置"标签的:挂上新位置牌
  • (实际React还会处理"删除"标签,这里简化了)

这样就能高效更新菜摊,避免全部重新布置!

更新commitWork和重构updateProps

tsx 复制代码
​
function commitWork(fiber) {
    if (!fiber) {
        return
    }
    let fiberParent = fiber.parent
    while (!fiberParent.dom) {
        fiberParent = fiberParent.parent
    }
    if (fiber.effectTag === 'update') {
        updateProps(fiber.dom, fiber.props, fiber.alternate?.props)
    } else if (fiber.effectTag === 'placement') {
        if (fiber.dom) {
            fiberParent.dom.append(fiber.dom)
        }
    }
​
    commitWork(fiber.child)
    commitWork(fiber.sibling)
}
​
​
function updateProps(dom, nextProps, prevProps) {
    // 1. old 有 new 没有 删除
    Object.keys(prevProps).forEach((key) => {
        if (key !== "children") {
            if (!(key in nextProps)) {
                dom.removeAttribute(key)
            }
        }
    })
    // 2. new 有 old 没有 添加
    // 3. new 有 old 有 修改,2和3其实是一种情况
    Object.keys(nextProps).forEach((key) => {
        if (key !== "children") {
            if (nextProps[key] !== prevProps[key]) {
                if (key.startsWith("on")) {
                    const eventType = key.slice(2).toLowerCase()
                    dom.addEventListener(eventType, nextProps[key])
                } else {
                    dom[key] = nextProps[key];
                }
            }
        }
    });
}
    
这两段代码是什么意思 结合上述菜市场卖菜的例子解释一下
  • 这里我们也用菜市场卖菜的例子解释一下以上代码

菜市场收摊开张:React提交阶段的完整故事

让我用菜市场的比喻重新完整解释这两段代码,确保故事更清晰易懂:

🥦 菜市场故事背景回顾

  • :菜摊老板(React的协调器)
  • 你的菜摊:当前展示给顾客的摊位(DOM树)
  • 新布局计划:基于新进货单制定的摊位布局(Fiber树)
  • 位置牌:每个蔬菜的价格/信息牌(DOM节点)
  • 标签:贴在位置牌上的操作指令(effectTag)

📜 第一段代码:commitWork(fiber) - 摊位实际布置

tsx 复制代码
function commitWork(fiber) {
    if (!fiber) {
        return
    }
    // 找到有实际摊位的上级(跳过区域管理员)
    let fiberParent = fiber.parent
    while (!fiberParent.dom) {
        fiberParent = fiberParent.parent
    }
    
    // 根据不同标签执行操作
    if (fiber.effectTag === 'update') {
        // 更新现有位置牌内容
        updateProps(fiber.dom, fiber.props, fiber.alternate?.props)
    } else if (fiber.effectTag === 'placement') {
        // 挂上新位置牌
        if (fiber.dom) {
            fiberParent.dom.append(fiber.dom)
        }
    }
​
    // 继续处理子区域和相邻区域
    commitWork(fiber.child)
    commitWork(fiber.sibling)
}

菜市场故事版 - 摊位布置流程:

  1. 准备收摊:今天营业结束,你要更新摊位

    • 顾客已经离开(浏览器渲染完成)
    • 新布局计划已准备好(Fiber树构建完成)
  2. 找到真正的摊位

    ini 复制代码
    let fiberParent = fiber.parent
    while (!fiberParent.dom) {
        fiberParent = fiberParent.parent
    }
    • 你(蔬菜区主管)要挂位置牌,但有些区域没有实际摊位
    • 你向上级汇报,直到找到真正有摊位的张老板(有DOM的父节点)
    • 例子:西红柿区 → 蔬菜区 → 张老板的实体摊位
  3. 处理"更新"标签

    ini 复制代码
    if (fiber.effectTag === 'update') {
        updateProps(fiber.dom, fiber.props, fiber.alternate?.props)
    }
    • 拿起贴有"更新"标签的位置牌(如西红柿牌)
    • 对比昨天的价格牌(fiber.alternate?.props)和今天的(fiber.props
    • 只修改变化的部分(调用updateProps
  4. 处理"新位置"标签

    go 复制代码
    else if (fiber.effectTag === 'placement') {
        if (fiber.dom) {
            fiberParent.dom.append(fiber.dom)
        }
    }
    • 拿起全新的位置牌(如黄瓜牌)
    • 直接挂到摊位的相应位置(DOM添加操作)
  5. 处理相邻区域

    scss 复制代码
    commitWork(fiber.child)
    commitWork(fiber.sibling)
    • 处理完当前区域后,继续处理:

      • 子区域(fiber.child)→ 比如西红柿区里的小番茄区
      • 兄弟区域(fiber.sibling)→ 比如西红柿区旁边的黄瓜区

老板的行动顺序:先处理自己区域 → 再处理子区域 → 最后处理旁边的兄弟区域

📋 第二段代码:updateProps() - 更新价格牌内容

scss 复制代码
function updateProps(dom, nextProps, prevProps) {
    // 1. 移除旧内容:昨天有今天没有的信息
    Object.keys(prevProps).forEach((key) => {
        if (key !== "children") {
            if (!(key in nextProps)) {
                dom.removeAttribute(key)
            }
        }
    })
    
    // 2. 添加/更新内容
    Object.keys(nextProps).forEach((key) => {
        if (key !== "children") {
            if (nextProps[key] !== prevProps[key]) {
                if (key.startsWith("on")) {
                    // 处理事件
                    const eventType = key.slice(2).toLowerCase()
                    dom.addEventListener(eventType, nextProps[key])
                } else {
                    // 更新普通信息
                    dom[key] = nextProps[key];
                }
            }
        }
    });
}

菜市场故事版 - 价格牌更新细节:

场景:你要更新西红柿位置牌的内容

  1. 清理过期信息

    scss 复制代码
    Object.keys(prevProps).forEach((key) => {
        if (!(key in nextProps)) {
            dom.removeAttribute(key)
        }
    })
    • 检查昨天的价格牌内容(prevProps
    • "特价促销"标签今天没有了 → 撕掉这个标签
    • "有机认证"标签今天没有了 → 撕掉这个标签
  2. 更新/添加新信息

    tsx 复制代码
    Object.keys(nextProps).forEach((key) => {
        if (nextProps[key] !== prevProps[key]) {
            // 处理逻辑...
        }
    })
    • 价格从10元→8元:更新价格数字
    • 产地从"山东"→"云南":更新产地信息
    • 新增"新鲜到货"标签:添加这个标签
  3. 处理特别活动

    tsx 复制代码
    if (key.startsWith("on")) {
        const eventType = key.slice(2).toLowerCase()
        dom.addEventListener(eventType, nextProps[key])
    }
    • 发现新事件:"onClick"→"点击免费品尝"
    • 设置活动:顾客点击牌子时,提供西红柿品尝
    • 注意:实际React会先移除旧事件监听器

🍅 完整案例:西红柿摊位的更新

更新前

  • 价格:10元/斤
  • 产地:山东
  • 标签:特价促销
  • 活动:点击查看详情

更新后

  • 价格:8元/斤(更新)
  • 产地:云南(更新)
  • 标签:新鲜到货(新增)
  • 活动:点击免费品尝(更新)
  • 移除:特价促销

执行步骤

  1. commitWork找到西红柿位置牌(标记为'update')

  2. 调用updateProps进行内容更新:

    tsx 复制代码
    updateProps(西红柿牌, 
      { price: "8元", origin: "云南", fresh: "到货", onClick: 品尝活动 }, // nextProps
      { price: "10元", origin: "山东", promotion: "特价", onClick: 旧活动 } // prevProps
    )
  3. updateProps中:

    • 移除旧属性:promotion: "特价" → 撕掉"特价促销"标签

    • 更新属性:

      • price: "10元" → "8元":修改价格
      • origin: "山东" → "云南":修改产地
      • 新增fresh: "到货":添加"新鲜到货"标签
    • 更新事件:移除旧活动,添加"点击免费品尝"新活动

📊 为什么需要这样的流程?

步骤 技术原因 菜市场好处
向上找DOM父节点 函数组件无DOM节点 找到真正能挂牌子的摊位
递归处理子节点 深度优先遍历高效 先整理好一个区域再处理旁边
分两步更新属性 避免属性残留 确保摊位信息完全准确
区分普通属性和事件 事件需要特殊处理 普通标签和促销活动分开处理

⚠️ 实际React的优化点

  1. 删除节点处理:实际React还有'deletion'标签处理被移除的节点
  2. 事件处理优化:会先移除旧事件监听器再添加新事件
  3. 批量更新:所有DOM操作集中处理,减少浏览器重排
  4. 错误边界:出错时回退到旧状态

💡 总结:菜市场的智慧

  1. 提交阶段(Commit Phase) 就像菜市场收摊后重新布置:

    • 按计划(Fiber树)执行实际操作
    • 避免在营业时修改(无中间状态)
  2. commitWork 是老板巡视摊位:

    • 找到实际摊位(DOM父节点)
    • 按标签执行操作(更新/新增)
    • 系统性地处理所有区域(递归)
  3. updateProps 是更新价格牌:

    • 先清理过时信息
    • 再更新变化的内容
    • 特别处理促销活动(事件)

这样高效的操作让顾客(用户)第二天看到的是完美更新的摊位(UI),没有中间混乱状态!

添加测试手动更新,目前没有实现setState,所以用手动更新进行测试

tsx 复制代码
import React from './core/React.js';
​
let count = 10
function Counter({ num }) {
  function handleClick() {
    console.log("click")
    count++
    React.update()
  }
  return <div>
    count: {count} 
    <button onClick={handleClick}>点击</button>
  </div>
}
​
function App() {
  return <div>
    hi-mini-react
    <Counter num={10}></Counter> 
    {/* <Counter num={20}></Counter> */}
  </div>
}
export default App;

发现问题,每次的log次数有点多

  • 因为每次更新都添加了事件,每次创建的函数都是新的函数,之前的Dom没有卸载,每次都挂载新的函数到dom上,移除掉之后就正常了
tsx 复制代码
function updateProps(dom, nextProps, prevProps) {
    // 1. old 有 new 没有 删除
    Object.keys(prevProps).forEach((key) => {
        if (key !== "children") {
            if (!(key in nextProps)) {
                dom.removeAttribute(key)
            }
        }
    })
    // 2. new 有 old 没有 添加
    // 3. new 有 old 有 修改,2和3其实是一种情况
    Object.keys(nextProps).forEach((key) => {
        if (key !== "children") {
            if (nextProps[key] !== prevProps[key]) {
                if (key.startsWith("on")) {
                    const eventType = key.slice(2).toLowerCase()
                    // 删掉之前的
                    dom.removeEventListner(eventType, prevProps[key])
                    dom.addEventListener(eventType, nextProps[key])
                } else {
                    dom[key] = nextProps[key];
                }
            }
        }
    });
}

验证删除,发现没有问题

tsx 复制代码
import React from './core/React.js';
​
let count = 10
let props = {id: '1111'}
function Counter({ num }) {
  function handleClick() {
    console.log("click")
    props = {}
    count++
    React.update()
  }
  return <div {...props}>
    count: {count}
    <button onClick={handleClick}>点击</button>
  </div>
}
​
function App() {
  return <div>
    hi-mini-react
    <Counter num={10}></Counter> 
    {/* <Counter num={20}></Counter> */}
  </div>
}
export default App;
相关推荐
Jinxiansen02113 分钟前
Vue + Element Plus 实战:大文件切片上传 + 断点续传
前端·javascript·vue.js
JarvanMo5 分钟前
在Flutter中将图像转换为灰度:ColorFiltered组件
前端
独立开阀者_FwtCoder9 分钟前
11 个 JavaScript 杀手脚本,用于自动执行日常任务
前端·javascript·github
ak啊14 分钟前
深入浅出:计算机网络中的数据封装与解封装之旅
前端·后端
汪子熙24 分钟前
深入理解 // eslint-disable-next-line no-eval 注释的作用与应用
前端·javascript·面试
Dignity_呱32 分钟前
Vue性能优化:从加载提速到运行时优化
前端·vue.js·面试
William Dawson44 分钟前
【从前端到后端导入excel文件实现批量导入-笔记模仿芋道源码的《系统管理-用户管理-导入-批量导入》】
java·前端·笔记·elementui·typescript·excel
Navicat中国1 小时前
Edge Databases:赋能分布式计算环境
前端·数据库·edge·sqlite
天天摸鱼的java工程师1 小时前
凌晨四点,掘金签到 bug 现场抓包,开发同学速来认领!
服务器·前端·后端