mini-react 第三天:统一提交+支持function component

统一提交

问题:页面显示不全

浏览器繁忙,requestIdleCallback不够时间render 完整个vdom,这时浏览器会渲染已完成的部分dom,导致页面显示不全

解决

等render完了再统一提交

  • 将原来的fiber.parent.append(dom) 改为 commitWorkcommitRoot
  • 在processUnitOfWork 之后判断是否执行commitRoot,仅当nextWork = null 时执行(已经处理完所有fiber链表节点,没有剩余的时候)
    • let root = null
    • render 函数中初始设置 root = 根节点
    • commitRoot 中append 到root节点并设置root = null

支持函数组件写法

第一步:支持App和其子组件Counter的function 写法

1.1 写App function返回dom
javascript 复制代码
import React from "./core/React.js";

function Counter() {
  return (
    <div>
      <div>count</div>
    </div>
  )
}

// const App = <div id="app">hi-mini-react</div>;

function App() {
  return <div>
      hi-mini-react
      <Counter/>
  </div>;
}
1.2 解决报错

【分析】

  • 使用function 返回dom,这时打印发现在正常处理vdom过程中遇到了function
  • 在performUnitofWork 函数中debug可以看到 typeof fiber.type === "function"。它无法直接用来做 document.createElement 或者 document.createTextNode

【解决】

  • 我们要做的就是执行这个function拿到vdom
  • 函数组件就是一个盒子,包裹着vdom;执行这个函数就是开盒子,拿到vdom

【代码逻辑】

  1. 跳过 createElement 和 updateProps - 函数组件本身不会成为一个实际的 html element
  2. 执行function并将其返回值作为children处理
javascript 复制代码
function performWorkOfUnit(fiber) {
  // 如果是函数组件,不直接为其append dom
  const isFunctionComponent = typeof fiber.type === "function";
  if (!isFunctionComponent) {
    if (!fiber.dom) {
      const dom = (fiber.dom = createDom(fiber.type));

      updateProps(dom, fiber.props);
    }
  }

  // 将函数组件的返回值塞进数组,因为children是一个数组
  const children = isFunctionComponent ? [fiber.type()] : fiber.props.children
  initChildren(fiber, children)
  ...
仍然显示异常

【原因】vdom中函数组件占据一个节点。但是map到dom结构中它是null

【解决】

javascript 复制代码
function commitWork(fiber) {
  if (!fiber) return

  // 原来的逻辑:导致报错 ->
  //           App函数组件的 fiber.parent.dom = null;
  //           App的Counter子组件 fiber.dom = null
  // fiber.parent.dom.append(fiber.dom)

  // 新逻辑:
  // 跳过函数组件造成的vdom -> dom结构断层
  let fiberParent = fiber.parent;
  while (!fiberParent.dom) {
    fiberParent = fiberParent.parent
  }

  // vdom中的函数组件节点本身并没有对应的dom节点,append 会导致null被append到dom树上
  if (fiber.dom) {
    fiberParent.dom.append(fiber.dom)
  }
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}
1.3 成功展示函数组件App和Counter

第二步:支持函数组件的props

2.1 添加属性并传值

添加完发现打印出来的props是undefined

js 复制代码
function Counter(props) {
  console.log("props: ", props); 
  return (
    <div>
      <div>count: {props.num}</div>
    </div>
  )
}

function App(params) {
  return (
    <div>
      mini-react
      <Counter num={10}></Counter>
    </div>
  )
}

这就引导我们去调用 Counter 函数的地方去了

js 复制代码
const children = isFunctionComponent ? [fiber.type()] : fiber.props.children

这里没有传入参数。添加debugger查看fiber object的结构,发现props就在 fiber.props 上。回想最初建立vdom结构时,<div id="app"></div> 中的id属性其实也是直接在fiber.props上的。

[fiber.type()]改为[fiber.type(fiber.props)]就可以看到Counter函数中正确打印值了

2.2 处理属性值

此时页面仍然报错

通过查看当前节点的 parent.props.children 发现当前节点就是传入的数字10

而实际上数字10 应当像它前面的count: 一样在dom中建成一个textNode

css 复制代码
<div>count: {props.num}</div>

这就需要修改 createElement 函数了: 当child是数字的时候,也调用 createTextNode

js 复制代码
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) => {
        // const isTextNode = typeof child === "string"
        const isTextNode = typeof child === "string" || typeof child === "number"
        return isTextNode ? createTextNode(child) : child
      }),
    },
  };
}

【成果】 现在属性值被正确render了。

第三步:支持多个函数组件

加多一个Counter组件发现第二个没被render。

【原因】 现有代码只会找叔叔的sibling,当叔叔没有sibling的时候还应该找爷爷的sibling (以及更高层级的,如果有的话)

js 复制代码
 // 4. 返回下一个要执行的任务
  if (fiber.child) {
    return fiber.child;
  }

  if (fiber.sibling) {
    return fiber.sibling;
  }

  return fiber.parent?.sibling;

【解决】

js 复制代码
 // 4. 返回下一个要执行的任务
  if (fiber.child) {
    return fiber.child;
  }
  
  // 循环找parent和它的sibling
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      // 如果有sibling就返回
      return nextFiber.sibling;
    } else {
      // 否则就往上找
      nextFiber = nextFiber.parent;
    }
  }

【成果】

能正常显示了

阶段总结

总体而言,由于函数组件dom=null导致了我们在commitWork中append dom的时候需要向上查找parent;以及在遍历fiber链表结构的时候需要处理没有叔叔的情况。

重构代码

  • 将普通组件和函数组件分成两个函数来处理,减少各处的 isFunctionComponent 分支。
js 复制代码
function updateFunctionComponent(fiber) {
  // 如果是函数组件,不直接为其append dom
  const children = [fiber.type(fiber.props)]
  initChildren(fiber, children)
}

function updateHostComponent(fiber) {
  // 将vdom节点转换为dom节点
  // 如果vdom节点对应的dom节点未被创建,先创建dom节点,并append到父节点的dom上
  if (!fiber.dom) {
    const dom = (fiber.dom = createDom(fiber.type));
    updateProps(dom, fiber.props);
  }
  const children = fiber.props.children;
  initChildren(fiber, children);
}
相关推荐
bbb1691 小时前
react源码分析 setStatae究竟是同步任务还是异步任务
前端·javascript·react.js
前端双越老师3 小时前
【万字总结】2025 前端+大前端+全栈 知识体系(下)
vue.js·react.js·node.js
Mr.NickJJ10 小时前
JavaScript系列06-深入理解 JavaScript 事件系统:从原生事件到 React 合成事件
开发语言·javascript·react.js
Mr.NickJJ12 小时前
React Native v0.78 更新
javascript·react native·react.js
你会发光耶16 小时前
彻底理解Redux的使用
前端·react.js·编程语言
十八般不精通16 小时前
react-项目目录认识
前端·react.js
十八般不精通16 小时前
react-项目搭建
前端·react.js
程序员王天19 小时前
阿里云oss开发实践:大文件分片、断点续传、实时进度 React+Node+Socket.IO
前端·react.js·阿里云·node.js
shmily_yyA21 小时前
【2025】Electron + React 架构筑基——从零到一的跨平台开发
react.js·架构·electron
hamburgerDaddy11 天前
从零开始用react + tailwindcss + express + mongodb实现一个聊天程序(十二) socketio 消息处理
前端·javascript·websocket·mongodb·react.js·node.js·express