React源码学习入门(十三)我用300行代码实现了React

之前我们基本将React源码的加载、更新过程分析完了,现在我们完全可以上手写一个自己实现的React,让我们一起来到学习金字塔的下层,印证之前所学。

准备工作

我们先使用最新版create-react-app,在example/目录下创建一个demo项目:

npx create-react-app demo

跑起来后,将index.js替换如下(要去掉webpack的ModuleScopePlugin插件,否则会报错):

import React from '../../../react';
import ReactDOM from '../../../reactDom';
import './index.css';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

在根目录补充react.jsreactDom.js,其中reactDom.js给一个render的实现:

export default class ReactDom {
    static render(element, container) {
        console.log('触发了render', element, container);
    }
}

跑起来项目后,我们发现控制台已经输出了:

代码目录结构是这样:

这个时候初始的准备工作就完成了,接下来我们可以聚焦在如何实现上。

实现React的挂载

初始化控制类

根据我们之前对React挂载机制的分析,首先需要实现的是相应控制类,在这里我们可以简化一下,实现两个控制类就好:

  • compositeComponent

  • domComponent

我们先分别初始化好这两个控制类:

实例化入口

然后实现一个实例化方法,这里可以根据jsx element的type来分别实例化两种控制类:

mount入口

最后我们在render里面实例化控制类,并执行mount流程:

当然,我们还需要实现一个mount的方法,先在composite实现:

可以看到我们的控制台输出,已经走到了mount方法,至此我们的目录结构是这样:

实现CompositeComponent的mount

接下来我们需要实现React组件的挂载逻辑,对于React组件来讲,其实挂载就相当于触发生命周期以及执行render,在做这些之前,我们首先得创建组件的实例:

注意这里我们根据isClassComponent来区分React组件是类组件还是函数组件,后面我们在实现类组件的时候会加上这个属性。

函数组件是不需要实例化的。

在实例化之后,就需要触发render:

要注意这里,如果对于类组件的话是调用render方法,对于函数组件则是直接调用函数。

我们看到控制台已经输出了render之后的element。最后我们让这个element再执行mount,从而开启递归挂载的流程:

最终叶子节点会走到DOM的mount:

至此,CompositeComponent的挂载过程就已经实现好了。

实现DomComponent的mount

接下来实现DomComponent的挂载过程,实际上对于DOM组件来说,我们需要实际创建一个DOM节点出来:

可以看到控制台上已经输出了我们创建好的DOM节点。

然后我们需要处理一下DOM属性:

注意这里对于属性的两点处理:

  • 跳过了children属性,这个属于jsx子元素语法,不属于DOM属性

  • 修正了className属性,在DOM中应该设置class

可以看到控制台,DOM属性已经生效了。

接着我们需要递归挂载DOM的子节点。

在我们挂载子节点时,发现jsx还会生成一类文本类型的element,我们需要额外再处理下,调整一下instantiate的代码:

增加一个TextComponent

然后,我们就可以对DOM子节点进行遍历递归挂载了:

可以看到目前我们的控制台中已经完全输出了被挂载好的DOM元素,现在只差最后一步了。

挂载DOM至Container

最后一步其实非常简单,我们只需要将拿到的DOM元素挂载到container上:

撒花💐💐💐!!!

写到这里,我们create-react-app的代码已经被正确地渲染到屏幕上了。

回顾一下整个渲染的代码,加起来也就50行左右,我们就实现了React挂载的核心,这就是代码的魅力,也是我们努力坚持看源码所获得的成果。

我们目前的目录结构:

实现React的更新

由于create-react-app默认生成的是一个函数组件,我们做更新目前暂时需要类组件去更新state,所以我们新写一个class组件,把React之前的Counter组件搬过来:

支持类组件

然后我们在react.js中实现一下Components

支持事件触发

由于这里我们是通过事件触发的,我们在挂载里面加一下事件的支持:

增加一个event.js,简单封装一下事件的处理:

然后我们在handleClick回调里输出一下:

可以看到事件回调已经被执行,一个简单的事件就支持好了。

缓存控制类实例和组件实例的关系

在实现setState之前,我们首先要缓存一下组件实例和控制类的关系,来方便我们更新的时候可以精准找到之前挂载时的控制实例:

