likes
comments
collection
share

手把手教你实现 React 渲染框架 看完不会来打我!

作者站长头像
站长
· 阅读数 63

引言

下面这段代码是React渲染jsx的语法,运行会发生报错,也就是说React的代码不加一些处理是不能运行在js的

<div id="root"></div>
<script>
    const element = <div>content</div> // jsx
    const container = document.getElementById('root')
    ReactDom.render(element,container)
</script>

接下来实现一下React转原生js实现渲染的过程

1.jsx语法会被Bable转化为以下语法

const element = <div>content</div> // jsx
const element = React.createElement('div', {
   title: 'hello word'
}, 'content')

Babel 使用 @babel/preset-react 预设来处理 JSX 代码。这个预设包含了一个插件 @babel/plugin-transform-react-jsx,用于将 JSX 转换为函数调用

例如:<div>content</div>转换为React.createElement('div', {title: 'hello word'}, 'content')

转化后的React.createElement接受三个参数,分别是Dom类型,Dom属性,内容(子元素)

2.React.createElement会把 element转化为虚拟DOM树

const element = {
  type: 'div',
  props: {
    title: 'hello word',
    children: 'content'
  }
}

经过React.createElement函数转换成js对象,type是 DOM 类型,props是参数,title是属性,children为内容(子元素),为下一步渲染做准备

3.ReactDom.render渲染到页面

const node = document.createElement(element.type)
node['title'] = element.props.title
const text = document.createTextNode(element.props.children)
node.appendChild(text)
container.appendChild(node)

ReactDom.render函数解析 DOM 节点树,创建元素,添加属性,添加内容(子元素),最后挂载到父元素

完整代码

<div id="root"></div>
<script>
const container = document.getElementById('root')
const element = {
  type: 'div',
  props: {
    title: 'hello word',
    children: 'content'
  }
}
const node = document.createElement(element.type)
node['title'] = element.props.title
const text = document.createTextNode(element.props.children)
node.appendChild(text)
container.appendChild(node)
</script>

效果 手把手教你实现 React 渲染框架 看完不会来打我!

以上是简单的示例介绍了渲染的流程,下面带大家详细分析渲染流程

React.createElement

在 React 中,React.createElement 函数用于创建虚拟 DOM 元素。它接受三个参数:元素类型、属性对象以及子元素。

const divObject = React.createElement('div', {
    title: 'hello word',
}, React.createElement('p', null, 'dell'), React.createElement('b', null, 'lee'))
console.log(divObject)

上面的示例首先通过React.createElement创建一个js对象

起始节点为div,有一个title属性,内容是两个子节点

  • 节点p,属性为空,子节点为dell文本节点
  • 节点b,属性为空,子节点为lee文本节点

手把手教你实现 React 渲染框架 看完不会来打我! createElement 原理

以下是 React 源码中 React.createElement 函数的简化版本:

    const React = {
      createElement: function(type, props, ...children) {
        return {
          type,
          props: {
            ...props,
            children: children.map(child => {
              if(typeof child === 'object') {
                return child;
              }else {
                return {
                  type: 'TEXT_NODE',
                  props: {
                    nodeValue: child,
                    children: []
                  },
                }
              }
            })
          }
        }
      }
    };

在上面的源码中,createElement 函数接收一个 type 参数(元素类型)、一个 props 参数(元素的属性对象)以及可选的 children 参数(子元素)。

函数返回值一个对象分别包含以下几个属性

  • type为元素类型,值是一个字符串
  • props则是一个对象,用来储存属性和子元素

props里面首先使用ES6 中的扩展运算符将 props 参数中的属性解析出来,children属性使用map遍历children参数,对不同类型的子元素分别进行了处理,

  • 对象类型,直接返回子元素,
  • 文本类型,封装成对象type类型定义为TEXT_NODEprops.nodeValue为文本的值,props.children子元素为空数组

