超超超详细-实现一个简单的基于 React Fiber 的 React

我们将从头一步一步来开始重写React,遵循真正的React代码的架构,但没有优化和非必要的功能。本次博客基于16.8版本使用hooks:

  1. createElement函数
  2. render渲染函数
  3. 并发模式
  4. Fibers
  5. 渲染和提交阶段
  6. 调和Reconciliation
  7. 函数组件
  8. Hooks

首先,先回顾一下基本概念:我们先定义了一个react元素,接着获取节点,最后渲染到容器中:

const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)

让我们用javascript来代替react这种写法:首先我们使用JSX语法去定义一个元素,JSX可以被Babel等构建工具转换为JS,只需要将标签名、属性和子元素作为参数传递即可。

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

使用React.createElement根据指定的第一个参数去创建一个react元素

const element = React.createElement(
  "h1",
  { title: "foo" },
  "Hello"
)

生成的元素长这个样子:是一个拥有两个属性的对象

  1. type 为字符串类型,指定我们要创建DOM节点的类型,当你想创建一个HTML元素时,它就是咱们document.createElement的tagName,它也可以是一个函数。【】
  2. props 为对象类型,它具有所有来自JSX属性的键和值,它还有一个特殊的属性:children,children在这里是一个字符串,但它通常是数组
const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}

接着,我们需要替换的另一段React代码是对ReactDOM.render的调用。render是React改变DOM的地方,所以让我们自己来做更新:

ReactDOM.render(element, container)

首先我们创建一个node节点,添加一个标题title属性,接着为子节点创建一个text文本节点,也添加一个值nodeValue属性:

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}

const node = document.createElement(element.type)
node["title"] = element.props.title


const text = document.createTextNode("")
text["nodeValue"] = element.props.children

最后,我们将text节点添加到node节点,node节点添加到容器root里:

const container = document.getElementById("root")
​​
node.appendChild(text)
container.appendChild(node)

 现在我们的代码和之前的相同没有使用React。

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}
​
const container = document.getElementById("root")
​
const node = document.createElement(element.type)
node["title"] = element.props.title
​
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
​
node.appendChild(text)
container.appendChild(node)

 1.createElement函数

我们将编写我们自己的createElement开始,让我们把JSX转换为JS,这样我们就可以看到createElement的调用。比如以这个为例:

const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

JSX可以被Babel等构建工具转换为JS:

const element = React.createElement(
  "div",
  { id: "foo" },
  React.createElement("a", null, "bar"),
  React.createElement("b")
)

接下来我们去创建createElement函数,形参为type、props、children,对props使用扩展运算符将数组或者对象转为用逗号分隔的参数序列,对children使用rest剩余参数将一个不定数量的参数表示为一个数组。

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children,
    },
  }
}

比如: createElement("div")返回:

{
  "type": "div",
  "props": { "children": [] }
}

createElement("div", null, a)返回:

{
  "type": "div",
  "props": { "children": [a] }
}

createElement("div", null, a, b) 返回:

{
  "type": "div",
  "props": { "children": [a, b] }
}

children数组也可以包含字符串或者数字,因此,我们把所有不是对象的包在自己的元素里,并为它们创建一个特殊的标签类型:TEXT_ELEMENT

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    },
  }
}
​
function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  }
}
​

我们仍然使用React的createElement,为了取代它,我们重新自定义名为Didact,但我们仍然想在这里使用JSX。我们如何告诉babel使用Didact的createElement而不是React的?

const element = React.createElement(
  "div",
  { id: "foo" },
  React.createElement("a", null, "bar"),
  React.createElement("b")
)
const Didact = {
  createElement,
}
​
const element = Didact.createElement(
  "div",
  { id: "foo" },
  Didact.createElement("a", null, "bar"),
  Didact.createElement("b")
)

如果我们有一个像这样的注释,当babel转置JSX时,它将使用我们定义的函数。

/** @jsx Didact.createElement */
const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)

2. render渲染函数

接下来,我们将编写我们的ReactDOM.render函数,我们只关心在DOM中添加东西,以后会处理更新和删除。

function render(element, container) {
  // TODO create dom nodes
}
​
const Didact = {
  createElement,
  render,
}

我们首先使用元素类型创建DOM节点,然后将新节点追加到容器中。

function render(element, container) {
  const dom = document.createElement(element.type)
​
  container.appendChild(dom)
}

我们递归地对每个child进行同样的处理:

function render(element, container) {
  const dom = document.createElement(element.type)
​
  element.props.children.forEach(child =>
    render(child, dom)
  )
​
  container.appendChild(dom)
}

我们还需要处理文本元素,如果元素类型是TEXT_ELEMENT,我们就创建一个文本节点,而不是普通节点。

function render(element, container) {
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type)
​
  element.props.children.forEach(child =>
    render(child, dom)
  )
​
  container.appendChild(dom)
}
​

我们在这里需要做的最后一件事是把元素的属性props分配给节点。

const isProperty = key => key !== "children"

Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name]
    })
​

我们现在有了一个可以将JSX渲染到DOM的库啦!

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    },
  }
}
​
function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  }
}
​
function render(element, container) {
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type)
​
  const isProperty = key => key !== "children"

  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name]
    })
​
  element.props.children.forEach(child =>
    render(child, dom)
  )
​
  container.appendChild(dom)
}
​
const Didact = {
  createElement,
  render,
}
​
/** @jsx Didact.createElement */
const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)
const container = document.getElementById("root")
Didact.render(element, container)

可以通过codesandbox来练习练习:

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值