在组件初始化实例的时候存入:

在setState的时候取出:

可以看到控制台中我们已经取到了控制实例。

实现setState

其实setState的核心逻辑就是update,我们直接调用控制类的update方法即可。

这里update,首先要更新组件的state,其次触发一下render,我们看一下控制台结果:

可以看到已经拿到了最新的element

接下来要将DOM更新,我们需要找到之前的DOM节点,实现一个getHostNode方法:

对于compositeComponent来说,其实是递归查找叶子节点的,这里的renderedComponent是我们之前挂载的时候赋值的:

最终会找到叶子节点的getHostNode

我们输出一下看看:

可以看到已经拿到了hostNode

接着我们先不考虑Diff,直接粗暴更新节点,先将当前组件挂载:

对于React组件的挂载,递归执行叶子节点的挂载。

注意在domComponenttextComponent我们也不能直接删除DOM元素,因为在删除后需要把新的DOM节点插回到原来的位置,这个时候我们在外面用replaceChild更方便,就不在里面处理了。

在外面我们update的时候,采用销毁重建的方式将子节点替换:

注意这里的toMount方法重新抽象了一下,相比mount排除掉了实例化的过程:

这样我们的节点就会更新到最新了:

至此我们其实已经实现了React更新状态的逻辑,整个功能实现已经完成!

我们最终的目录结构:

实现简易的diff算法

实际上当我们判断两个组件类型没有发生变化的时候,是不需要销毁重建的,我们将diff算法实现一下:

这里updatesetState的入口,为了区分是当前组件自更新还是由于父组件更新引起的子组件更新,我们分为updatereceive两个方法,当前后的子元素类型没有发生变化的时候,我们可以直接走receive

接着分两部分来看receive的实现,一个是React组件本身,一个是叶子节点,先看React组件本身:

当组件本身调用receive的时候,说明是父组件的更新引起当前组件更新,那需要更新当前组件的所有信息,并且递归子组件的更新(这里调用update接口递归)。

再来实现一下DOMComponentreceive

当DOM节点走到receive的时候,说明当前DOM节点类型是一致的,那我们先对当前DOM节点的属性进行更新,再递归它的子元素。

首先是更新属性:

我们首先考虑到的是新属性的更新替换,需要额外处理一下事件的重新监听。然后是新属性不存在的老属性的删除。

在更新完当前节点的属性后,需要递归更新子元素:

这里其实就是DOM Diff的实现了,除了没有支持key的优化外,和之前我们分析过的DOM Diff算法保持一致,有三种情况:

  • 新节点直接插入(旧节点不存在)

  • 新节点替换(类型相同,递归receive,类型不同,销毁重建,replaceChild)

  • 旧节点删除

最后是文本叶子节点的实现,可以直接替换文本内容:

至此我们就实现了整个Diff算法,现在点击按钮是不会触发DOM的销毁重建的:

生命周期钩子支持

最后我们来完善一下React生命周期函数的支持,主要是React组件的几个声明周期:

  • componentWillMount

  • componentDidMount

  • componentWillUpdate

  • componentDidUpdate

  • componentWillReceiveProps

这里对代码进行微调,update的hook需要注意时机。通过execHook来触发相应的Hook,在组件里面做个测试:

可以看到相应的生命周期我们已经能正常运作。

至此一个最小版本的React已经全部开发完成!

小结一下

我们通过300行左右的代码实现了React的核心逻辑,麻雀虽小,但五脏俱全,让我们回顾下实现了什么:

  • 支持React挂载,DOM挂载,JSX语法render

  • 支持函数式组件、类组件的写法

  • 支持通过setState更新组件状态

  • 支持React完整的生命周期

  • 支持diff算法,不会频繁进行DOM的挂载与删除

这些特性也是支撑React的核心逻辑。

而我们不支持的绝大多数是React16之后的特性,如:

  • 不支持fiber架构

  • 不支持React hooks

  • 不支持Fragment等

本篇文章的实现可以作为对之前React源码分析的成果检验,事实证明通过之前源码的学习,我们现阶段是完全可以实现React的。

本文相关代码已上传github,相关资源:

Last updated