总结起来,createElement 函数通过创建一个对象来描述虚拟 DOM 元素,其中包含了元素的类型、属性和子元素等信息。对于子元素,会根据其类型进行判断,如果是对象类型,则直接添加到 props.children 中;如果是文本类型,则通过 createTextElement 函数创建对应的虚拟 DOM 对象。这样就生成了一个虚拟 DOM 元素,可以用于进行后续的渲染和更新操作。

ReactDom.render

在 React 中,ReactDom.render 函数用于渲染虚拟 DOM 元素。它接受两个参数:DOM 节点树,父元素。

const element = <div title="Hello Welcome"><p>Dell</p><b>Lee</b></div>
const root = document.getElementById("root");
ReactDOM.render(element, root);

上面的示例使用ReactDom.render把 DOM 节点树挂载到root上 手把手教你实现 React 渲染框架 看完不会来打我! render 函数实现

以下是 ReactDom.render 函数抽象的实现方式:

const ReactDOM = {
  render: function(element, container) {
    // 创建合理的元素节点
    const dom = element.type === "TEXT_NODE" ? document.createTextNode("") : document.createElement(element.type);
    // 给元素节点挂载属性
    Object.keys(element.props).filter(key=> key!== 'children').forEach((name) => { dom[name] = element.props[name] })
    // 使用 children 属性作一个递归
    element.props.children.forEach(child => ReactDOM.render(child, dom));
    container.append(dom);
  }
};

ReactDOM.render接收两个参数elementcontainer分别是 DOM 节点树,父元素

渲染 DOM 节点树的三个核心步骤:

  • 创建合理的元素节点,元素和文本
  • 给元素节点挂载属性,props中除children以外的属性
  • 使用children属性作一个递归,继续渲染子节点

React16之前的版本采用的是这个写法,一次性把所有的 DOM 节点渲染完成,在渲染的过程中浏览器不能做别的操作,更新组件的时候会导致页面卡顿,React18 采用的是拆分成小的片段,等待浏览器空闲时渲染

Concurrent Mode

在 Concurrent Mode 中,React 可以中断渲染过程,处理优先级更高的任务,然后再返回并继续渲染。这种能力使 React 能够更好地处理复杂的用户交互、动画和其他需要实时性能的场景。

以下是Concurrent Mode的实现思路

// 下一个要执行的单元
let nextUnitWork = {};

// 该函数用来处理下一个执行的单元,同时返回下下一个执行单元
function performUnitOfWork(nextUnitOfWork) {
  console.log(nextUnitOfWork);
  return null;
}

// 自动调度
function workLoop(deadline) {
  while(nextUnitWork){
    nextUnitWork = performUnitOfWork(nextUnitWork);
  }
  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop)

requestIdleCallback 是什么?

requestIdleCallback是一个用于在浏览器空闲时执行任务的API。它允许开发者在浏览器空闲时执行一些较为耗时的任务,而不会影响到页面的性能和用户体验。 使用requestIdleCallback可以将一些耗时的操作分解成多个小任务,然后在浏览器空闲时依次执行这些任务,从而避免阻塞主线程。

首先使用requestIdleCallback解决浏览器什么时候空闲的问题,由于兼容性问题React并没有使用requestIdleCallback,而是自己封装了一套方法,requestIdleCallback接收一个参数workLoop为回调函数

workLoop函数起到一个自动调度的作用,参数deadline.timeRemaining()获取到本次浏览器的空闲时间,通过while循环执行下一个需要执行的单元,等待空闲时间结束,再次调用requestIdleCallback等待浏览器空闲时继续执行workLoop函数

performUnitOfWork该函数用来处理下一个执行的单元,同时返回下下一个执行单元,如果没有返回null渲染结束

总结,React中使用Concurrent Mode是想把一个比较大的 DOM 节点对象,打散成多个小的 DOM 节点对象,通过调度一段一段的去渲染

Fiber

Fiber架构的核心思想是将组件的渲染和更新过程拆分成多个小任务单元,然后通过调度器来决定任务的执行顺序。这样可以在每个任务单元之间进行中断、暂停和恢复,从而实现更灵活的调度和优化。

