这节课我们来实现一下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
}
- 我们来用菜市场买菜的例子来解释一下上述代码,让它更容易理解:
🥦 菜市场故事背景:
- 你:菜摊老板(相当于 React 的协调器)
- 你的菜摊 :当前展示给顾客的摊位(对应
fiber
) - 旧进货单 :昨天的蔬菜摆放记录(对应
oldFiber = fiber.alternate?.child
) - 新进货单 :今天要摆放的蔬菜清单(对应
children
数组) - 蔬菜位置牌 :每个蔬菜的位置标识(对应
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
)- 第三个蔬菜(土豆)的牌子挂在黄瓜牌子旁边(以此类推)
🌟 完整故事流程:
假设今天进货单是:[西红柿, 黄瓜, 土豆]
-
处理西红柿:
- 查看昨天第一个位置:也是西红柿(
isSameType=true
) - 拿出昨天的西红柿位置牌,贴上"更新"标签
- 把牌子挂在摊位入口处
- 把昨天的进货单翻到第二页(准备看昨天第二个位置是什么)
- 查看昨天第一个位置:也是西红柿(
-
处理黄瓜:
- 查看昨天第二个位置:假设是胡萝卜(类型不同!)
- 准备新位置牌,贴上"新位置"标签
- 把黄瓜牌子挂在西红柿牌子旁边
- 进货单翻到第三页
-
处理土豆:
- 昨天第三个位置:假设是空的
- 准备新位置牌,贴上"新位置"标签
- 把土豆牌子挂在黄瓜牌子旁边
📌 关键点总结:
代码概念 | 菜市场比喻 | 作用 |
---|---|---|
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)
}
菜市场故事版 - 摊位布置流程:
-
准备收摊:今天营业结束,你要更新摊位
- 顾客已经离开(浏览器渲染完成)
- 新布局计划已准备好(Fiber树构建完成)
-
找到真正的摊位:
inilet fiberParent = fiber.parent while (!fiberParent.dom) { fiberParent = fiberParent.parent }
- 你(蔬菜区主管)要挂位置牌,但有些区域没有实际摊位
- 你向上级汇报,直到找到真正有摊位的张老板(有DOM的父节点)
- 例子:西红柿区 → 蔬菜区 → 张老板的实体摊位
-
处理"更新"标签:
iniif (fiber.effectTag === 'update') { updateProps(fiber.dom, fiber.props, fiber.alternate?.props) }
- 拿起贴有"更新"标签的位置牌(如西红柿牌)
- 对比昨天的价格牌(
fiber.alternate?.props
)和今天的(fiber.props
) - 只修改变化的部分(调用
updateProps
)
-
处理"新位置"标签:
goelse if (fiber.effectTag === 'placement') { if (fiber.dom) { fiberParent.dom.append(fiber.dom) } }
- 拿起全新的位置牌(如黄瓜牌)
- 直接挂到摊位的相应位置(DOM添加操作)
-
处理相邻区域:
scsscommitWork(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];
}
}
}
});
}
菜市场故事版 - 价格牌更新细节:
场景:你要更新西红柿位置牌的内容
-
清理过期信息:
scssObject.keys(prevProps).forEach((key) => { if (!(key in nextProps)) { dom.removeAttribute(key) } })
- 检查昨天的价格牌内容(
prevProps
) - "特价促销"标签今天没有了 → 撕掉这个标签
- "有机认证"标签今天没有了 → 撕掉这个标签
- 检查昨天的价格牌内容(
-
更新/添加新信息:
tsxObject.keys(nextProps).forEach((key) => { if (nextProps[key] !== prevProps[key]) { // 处理逻辑... } })
- 价格从10元→8元:更新价格数字
- 产地从"山东"→"云南":更新产地信息
- 新增"新鲜到货"标签:添加这个标签
-
处理特别活动:
tsxif (key.startsWith("on")) { const eventType = key.slice(2).toLowerCase() dom.addEventListener(eventType, nextProps[key]) }
- 发现新事件:"onClick"→"点击免费品尝"
- 设置活动:顾客点击牌子时,提供西红柿品尝
- 注意:实际React会先移除旧事件监听器
🍅 完整案例:西红柿摊位的更新
更新前:
- 价格:10元/斤
- 产地:山东
- 标签:特价促销
- 活动:点击查看详情
更新后:
- 价格:8元/斤(更新)
- 产地:云南(更新)
- 标签:新鲜到货(新增)
- 活动:点击免费品尝(更新)
- 移除:特价促销
执行步骤:
-
commitWork
找到西红柿位置牌(标记为'update') -
调用
updateProps
进行内容更新:tsxupdateProps(西红柿牌, { price: "8元", origin: "云南", fresh: "到货", onClick: 品尝活动 }, // nextProps { price: "10元", origin: "山东", promotion: "特价", onClick: 旧活动 } // prevProps )
-
在
updateProps
中:-
移除旧属性:
promotion: "特价"
→ 撕掉"特价促销"标签 -
更新属性:
price: "10元" → "8元"
:修改价格origin: "山东" → "云南"
:修改产地- 新增
fresh: "到货"
:添加"新鲜到货"标签
-
更新事件:移除旧活动,添加"点击免费品尝"新活动
-
📊 为什么需要这样的流程?
步骤 | 技术原因 | 菜市场好处 |
---|---|---|
向上找DOM父节点 | 函数组件无DOM节点 | 找到真正能挂牌子的摊位 |
递归处理子节点 | 深度优先遍历高效 | 先整理好一个区域再处理旁边 |
分两步更新属性 | 避免属性残留 | 确保摊位信息完全准确 |
区分普通属性和事件 | 事件需要特殊处理 | 普通标签和促销活动分开处理 |
⚠️ 实际React的优化点
- 删除节点处理:实际React还有'deletion'标签处理被移除的节点
- 事件处理优化:会先移除旧事件监听器再添加新事件
- 批量更新:所有DOM操作集中处理,减少浏览器重排
- 错误边界:出错时回退到旧状态
💡 总结:菜市场的智慧
-
提交阶段(Commit Phase) 就像菜市场收摊后重新布置:
- 按计划(Fiber树)执行实际操作
- 避免在营业时修改(无中间状态)
-
commitWork
是老板巡视摊位:- 找到实际摊位(DOM父节点)
- 按标签执行操作(更新/新增)
- 系统性地处理所有区域(递归)
-
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;