Skip to content

Latest commit

 

History

History
372 lines (295 loc) · 8.9 KB

01.从零构建一个简单的 react.md

File metadata and controls

372 lines (295 loc) · 8.9 KB

从零构建一个简单的 react

这篇文章会带领读者从零构建一个简单的 react,一共大概 300 行代码。其中包含了 react 的核心理念,例如:fibers、hooks、reconciliation。

这边文章专注实现 React的方法组件 ,所以没有 Class组件相关的实现代码。

从头开始,以下是我们逐一添加到 React 版本中的所有内容:

  • 第一步: createElement Function
  • 第二步: render Function 渲染函数
  • 第三步: Concurrent Mode 并发模式
  • 第四步: Fibers
  • 第五步: Render and Commit 阶段
  • 第六步: Reconciliation 调和
  • 第七步: Function Components 函数组件
  • 第八步: Hooks

第一步:先谈谈 JSX

众所周知,React 的一大特色是 JSX 语法,它本质上是一个语法糖,类似 XML 的风格让 JSX 拥有更丰富的表现力。

例如下面的代码:

const element = <div title='foo'>Hello</div>

// 等同于

const element = React.createElement(
  'div',
  { title: 'foo' },
  'Hello'
)

再查看一个嵌套的例子:

const element = (<div title='foo'>
  <span id='a'>hello</span>
  <span id='b'>world</span>
</div>)

// 等同于

const element = React.createElement(
  'div',
  { title: 'foo' },
  React.createElement(
    'span',
    { id: 'a' },
    'hello'
  ),
  React.createElement(
    'span',
    { id: 'b' },
    'world'
  )
);

可以看到与使用原生 JS 相比, JSX 的表达更加简约直接,上面的例子可以直接运行,查看如下代码:

import React from 'react';
import ReactDOM from 'react-dom';

const element = React.createElement(
  'div',
  { title: 'foo' },
  React.createElement(
    'span',
    { id: 'a' },
    'hello'
  ),
  React.createElement(
    'span',
    { id: 'b' },
    'world'
  )
);

const container = document.getElementById('root');
ReactDOM.render(element, container);

运行 yarn demo11 可以查看效果。

解析 JSX 不在这篇文章要将的范畴,我们会借助类似 babel 这样的转换工具帮我们转换 JSX

第二步:实现 createElement 方法

上面我们谈论 JSX 的时候,我们用到了 React.createElement 方法,它是干什么的呢?

我们在 第一步 的例子中打印个日志看一下它的数据结构,如下图:

其中 propstype 这两个属性比较关键,这两个属性会帮助我们构建完整的虚拟dom树。其中:

type 是一个字符串,我们用它来创建不同类型的 DOM 节点,例如 divspanh1

props 是一个对象,它具有 JSX 属性中的所有 key 和 value。另外,它还有一个特殊的属性:children,它又是与上面一样类型的对象,这也是为什么整个对象其实是一棵树。

如果用 TypeScript 来表示这个对象,可以写为:

export interface IPropsFiber {
  type: string;
  props: {
    children?: IPropsFiber[];
    [props: string]: any
  };
}

注意到我们的类型定义里有 fiber 这个单词,这一个重要的概念,但是这里先不展开讲,我们后续会更详细介绍。

言归正传,createElement 要做的事情很简单,就是根据入参转换出一个树结构出来,例如:

const element = (<div title='foo'>
  <span id='a'>hello</span>
  <span id='b'>world</span>
</div>)

// 等同于

const element = MyReact.createElement(
  'div',
  { title: 'foo' },
  MyReact.createElement(
    'span',
    { id: 'a' },
    'hello'
  ),
  MyReact.createElement(
    'span',
    { id: 'b' },
    'world'
  )
);

// 等同于
const element = {
  type: 'div',
  props: {
    title: 'foo',
    children: [
      {
        type: 'span',
        props: {
          id: 'a',
          children: [
            {
              type: 'TEXT_ELEMENT',
              props: {
                nodeValue: 'hello',
                children: []
              }
            }
          ]
        }
      },
      {
        type: 'span',
        props: {
          id: 'b',
          children: [
            {
              type: 'TEXT_ELEMENT',
              props: {
                nodeValue: 'world',
                children: []
              }
            }
          ]
        }
      }
    ]
  }
};

注意到 createElement 会有三个或者更多入参,并且第三个参数后(包括第三个参数)都是其子元素,所以 createElement 的形参可以定义为:

createElement(type, props, ...children){
  // ...
}

这样 children 其实就是包含所有子元素的数组。另外要注意,children 有可能包含原始数据类型,例如字符串,因此,我们需要将所有不是对象的 element 也包装成 element 对象,并为其创建特殊类型: TEXT_ELEMENT

于是我们的 createElement 方法可以定义为:

function createElement(type: string, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === 'object'
          ? child
          : createTextElement(child)
      )
    }
  };
}

function createTextElement(text: string) {
  return {
    type: 'TEXT_ELEMENT',
    props: {
      nodeValue: text,
      children: []
    }
  };
}

运行 yarn demo12 可以查看效果:

可看到一个树状的数据结构。

细心的同学会在 demo12 中看到 @jsx 的注释,如下:

/** @jsx createElement */
const element = (<div title='foo'>
  <span id='a'>hello</span>
  <span id='b'>world</span>
</div>);

解释一下,加上这行注释 parcel 就会用 babel 相应的插件来解析 JSX 语法。

总结一下:在第二步中,我们借助了 babel 的能力解析了 JSX 语法,然后通过我们自定义的 createElement 实现了树状结构。

第三步:实现 render 方法

实现 render 方法其实非常简单:

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

是的,就是这个可用的 render 方法。

但是,我们需要考虑递归的问题,因为 render 的第2个参数是一个树状的结构。

于是我们优化如下:

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

考虑到 element 可能是原始数据类型,我们还需要继续优化:

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

另外,我们还要将树中属性写到 Dom 上:

  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)
  }

注意到这里过滤了 children 属性,因为在 react 中 children 表示子元素。

先阶段完整的 react 代码如下:

function createElement(type: string, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === 'object'
          ? child
          : createTextElement(child)
      )
    }
  };
}

function createTextElement(text: string) {
  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);
}

/** 以下是业务代码 **/

/** @jsx createElement */
const element = (<div title='foo'>
  <span id='a'>hello</span>
  <span id='b'>world</span>
</div>);

render(element, document.getElementById('root'));

我们可以运行 yarn demo13 查看它的运行效果,和 react 是一样的效果。

第四步

相关链接

Build your own React react-fiber-architecture React docs 烤透 React Hook