const element = (
  <div>
    <h1>
      <p>Paragraph</p>
      <a href='https://www.imooc.com'>Link</a>
    </h1>
    <h2>Subtitle</h2>
  </div>
)
const root = document.getElementById("root");
ReactDOM.render(element, root);

分析这段代码,首先root节点是根节点,

divroot下面的子节点,但没有兄弟节点

h1h2是兄弟节点,div分别是h1h2的父节点

pa是兄弟节点,h1分别是pa的父节点

Fiber是一个个小的单元对象对象,加上父子,兄弟节点的链表关系,Fiber节点组合在一起就是Fiber Tree

手把手教你实现 React 渲染框架 看完不会来打我! Fiber的基础实现

Fiber的基础实现主要分3个步骤:

  • 把 Fiber 对应的内容渲染到页面上
  • 计算下一层 Fiber Tree
  • 选择下一个要执行的 Fiber 单元
// 下一个要执行的单元
let nextUnitWork = null;

// 该函数用来处理下一个执行的单元,同时返回下下一个执行单元
function performUnitOfWork(fiber) {
  // 1. 把 fiber 对应的内容渲染到页面上
  if(!fiber.dom) {
    fiber.dom = ReactDOM.createDom(fiber);
  }
  if(fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom);
  }
  // 2. 计算下一层 fiber tree
  const elements = fiber.props.children;
  let index = 0;
  let prevSibling = null;
  while(index < elements.length) {
    const element = elements[index];
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null
    }
    if(index === 0) {
      fiber.child = newFiber;
    }else {
      prevSibling.sibling = newFiber;
    }
    prevSibling = newFiber;
    index++;
  }
  // 3. 选择下一个要执行的 fiber 单元
  if(fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while(nextFiber) {
    if(nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}

// 自动调度
function workLoop(deadline) {
  while(nextUnitWork){
    nextUnitWork = performUnitOfWork(nextUnitWork);
  }
  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

const ReactDOM = {
  createDom: function(fiber) {
    // 创建合理的元素节点
    const dom = fiber.type === "TEXT_NODE" ? document.createTextNode("") : document.createElement(fiber.type);
    // 给元素节点挂载属性
    Object.keys(fiber.props).filter(key=> key!== 'children').forEach((name) => { dom[name] = fiber.props[name] })
    return dom;
  },

  render: function(element, container) {
    nextUnitWork = {
      dom: container,
      props: {
        children: [element]
      }
    }
  }
};

重新改写ReactDOM对象,渲染 DOM 的功能分给createDom方法,

render只提供开始渲染的条件,把nextUnitWork赋值为一个初始化的Fiber

1.把 Fiber 对应的内容渲染到页面上,创建元素,挂载到父元素上

2.计算下一层 Fiber Tree,每一个子节点都生成Fiber,然后构建父子,兄弟之间的链路关系

3.选择下一个要执行的 Fiber 单元,自上而下,优先执行第一个子元素,例如root->div->h1->p 执行到最后一个子元素为止,然后执行最后一个元素兄弟元素,执行完返回去执行父元素的兄弟元素

这样就实现的一个复杂的 DOM 树渲染,打散成多个小的DOM单元,等到游览器空闲就开始渲染一部分工作

总结:正是因为workLoop这种循环队列加上Fiber Tree这种的数据结构,使得React17之后的版本,遇到比较复杂的 DOM 树渲染的时候,避免出现卡顿的现象,提升用户体验

手把手教你实现 React 渲染框架 看完不会来打我!

目前还有一些问题,比如浏览器渲染了一些元素,突然有一个大的耗时任务需要执行,剩余的 DOM 元素则需要等待耗时任务执行完才能渲染,这种切块渲染方式,可能会导致 DOM 元素一块一块的渲染,而React则是希望等待执行完一次性的渲染到页面上

Render & Commit 阶段

针对上面的问题,React中引入了两个概念:

  • Render阶段:用来创建 DOM 节点,生成Fiber Tree
  • Commit阶段:把Fiber Tree中的元素渲染到页面
// 下一个要执行的单元
let nextUnitWork = null;

// workInProgress 当前正在计算的 fiber 节点
let wipRoot = null;

function commitWork(fiber) {
  if(!fiber) {
    return;
  }
  const domParent = fiber.parent.dom;
  domParent.appendChild(fiber.dom);
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

function commitRoot() {
  commitWork(wipRoot.child)
  wipRoot = null;
}

// 该函数用来处理下一个执行的单元,同时返回下下一个执行单元
// 用来生成 Fiber Tree 的一个函数,生成 fiber tree 的过程,在 React 中叫做 render
function performUnitOfWork(fiber) {
  // 1. 把 fiber 对应的内容渲染到页面上
  if(!fiber.dom) {
    fiber.dom = ReactDOM.createDom(fiber);
  }
  // if(fiber.parent) {
  //   fiber.parent.dom.appendChild(fiber.dom);
  // }
  // 2. 计算下一层 fiber tree
  const elements = fiber.props.children;
  let index = 0;
  let prevSibling = null;
  while(index < elements.length) {
    const element = elements[index];
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null
    }
    if(index === 0) {
      fiber.child = newFiber;
    }else {
      prevSibling.sibling = newFiber;
    }
    prevSibling = newFiber;
    index++;
  }
  // 3. 选择下一个要执行的 fiber 单元
  if(fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while(nextFiber) {
    if(nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}

// 自动调度
function workLoop(deadline) {
  while(nextUnitWork){
    nextUnitWork = performUnitOfWork(nextUnitWork);
  }
  if(!nextUnitWork && wipRoot) {
    // fiber tree 已经准备好了,需要一次性的挂载 DOM
    // 一次性把 fiber tree 的内容渲染到页面上,这个过程叫做 react 中的 commit 阶段
    commitRoot()
  }
  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

const ReactDOM = {
  createDom: function(fiber) {
    // 创建合理的元素节点
    const dom = fiber.type === "TEXT_NODE" ? document.createTextNode("") : document.createElement(fiber.type);
    // 给元素节点挂载属性
    Object.keys(fiber.props).filter(key=> key!== 'children').forEach((name) => { dom[name] = fiber.props[name] })
    return dom;
  },

  render: function(element, container) {
    wipRoot = {
      dom: container,
      props: {
        children: [element]
      }
    }
    nextUnitWork = wipRoot;
  }
};

performUnitOfWork方法只负责创建 DOM 元素(生成Fiber Tree)不具备渲染的能力,在 React 中叫做 render 阶段

wipRoot用来表示当前正在计算的 Fiber 节点,并在render中赋初始值

workLoop函数中等待所有的执行单元执行完成,并且存在正在计算的 Fiber 节点,调用commitRoot函数一次性把 Fiber Tree 的内容渲染到页面上,这个过程叫做 React 中的 Commit 阶段

commitRoot函数,调用commitWork函数从wipRoot中的子节点开始渲染(初始值root已经被渲染,所以每次从子节点开始),最后全部渲染后把wipRoot的状态置为null

commitWork函数,首先先去渲染当前节点,挂载到父节点上,接着递归渲染子节点,等待所有子节点渲染完成再去渲染兄弟节点

总结:rander阶段就是创建 DOM 生成 Fiber Tree 的过程,Commit阶段是把Fiber Tree 做一个遍历一次性的渲染到页面

Reconciliation 阶段

在React中,Reconciliation(协调)阶段是指React通过比较新旧虚拟DOM树的差异,找出需要更新的部分,并进行相应的更新操作的过程。这个过程是React中非常重要的一部分,用于确保页面的UI与数据的一致性。

举个例子说明一下Reconciliation 阶段存在的重要性

let element = (
  <div>
    <h1>
      <p>Paragraph</p>
      <a href='https://www.imooc.com'>Link</a>
    </h1>
    <h2>Subtitle</h2>
  </div>
)
const root = document.getElementById("root");
ReactDOM.render(element, root);

element = (
  <div>
    <h1>
      Paragraph update
    </h1>
    <h2>Subtitle</h2>
  </div>
)

ReactDOM.render(element, root);

这段代码中ReactDOM.render执行了两次,根据目前的实现方法,每次都是重新创建 DOM 元素,对于重复的 DOM 重新创建会浪费性能,Reconciliation 阶段就是解决DOM的复用的问题

Reconciliation 阶段的实现在Render 和 Commit 两个阶段

// 下一个要执行的单元
let nextUnitWork = null;

// workInProgress 当前正在计算的 fiber 节点
let wipRoot = null;

// 存储上一次渲染对应的 FiberTree Root
let currentRoot = null;

// 存储本次渲染需要删除的 fiber 节点
let deletions = [];

const isEvent = key => key.startWith("on");
const isProperty = key => key !== "children" && !isEvent(key);
const isNew = (prev, next) => key=> preProps[key] !== nextProps[key];
const isGone = (next) => key => !(key in nextProps);

// 当 DOM 可以复用时,复用 DOM 节点的逻辑
function updateDom(dom, preProps, nextProps) {
  // 清除老的或者被改变的 dom 节点事件处理函数
   Object.keys(preProps)
    .filter(isEvent)
    .filter(key => !(key in nextProps) || isNew(preProps, nextProps)(key))
    .forEach(name => {
      const eventType = name.toLowerCase().substring(2);
      dom.removeEventListener(eventType, preProps[name])
    })
  // 清除老的 DOM 属性
  Object.keys(preProps)
    .filter(isProperty)
    .filter(isGone(nextProps))
    .forEach(name => dom[name] = "")
  // 增加新的或者修改老的 DOM 属性
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(preProps, nextProps))
    .forEach(name => dom[name] = nextProps[name])
  // 新增事件监听
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(preProps, nextProps))
    .forEach((name) => {
      const eventType = name.toLowerCase().substring(2);
      dom.addEventListener(eventType, nextProps[name])
    })
}

function commitWork(fiber) {
  if(!fiber) {
    return;
  }
  const domParent = fiber.parent.dom;
  if(fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
    domParent.appendChild(fiber.dom);
  } else if(fiber.effectTag === "DELETION") {
    domParent.removeChild(fiber.dom);
  } else if(
    fiber.effectTag === "UPDATE" && fiber.dom != null
  ){
    updateDom(fiber.dom, fiber.alternate.props, fiber.props)
  }

  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

// commit 函数,用于一次性更新 DOM
function commitRoot() {
  deletions.forEach(commitWork)
  commitWork(wipRoot.child);
  currentRoot = wipRoot;
  wipRoot = null;
}

// 调协函数
function reconcileChildren(wipFiber, elements) {
  let index = 0;
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
  let prevSibling = null;
  while(index < elements.length || oldFiber != null) {
    // 1. fiber ,fiber 合并成一个大树,删掉老fiber 上需要删除的东西
    const element = elements[index];
    let newFiber = null;

    const sameType = oldFiber && element && element.type == oldFiber.type;

    if(sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: 'UPDATE'
      }
    }
    if(element && !sameType) {
      newFiber = {
        type: element.type,
        props: element.props,
        parent: wipFiber,
        dom: null,
        alternate: null,
        effectTag: 'PLACEMENT'
      }
    }
    if(oldFiber && !sameType) {
      oldFiber.effectTag = 'DELETION';
      deletions.push(oldFiber);
    }

    if(oldFiber) {
      oldFibler = oldFibler.sibling;
    }

    if(index === 0) {
      wipFiber.child = newFiber;
    }else {
      prevSibling.sibling = newFiber;
    }
    prevSibling = newFiber;
    index++;
  }
}

// 该函数用来处理下一个执行的单元,同时返回下下一个执行单元
// 用来生成 Fiber Tree 的一个函数,生成 fiber tree 的过程,在 React 中叫做 render
function performUnitOfWork(fiber) {
  // 1. 把 fiber 对应的内容渲染到页面上
  if(!fiber.dom) {
    fiber.dom = ReactDOM.createDom(fiber);
  }
  // 2. 计算下一层 fiber tree
  const elements = fiber.props.children;
  reconcileChildren(fiber, elements);
  // 3. 选择下一个要执行的 fiber 单元
  if(fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while(nextFiber) {
    if(nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}

// 自动调度
function workLoop(deadline) {
  while(nextUnitWork){
    nextUnitWork = performUnitOfWork(nextUnitWork);
  }
  if(!nextUnitWork && wipRoot) {
    // fiber tree 已经准备好了,需要一次性的挂载 DOM
    // 一次性把 fiber tree 的内容渲染到页面上,这个过程叫做 react 中的 commit 阶段
    commitRoot()
  }
  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

const ReactDOM = {
  createDom: function(fiber) {
    // 创建合理的元素节点
    const dom = fiber.type === "TEXT_NODE" ? document.createTextNode("") : document.createElement(fiber.type);
    // 给元素节点挂载属性
    Object.keys(fiber.props).filter(key=> key!== 'children').forEach((name) => { dom[name] = fiber.props[name] })
    return dom;
  },

  render: function(element, container) {
    wipRoot = {
      dom: container,
      props: {
        children: [element]
      },
      alternate: currentRoot,
    }
    deletions = [];
    nextUnitWork = wipRoot;
  }
};

新建两个变量currentRootdeletions

  • currentRoot 存储上一次渲染对应的 FiberTree Root
  • deletions 存储本次渲染需要删除的 Fiber 节点

currentRoot在渲染完成后赋值,用于下一次渲染的时候做对比

deletions在创建DOM的时候添加值(上一次渲染Fiber Tree中用不到的 DOM),方便Commit 阶段删除

Render 阶段

Render 阶段的变化主要发生在计算下一层 Fiber Tree上,把这一块的逻辑封装成了一个函数reconcileChildren,接收两个参数wipFiber, elements分别是当前元素 Fiber 和子元素,while循环里面执行的逻辑是把Fiber 合并成一个大树,删掉老Fiber 上需要删除的东西,合成过程

  • 添加(PLACEMENT) 新元素类型在上一次渲染的Fiber中不存在
  • 更新(UPDATE) sameType变量为true,意味着新的 DOM 元素类型在上一次渲染的Fiber中存在
  • 删除(DELETION) 在新Fiber不存在旧的Fiber存在的元素类型

Commit 阶段

commitWork函数中根据fiber.effectTag区分 DOM 属于那种类型,新增和删除这两个直接就在父元素上进行相应的操作,更新需要在updateDom函数中额外处理一下,也就是复用 DOM 节点的逻辑

updateDom函数接受三个参数,DOM 当前渲染的 DOM,preProps新Fiber的参数,nextProps旧Fiber的参数,从参数也可以看出来主要是对属性做处理,通过prePropsnextProps中参数的差异做删除,添加和更改,操作的对象是 DOM 对应的属性和事件。

总的来说,在比较新旧虚拟DOM树时,React会尽可能地复用已有的DOM节点,而不是直接销毁和重新创建。这样可以减少对DOM的操作,提高性能。React还会根据组件的key属性来判断是否需要重新渲染组件,以确保页面的稳定性和一致性。

讲到这里有关渲染的原理基本都讲完了,另外再补充一个功能点函数组件的实现

函数组件的实现

渲染函数组件跟渲染jsx有两个区别:

  • Fiber没有 DOM
  • 没办法直接取 children

通过示例观察一下结构

function App(props) {
  return <h1>hello, {props.name}</h1>
}

const element = <App name="Dell" />
const root = document.getElementById("root");
ReactDOM.render(element, root);

手把手教你实现 React 渲染框架 看完不会来打我! <App name="Dell" />转化为React.createElement(App, { name: 'Dell' })App是一个函数,children作为函数的返回值,之前的处理方式是App.dom,获取子元素是children,很明显需要额外处理一下数据

部分代码

function updateHostComponent(fiber) {
   // 1. 把 fiber 对应的内容渲染到页面上
   if(!fiber.dom) {
    fiber.dom = ReactDOM.createDom(fiber);
  }
  // 2. 计算下一层 fiber tree
  const elements = fiber.props.children;
  reconcileChildren(fiber, elements);
}

function updateFunctionComponent(fiber) {
  const children = [fiber.type(fiber.props)]; 
  reconcileChildren(fiber, children);
}

// 该函数用来处理下一个执行的单元,同时返回下下一个执行单元
// 用来生成 Fiber Tree 的一个函数,生成 fiber tree 的过程,在 React 中叫做 render
function performUnitOfWork(fiber) {
  const isFunctionComonent = fiber.type instanceof Function;

  if(isFunctionComonent) {
    updateFunctionComponent(fiber);
  }else {
    updateHostComponent(fiber)
  }
  // 3. 选择下一个要执行的 fiber 单元
  if(fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while(nextFiber) {
    if(nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}

在performUnitOfWork创建DOM 元素时分别对 DOM 和函数作处理,updateFunctionComponent方法处理函数的情况,updateHostComponent方法处理 DOM 的情况

function commitDeletion(fiber, domParent) {
  if(fiber.dom) {
    domParent.removeChild(fiber.dom);
  }else {
    commitDeletion(fiber.child, domParent);
  }
}

function commitWork(fiber) {
  if(!fiber) {
    return;
  }
  let domParentFiber = fiber.parent;
  while(!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent;
  }
  const domParent = domParentFiber.dom;
  if(fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
    domParent.appendChild(fiber.dom);
  } else if(fiber.effectTag === "DELETION") {
    commitDeletion(fiber, domParent);
  } else if(
    fiber.effectTag === "UPDATE" && fiber.dom != null
  ){
    updateDom(fiber.dom, fiber.alternate.props, fiber.props)
  }

  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

在删除 DOM 的时候通过commitDeletion判断是否有dom属性,如果有就直接删除,没有就使用递归找到子元素中的 DOM 删除

执行新增的时候遇到函数式组件需要一层层的往上找,找到最近的一个能挂载元素的 DOM

完整代码

// 下一个要执行的单元
let nextUnitWork = null;

// workInProgress 当前正在计算的 fiber 节点
let wipRoot = null;

// 存储上一次渲染对应的 FiberTree Root
let currentRoot = null;

// 存储本次渲染需要删除的 fiber 节点
let deletions = [];

const isEvent = key => key.startWith("on");
const isProperty = key => key !== "children" && !isEvent(key);
const isNew = (prev, next) => key=> preProps[key] !== nextProps[key];
const isGone = (next) => key => !(key in nextProps);

// 当 DOM 可以复用时,复用 DOM 节点的逻辑
function updateDom(dom, preProps, nextProps) {
  // 清除老的或者被改变的 dom 节点事件处理函数
   Object.keys(preProps)
    .filter(isEvent)
    .filter(key => !(key in nextProps) || isNew(preProps, nextProps)(key))
    .forEach(name => {
      const eventType = name.toLowerCase().substring(2);
      dom.removeEventListener(eventType, preProps[name])
    })
  // 清除老的 DOM 属性
  Object.keys(preProps)
    .filter(isProperty)
    .filter(isGone(nextProps))
    .forEach(name => dom[name] = "")
  // 增加新的或者修改老的 DOM 属性
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(preProps, nextProps))
    .forEach(name => dom[name] = nextProps[name])
  // 新增事件监听
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(preProps, nextProps))
    .forEach((name) => {
      const eventType = name.toLowerCase().substring(2);
      dom.addEventListener(eventType, nextProps[name])
    })
}

function commitDeletion(fiber, domParent) {
  if(fiber.dom) {
    domParent.removeChild(fiber.dom);
  }else {
    commitDeletion(fiber.child, domParent);
  }
}

function commitWork(fiber) {
  if(!fiber) {
    return;
  }
  let domParentFiber = fiber.parent;
  while(!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent;
  }
  const domParent = domParentFiber.dom;
  if(fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
    domParent.appendChild(fiber.dom);
  } else if(fiber.effectTag === "DELETION") {
    commitDeletion(fiber, domParent);
  } else if(
    fiber.effectTag === "UPDATE" && fiber.dom != null
  ){
    updateDom(fiber.dom, fiber.alternate.props, fiber.props)
  }

  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

// commit 函数,用于一次性更新 DOM
function commitRoot() {
  deletions.forEach(commitWork)
  commitWork(wipRoot.child);
  currentRoot = wipRoot;
  wipRoot = null;
}

// 调协函数
function reconcileChildren(wipFiber, elements) {
  let index = 0;
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
  let prevSibling = null;
  while(index < elements.length || oldFiber != null) {
    // 1. fiber ,fiber 合并成一个大树,删掉老fiber 上需要删除的东西
    const element = elements[index];
    let newFiber = null;

    const sameType = oldFiber && element && element.type == oldFiber.type;

    if(sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: 'UPDATE'
      }
    }
    if(element && !sameType) {
      newFiber = {
        type: element.type,
        props: element.props,
        parent: wipFiber,
        dom: null,
        alternate: null,
        effectTag: 'PLACEMENT'
      }
    }
    if(oldFiber && !sameType) {
      oldFiber.effectTag = 'DELETION';
      deletions.push(oldFiber);
    }

    if(oldFiber) {
      oldFibler = oldFibler.sibling;
    }

    if(index === 0) {
      wipFiber.child = newFiber;
    }else {
      prevSibling.sibling = newFiber;
    }
    prevSibling = newFiber;
    index++;
  }
}

function updateHostComponent(fiber) {
   // 1. 把 fiber 对应的内容渲染到页面上
   if(!fiber.dom) {
    fiber.dom = ReactDOM.createDom(fiber);
  }
  // 2. 计算下一层 fiber tree
  const elements = fiber.props.children;
  reconcileChildren(fiber, elements);
}

function updateFunctionComponent(fiber) {
  const children = [fiber.type(fiber.props)]; 
  reconcileChildren(fiber, children);
}

// 该函数用来处理下一个执行的单元,同时返回下下一个执行单元
// 用来生成 Fiber Tree 的一个函数,生成 fiber tree 的过程,在 React 中叫做 render
function performUnitOfWork(fiber) {
  const isFunctionComonent = fiber.type instanceof Function;

  if(isFunctionComonent) {
    updateFunctionComponent(fiber);
  }else {
    updateHostComponent(fiber)
  }
  // 3. 选择下一个要执行的 fiber 单元
  if(fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while(nextFiber) {
    if(nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}

// 自动调度
function workLoop(deadline) {
  while(nextUnitWork){
    nextUnitWork = performUnitOfWork(nextUnitWork);
  }
  if(!nextUnitWork && wipRoot) {
    // fiber tree 已经准备好了,需要一次性的挂载 DOM
    // 一次性把 fiber tree 的内容渲染到页面上,这个过程叫做 react 中的 commit 阶段
    commitRoot()
  }
  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

const ReactDOM = {
  createDom: function(fiber) {
    // 创建合理的元素节点
    const dom = fiber.type === "TEXT_NODE" ? document.createTextNode("") : document.createElement(fiber.type);
    // 给元素节点挂载属性
    Object.keys(fiber.props).filter(key=> key!== 'children').forEach((name) => { dom[name] = fiber.props[name] })
    return dom;
  },

  render: function(element, container) {
    wipRoot = {
      dom: container,
      props: {
        children: [element]
      },
      alternate: currentRoot,
    }
    deletions = [];
    nextUnitWork = wipRoot;
  }
};

总结

以上就是React18渲染jsx语法实现过程,首先使用Babel转换成React.createElement函数,再通过React.render开启渲染流程,采用分段渲染的模式,等待浏览器空闲时才做渲染,Render 阶段生成Fiber Tree树形结构,Commit 阶段渲染 DOM,这两个阶段都有Reconciliation(协调)的参与,尽可能地复用已有的DOM节点,提升渲染性能

希望对大家学习React有所帮助,有什么问题欢迎评论区留言,有什么地方表达不清楚的欢迎各位大佬提出宝贵的建议,如果喜欢可以点赞,关注,后面也会持续更新更多优质文章

转载自:https://juejin.cn/post/7370980495155232780
评论
请登录