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;
相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax