React源码分析-手动实现React库

概述

这篇文章主要实现一个简单的React。主要争对下面8个点来实现。需要先使用
npx create-react-app my-app来创建一个运行的环境

  • Step I: 实现createElement函数
  • Step II: 实现render函数
  • Step III: 并发模式
  • Step IV: Fibers (虚拟dom)
  • Step V: Render and Commit 阶段(渲染和提交阶段)
  • Step VI: Reconciliation 阶段(dom diff)
  • Step VII: Function Components
  • Step VIII: Hooks

最基本的React是什么样子的?

const element = <h1 title="foo">Hello</h1>

React会通过createElement将element转化为虚拟dom

const container = document.getElementById("root")

ReactDOM.render(element, container)

大概如上图,就是babel解析后会调createElement将 Jsx转化为虚拟dom,将虚拟dom转化为真实dom,将真实dom挂载到root跟节点这一过程。

React会通过createElement将element转化为虚拟dom过程
const element = <h1 title="foo">Hello</h1>  //bable解析后调用createElement
const element = React.createElement(
"h1",
{ title: "foo" },
"Hello"
)
 最终得到
 const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}

1、实现createElement函数

React.createElement函数有3个参数,第一个是dom节点类型,第二个是dom节点的属性,第三个是其他子节点

/**
 * 
 * @param {*} type dom节点类型
 * @param {*} props dom节点的属性
 * @param  {...any} children //其他子节点
 * @returns 
 */
function createElement(type,props,...children){
  return {
    type,
    props:{
      ...props,
      //如果子节点是一个数值型或者字符串就创建一个text节点
      children:children?.map(child => 
        typeof child === 'object' 
        ? child
        : createTextElement(child)
        )
    }
  }
}


function createTextElement(text){
  return {
    type:'TEXT_ELEMENT',//自己定义一个常量
    props:{
      nodeValue: text,
      children: [],
    }
  }
}
2、实现render函数

我们得render函数是渲染的入口函数,一般2个参数,第一个是要渲染的虚拟dom,还有一个就是需要挂载的节点

function render(element, container) {

  const { type, props } = element
  const dom = type === 'TEXT_ELEMENT' //是否是文本节点
    ? document.createTextNode("")
    : document.createElement(type)


  const isProperty = key => key !== 'children'

  //将元素props分配给节点
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name]
    })


  //我们递归地为每个children做同样的事情
  element.props?.children?.forEach(child => {
    render(child, dom)
  });

  container.appendChild(dom)

}


通过render函数,我们已经实现了一个可以渲染的库。但是离实现一个React我们才开始。

3、并发模式

上面的render是采用递归的方式更新,不能终端,一旦dom多了,更新必然会卡顿,我们就需要一个可中断的异步更新来代替同步更新。想要实现异步更新,我们需要把更新拆分得更细,比如之前需要渲染整棵dom树,现在我们就渲染树的每个节点。我们将把工作分成几个小单元,在完成每个单元后,如果有其他需要做的事情,我们将让浏览器中断渲染。

let nextUnitOfWork = null //全局定义下一个需要更新的单元

function performUnitOfWork(fiber){
  //得到下一个需要更新的fiber
}


function workLoop(deadline){
  let shouldYield = false //优先级有关的
  while(nextUnitOfWork && !shouldYield){//遇到高优先级的就中断

    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1

  }

  requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)//第一次主动调度

requestIdleCallback类似于setTimeout函数,不过它是在浏览器空闲的时候调度。

现在大家可能会猜到,我们定义了一个nextUnitOfWork,他会在调用render的时候第一次赋值。我们改造一下render

4、fiber

fiber是React里面存储数据的结构,如下图

在这里插入图片描述

每个节点基础的一些属性就是【cildren,parent,sibing,props】等等
react里面使用的是双缓存机制,一个是currentFiber表示展示在屏幕上domTree的结构,还有一个workInProgressFiber表示更新渲染过程中的domeTree,最后用更新好的workInProgressFiber 替换currentFiber就更新完成。
他们通过alternate属性连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
//react中采用双缓存
let currentFiber = null 
let workInProgressFiber = null

function render(element, container){
  workInProgressFiber = {
    dom:container,
    alternate:currentFiber,
    props:{
      children:[element]
    }
  }
  nextUnitOfWork = workInProgressFiber
}


创建节点的函数我们就新取一个名createDom

function createDom(fiber) {
  const dom =
    fiber.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type)

  const isProperty = key => key !== "children"
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = fiber.props[name]
    })

  return dom
}

这样在调render函数的时候,我们就找到了第一个更新的单元,然后会在浏览器空闲的时候调用requestIdleCallback,就正式开启了React的渲染

5、Render and Commit 阶段

React中有render阶段和commit阶段,render阶段是创建得到最后的fiber数据,commit阶段就是把fiber数据渲染到页面。React中的diff比较是在render阶段。下面先看看render阶段干了些什么

6、Reconciliation 阶段

这个阶段主要是在第一次新增的时候是创建render tree,在后面update的时候进行的是diff 比较,将没有修改的部分直接使用原先的数据替换。这个部分主要在performUnitOfWork中经行。

function performUnitOfWork(fiber) {
  const isFunctionComponent =
    fiber.type instanceof Function
  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}

function updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0
  wipFiber.hooks = []
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
  reconcileChildren(fiber, fiber.props.children)
}
function reconcileChildren(wipFiber, elements) {
  let index = 0
  let oldFiber =
    wipFiber.alternate && wipFiber.alternate.child
  let prevSibling = null

  while (
    index < elements.length ||
    oldFiber != null
  ) {
    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,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT",
      }
    }
    if (oldFiber && !sameType) {
      oldFiber.effectTag = "DELETION"
      deletions.push(oldFiber)
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }

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

    prevSibling = newFiber
    index++
  }
}

后面继续更新

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值