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);
}
相关推荐
空中海1 天前
01 React Native 基础、核心组件与布局体系
javascript·react native·react.js
空中海1 天前
05 React架构设计、项目实践与专家清单
前端·react.js·前端框架
空中海2 天前
04 工程化、质量体系与 React 生态
前端·ubuntu·react.js
空中海2 天前
03 性能、动画与 React Native 新架构
react native·react.js·架构
空中海2 天前
02 React Native状态、导航、数据流与设备能力
javascript·react native·react.js
空中海2 天前
04 React Native工程化、质量、发布与生态选型
javascript·react native·react.js
郑生zs2 天前
Hooks-useEffect
react.js
光影少年2 天前
react函数组件、类组件、纯组件、受控/非受控组件
前端·react.js·掘金·金石计划
空中海2 天前
05 React Native架构设计、主线项目与专家实践
javascript·react native·react.js
killerbasd2 天前
还是迷茫 5.3
前端·react.js·前端框架