手把手带你写一个mini版的React,掌握React的基本设计思想

4,533 阅读13分钟

一、总体实现目标

本文通过手写一个简易版 React,来了解 Fiber 架构到底做了些什么?从而对 React 基本原理有一个直观的认识;

学习建议:下载本节代码,对照着文章查看,尽量动手实现一遍;

实现的版本为16.8,基于pomb.us/build-your-…

实现目标如下:

  1. createElement;
  2. render;
  3. 并发模式;
  4. Fibers;
  5. 渲染和提交 ;
  6. 协调;
  7. 函数组件;
  8. hooks;
  9. 类组件

二、createElement实现

学习建议:下载本节代码,对照着文章查看,尽量动手实现一遍。

1、前言

在React17之前,我们写React代码的时候都会去引入React,不引入代码就会报错,而且自己的代码中没有用到React,这是为什么呢?带着这个问题我们向下学习;

import React from 'react'

2、element变量解析

我们先创建一个element变量,将本段代码放到babel上查看编译结果:

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

通过babel会编译成下面这种形式:

image.png

经过编译后的代码为:

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

element参数说明:

  • dom元素
  • 属性
  • children子元素

解答一下开篇提出的问题:引入React的作用,使用React进行解析JSX,如果不引入React,上面代码就会报错。JSX实际上是一个语法糖,它真正是需要解析成js代码来执行;

3、创建项目

我们先来创建执行命令:

npm init 

安装相关的依赖:

npm install --save-dev babel-loader @babel/core
npm install webpack --save-dev
npm install --save-dev @babel/preset-react
npm install --save-dev html-webpack-plugin 

创建项目目录:

image.png

配置webpack:

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    main: './src/index.js'
  },
  devServer: {
    port: 9000,
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: ['@babel/plugin-transform-react-jsx']
          }
        }
      }
    ]
  },
  mode: "development",
  optimization: {
    minimize: false
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'React',
    }),
  ],
}

加入启动命令:

image.png

4、打印结果值

创建一个真实的React项目,使用create-react-app,本文就不在叙述安装过程。再来看看上文的 React.createElement 实际生成什么?打印一下element:

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

const element = <h1 title="foo">Hello</h1>
console.log(element)

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

打印结果: image.png

简化一下,将其他属性刨除(其他属性我们不关心):

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

简单总结一下,React.createElement 实际上是生成了一个 element 对象,包含两个属性对象 type 和 props ,该对象拥有以下属性:

element对象参数:

  • type:标签名称
  • props:属性
    • title:标签属性
    • children:子属性

5、render简单流程

提前了解一下render的简单流程:

image.png

ReactDOM.render() 将 element 添加到 id 为 root 的 DOM 节点中,我们接下来实现这个方法来代替React源码中的 ReactDOM.render()方法;

示例代码:

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

1.首先,我们使用元素类型创建一个节点(element.type) ,在本例中是 h1;

const node = document.createElement(element.type)

2.设置节点属性为title;

node["title"] = element.props.title

3.只有一个字符串作为子节点,我们创建一个文本节点,并且设置文本节点的nodeValue为element.props.children;

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

4.最后,我们将 textNode 附加到 h1,并将 h1附加到容器;

node.appendChild(text)
container.appendChild(node)

6、createElement实现(虚拟DOM)

用我们自己的代码实现React的代码;

从上文了解到createElement的作用是创建一个element对象:

const element = {
  type: "h1",  //标签
  props: {
    title: "foo", // 属性
    children: "Hello", // 节点
  },
}

调用方式:

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

根据调用和返回结果,设计createElement函数如下:

// react/createElement.js

/**
 * 创建虚拟DOM结构
 * @param {*} type 标签
 * @param {*} props 属性
 * @param  {...any} children 自己诶单 
 * @returns 虚拟DOM结构
 */
export function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === "object" 
          ? child  
          : createTextElement(child) //不是对象说明是文本节点
      ),
    },
  }
}


/**
 * 当children为非对象时,创建文本节点
 * @param {*} text 文本值
 * @returns 虚拟DOM结构
 */
function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  }
}

为了直观的展示,我们更改一下element结构:

const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)

测试一下:

// src/index.js
import React from '../react';

const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)

console.log(element

打印结果:

image.png

7、本节代码

地址:github.com/linhexs/min…

三、render实现

本节目标实现ReactDOM.render,只关心向 DOM 添加内容,之后处理更新和删除内容;

本节代码实现目录为:react/react-dom.js文件;

有了虚拟 DOM 数据结构,接下来要把它转换成真实的DOM结构渲染到页面上,基本分为四步:

  • 创建不同类型节点
  • 添加属性 props
  • 遍历 children,递归调用 render
  • 将生成的节点 append 到 root 根节点上

具体实现步骤:

1、新建react-dom.js文件

image.png

2、创建 DOM 节点,然后将新节点添加到容器

// react/react-dom.js
/**
 * 将虚拟 DOM 转换为真实 DOM 并添加到容器中
 * @param {element} 虚拟 DOM
 * @param {container} 真实 DOM
 */
function render(element, container) {
  const dom = document.createElement(element.type)
  container.appendChild(dom)
}

3、将 element.props.children 都添加至 dom 节点中

 element.props.children.forEach(child =>
    render(child, dom)
  )

4、处理文本节点

const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type)

5、为节点绑定属性

// 为节点绑定属性
  const isProperty = key => key !== "children"
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name]
    })

6、测试

以上我们实现了一个jsx转换为dom的库,测试一下:

6.1 将render方法引入到react/index.js文件中

image.png

6.2 添加React.render方法

在src/index.js文件添加React.render方法:

// src/index
import React from '../react';

const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)

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

6.3 修改webpack配置

在src目录下添加index.html文件,并添加一个dom属性为id的节点: image.png

修改webpack配置,添加html模板: image.png image.png

6.4 运行

运行命令 npm run start 启动,可以看到已经成功显示出结果: image.png

7、总结

使用流程图简单总结一下2、3小节:

image.png

8、本节代码

代码地址:github.com/linhexs/min…

四、并发模式&fiber导读

1、出现的问题

上节的递归调用会出现一个问题,也就是一旦开始渲染,就不能停止了,直到渲染出完整的树结构。也就是说会造成主线程被持续占⽤,造成的后果就是主线程上的布局、动画等周期性任务就⽆法立即得到处理,造成视觉上的卡顿,影响⽤户体验。

在浏览器中,页面是一帧一帧绘制出来的,主流浏览器刷新频率为60Hz,即每(1000ms / 60Hz)16.6ms浏览器刷新一次,在这一帧中浏览器要完成JS脚本执行、样式布局、样式绘制,如果在某个阶段执行时间很长,超过 16.6ms,那么就会阻塞页面的渲染,从而出现卡顿现象,也就是常说的掉帧。

2、如何解决

使用增量渲染(把渲染任务拆分成块,匀到多帧),将把工作分解成小单元,在完成每个单元之后,如果还有其他任务需要完成,我们将让浏览器中断渲染,也就是经常听到的fiber。 ​

关键点: ​

  • 增量渲染
  • 更新时能够暂停,终止,复⽤渲染任务
  • 给不同类型的更新赋予优先级

3、什么是fiber

用一张非常经典的图: image.png

简单说一下:fiber是指组件上将要完成或者已经完成的任务,每个组件可以⼀个或者多个。

4、window.requestIdleCallback()

实现fiber的核心是window.requestIdleCallback()window.requestIdleCallback()⽅法将在浏览器的空闲时段内调⽤的函数队列。关于window.requestIdleCallback()请点击查看文档。 ​

你可以把 requestIdlecallback 想象成一个 setTimeout,但是浏览器不会告诉你它什么时候运行,而是在主线程空闲时运行回调。 ​

window.requestIdleCallback将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。 ​

我们使用window.requestIdleCallback来实现代码:

// 下一个功能单元
let nextUnitOfWork = null

/**
 * 工作循环
 * @param {*} deadline 截止时间
 */
function workLoop(deadline) {
  // 停止循环标识
  let shouldYield = false

  // 循环条件为存在下一个工作单元,且没有更高优先级的工作
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    // 当前帧空余时间要没了,停止工作循环
    shouldYield = deadline.timeRemaining() < 1
  }
  // 空闲时间应该任务
  requestIdleCallback(workLoop)
}
// 空闲时间执行任务
requestIdleCallback(workLoop)

// 执行单元事件,返回下一个单元事件
function performUnitOfWork(fiber) {
  // TODO
}

performUnitOfWork函数功能我们会在下一节实现。 ​

5、本节代码

代码地址:github.com/linhexs/min…

五、fiber

1、Fiebr 作为数据结构

每个Fiber节点有个对应的React element,多个Fiber节点是如何连接形成树呢? ​

例如我们有这样一个属性结构:

react.render(
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>,
  root
)

React Fiber 机制的实现,是依赖下面这种数据结构(链表),每一个节点都是一个fiber。一个 fiber 包括了 child(第一个子节点)、sibling(兄弟节点)、parent(父节点)属性。

// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;

image.png

2、构建过程

在构建中,我们将创建根fiber,并将其设置为 nextUnitOfWork。剩下的工作将在 performUnitOfWork 功能上进行,每个fiber做了三件事: ​

  • 将元素添加到 DOM
  • 为元素的子元素创建fiber
  • 选择下一个工作单元

实现目的是为了查找下一个工作单元变得更容易,使用的深度优先遍历,先查找子节点,在查找兄弟节点。 ​

上图渲染过程详细描述如下: ​

  1. 当完成root的fiber工作时,如果有孩子,那么fiber是下一个工作的单元,root的子节点是div
  2. 当完成div的fiber工作时,下一个工作单元是h1
  3. h1的节点是p,继续下一个工作单元p
  4. p没有子节点,去找兄弟节点a
  5. a兄弟节点和子节点都没有,返回到父亲节点h1
  6. h1的子节点都已经工作完成了,去找h1的兄弟节点h2
  7. h2既没有兄弟节点,也没有子节点,返回到父亲节点div
  8. 同上,div在返回到父亲节点root
  9. 至此已经完成了所有的渲染工作

3、代码实现

1.抽离DOM节点的代码,放入到createDom()函数中;

/**
 * 创建DOM
 * @param {*} fiber fiber节点
 */
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
}

2.在 render 函数中,设置nextUnitOfWork为fiber的根节点,根节点只包含一个children属性;

export function render(element, container) {
  // 将根节点设置为第一个将要工作单元
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    },
  }
}

3.当浏览器存在空闲时间,开始处理根节点;

/**
 * 处理工作单元,返回下一个单元事件
 * @param {*} nextUnitOfWork 
 */
function performUnitOfWork(fiber) {
  // 如果fiber上没有dom节点,为其创建一个
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }

  // 如果fiber有父节点,将fiber.dom添加到父节点
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }
}

4.为每一个孩子节点创建一个新的fiber;

function performUnitOfWork(fiber) {
  
  // 省略上面内容
  
  // 获取到当前fiber的孩子节点
  const elements = fiber.props.children
  // 索引
  let index = 0
  // 上一个兄弟节点
  let prevSibling = null

  // 遍历孩子节点
  while (index < elements.length) {
    const element = elements[index]
    // 创建fiber
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    }
  }
}

5.将新节点添加到fiber树中;

function performUnitOfWork(fiber) {
  
  // 省略上面内容
  
 while(index < elements.length){
       
     // 省略上面内容
       
    // 将第一个孩子节点设置为 fiber 的子节点
    if (index === 0) {
      fiber.child = newFiber
    } else if(element) {
      // 第一个之外的子节点设置为第一个子节点的兄弟节点
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    index++
 }
}

6.寻找下一个工作单元,先查找孩子,然后兄弟,如果没有就返回父节点;

function performUnitOfWork(fiber) {
  
  // 省略上面内容
  
	// 寻找下一个孩子节点,如果有返回
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    // 如果有兄弟节点,返回兄弟节点
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    // 否则返回父节点
    nextFiber = nextFiber.parent
  }
}

4、效果

修改src/index.js文件

image.png

实现效果:

image.png

5、本节代码

代码地址:github.com/linhexs/min…

六、渲染和提交

1、存在问题

这里还存在一个问题,就是每次处理一个元素,都要向DOM添加一个新的节点,在完成整个树的渲染之前,由于做了可中断操作,那将看到一个不完整的UI,这样显然是不行的。 ​

2、处理步骤

1.删除子节点添加到父节点的逻辑;

function performUnitOfWork(fiber) {
  // 这段逻辑删了
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }
}

2.添加fiber根节点wipRoot,并设置为下一个工作单元;

let wipRoot = null
export function render(element, container) {
  // 将根节点设置为第一个工作单元
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
  }
  nextUnitOfWork = wipRoot
}

3.当完成了所有任务,也就是说没有下一个工作单元了,这时需要把整个fiber渲染为DOM;

// 提交任务,将fiber tree 渲染为真实 DOM
function commitRoot(){

}

function workLoop(deadline) {
  
 	// 省略
  
  // 没有下一个工作单元,提交当前fiber树
  if (!nextUnitOfWork && wipRoot) {
    commitRoot()
  }

  // 省略
  
}

4.在 commitRoot 函数中执行提交工作,递归将所有节点附加到 dom 中;

/**
 * 处理提交的fiber树
 * @param {*} fiber 
 * @returns 
 */
function commitWork(fiber){
  if (!fiber) {
    return
  }
  const domParent = fiber.parent.dom
  // 将自己点添加到父节点下
  domParent.appendChild(fiber.dom)
  // 渲染子节点
  commitWork(fiber.child)
  // 渲染兄弟节点
  commitWork(fiber.sibling)
}

/**
 * 提交任务,将fiber tree 渲染为真实 DOM
 */
function commitRoot(){
  commitWork(wipRoot.child)
  wipRoot = null
}

3、运行结果

运行结果没有问题 image.png

4、本节代码

代码地址:github.com/linhexs/min…

七、协调

1、前言

到目前为止,我们只实现了向DOM添加内容,所以接下来的目标我们实现更新和删除节点; ​

当执行更新时,我们要对比两棵fiber树,对有变化的DOM进行更新; ​

关于协调的原理篇请移步这里; ​

2、实现步骤

2.1 新增变量

新增 currentRoot 变量,保存根节点更新前的fiber树,添加alternate属性到每一个fiber,关联老的fiber,老fiber是我们上一次提交阶段提交给DOM的fiber;

// 更新前的根节点fiber树
let currentRoot = null

function render (element, container) {
    wipRoot = {
        // 省略
        alternate: currentRoot
    }
  // 省略
}

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

2.2 新建reconcileChildren并提取performUnitOfWork中的逻辑

提取创建新fiber的代码到reconcileChildren中; ​

performUnitOfWork代码更改:

/**
 * 处理工作单元,返回下一个单元事件
 * @param {*} fiber 
 */
function performUnitOfWork(fiber) {
  // 如果fiber上没有dom节点,为其创建一个
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }

  // 获取到当前fiber的孩子节点
  const elements = fiber.props.children
  
 	// 协调
  reconcileChildren(fiber, elements)

  // 寻找下一个孩子节点,如果有返回
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    // 如果有兄弟节点,返回兄弟节点
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    // 否则返回父节点
    nextFiber = nextFiber.parent
  }
}

reconcileChildren代码:

/**
 * 协调
 * @param {*} wipFiber 
 * @param {*} elements 
 */
function reconcileChildren(wipFiber,elements){
   // 索引
   let index = 0
   // 上一个兄弟节点
   let prevSibling = null
 
   // 遍历孩子节点
   while (index < elements.length) {
     const element = elements[index]
     // 创建fiber
     const newFiber = {
       type: element.type,
       props: element.props,
       parent: wipFiber,
       dom: null,
     }
 
     // 将第一个孩子节点设置为 fiber 的子节点
     if (index === 0) {
       wipFiber.child = newFiber
     }	else if (element) {
       // 第一个之外的子节点设置为第一个子节点的兄弟节点
       prevSibling.sibling = newFiber
     }
 
     prevSibling = newFiber
     index++
   }
}

2.3 对比新旧fiber

添加循环条件oldFiber,将newFiber赋值为null;

function reconcileChildren(wipFiber, elements) {
  
  // 省略
  
  // 上一次渲染的fiber
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child

  // 省略
  
   while (index < elements.length || oldFiber != null) {
     
   		// 省略
     
      const newFiber = null
      
      // 省略
 			
   }
  
  // 省略
  
}

新旧fiber进行对比,看看是否需要对 DOM 应用进行更改;

function reconcileChildren(wipFiber, elements) {
  
  // 省略
  
  // 上一次渲染的fiber
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child

  // 省略
  
   while (index < elements.length || oldFiber != null) {
     
   		// 省略
     	
      // 类型判断
    	const sameType = oldFiber && element && element.type == oldFiber.type
			
      // 类型相同需要更新
    	if (sameType) {
    	  // TODO update the node
   	 	}
     	
     	// 新的存在并且类型和老的不同需要新增
    	if (element && !sameType) {
    	  // TODO add this node
   	 	}
     
     	// 老的存在并且类型和新的不同需要移除
    	if (oldFiber && !sameType) {
      // TODO delete the oldFiber's node
   	 	}
     	
     	// 处理老fiber的兄弟节点
     	if (oldFiber) {
      	oldFiber = oldFiber.sibling
    	}
      
     // 省略
 			
   }
  
  // 省略
  
}



当类型相同时,创建一个新的fiber,保留旧的fiber的dom节点,更新props,此外还加入一个effectTag属性来标识当前执行状态;

function reconcileChildren(wipFiber, elements) { 
	while (index < elements.length || oldFiber != null) {
    
    // 省略
    
		// 类型相同只更新props
    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE", 
      }
    }
    
    // 省略
}

对于元需要一个新的 DOM 节点的情况,我们用 PLACEMENT effect 标签标记新的fiber;

function reconcileChildren(wipFiber, elements) { 
	while (index < elements.length || oldFiber != null) {
    
    // 省略
    
    // 新的存在并且类型和老的不同需要新增
    if (element && !sameType) {
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT",
      }
    }
    
    // 省略
}

对于需要删除节点的情况,没有新fiber,将 effect 标签添加到旧的fiber中,删除旧的fiber;

function reconcileChildren(wipFiber, elements) { 
	while (index < elements.length || oldFiber != null) {
    
    // 省略
    
   // 老的存在并且类型和新的不同需要移除
   if (oldFiber && !sameType) {
     oldFiber.effectTag = "DELETION"
     deletions.push(oldFiber)
   }
    
    // 省略
}

设置一个数组来存储需要删除的节点;

let deletions = null

function render(element, container) {
  
  // 省略
  
  deletions = []
  
  // 省略
}

渲染DOM时,遍历删除旧节点;

function commitRoot() {
  deletions.forEach(commitWork)
 
  // 省略
  
}

修改commitWork处理effectTag标记,处理新增节点(PLACEMENT);

function commitWork(fiber) {
  
 	// 省略
  
  if (
    fiber.effectTag === "PLACEMENT" &&
    fiber.dom != null
  ) {
    domParent.appendChild(fiber.dom)
  }
  
  // 省略
  
}

处理删除节点标记;

function commitWork(fiber) {
  
 	// 省略
  
  // 处理删除节点标记
  else if (fiber.effectTag === "DELETION") {
    domParent.removeChild(fiber.dom)
  }
  
  // 省略
  
}


处理更新节点,加入updateDom方法,更新props属性;

function updateDom(){
  
}


function commitWork(fiber) {
  
 	// 省略
  
  // 处理删除节点标记
  else if (
    fiber.effectTag === "UPDATE" &&
    fiber.dom != null
  ) {
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  }
  
  // 省略
  
}


updateDom方法根据不同的更新类型,对props更新;

const isProperty = key => key !== "children" 
// 是否有新属性
const isNew = (prev, next) => key =>
  prev[key] !== next[key]
// 是否是旧属性
const isGone = (prev, next) => key => !(key in next)

/**
 * 更新dom属性
 * @param {*} dom 
 * @param {*} prevProps 老属性
 * @param {*} nextProps 新属性
 */
function updateDom(dom, prevProps, nextProps) {
  // 移除老的属性
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach(name => {
      dom[name] = ""
    })

  // 设置新的属性
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      dom[name] = nextProps[name]
    })
}

修改一下createDom方法,将更新属性逻辑修改为updateDom方法调用;

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

  updateDom(dom, {}, fiber.props)
  return dom
}

添加是否为事件监听,以on开头,并修改isProperty方法;

const isEvent = key => key.startsWith("on")
const isProperty = key =>
  key !== "children" && !isEvent(key)

修改updateDom方法,处理事件监听,并从节点移除;

function updateDom(dom, prevProps, nextProps) {
  // 移除老的事件监听
  Object.keys(prevProps)
    .filter(isEvent)
    .filter(
      key =>
        !(key in nextProps) ||
        isNew(prevProps, nextProps)(key)
    )
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.removeEventListener(
        eventType,
        prevProps[name]
      )
    })
  
  // 省略
  
}

添加新的事件监听;

function updateDom(dom, prevProps, nextProps) {  
		// 添加新的事件处理
    Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.addEventListener(
        eventType,
        nextProps[name]
      )
    })
  
  // 省略
  
}

3、实现效果

修改src/index.js代码:

// src/index
import React from '../react';

const container = document.getElementById("root")

const updateValue = e => {
    rerender(e.target.value)
}

const rerender = value => {
    const element = (
        <div>
            <input onInput={updateValue} value={value} />
            <h2>Hello {value}</h2>
        </div>
    )
    React.render(element, container)
}

rerender("World")


运行:

image.png

4、本节代码

代码地址:github.com/linhexs/min…

八、function组件

1、前言

我们先来编写一个函数组件:

// src/index
import React from '../react'

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

const element = (<App name='foo'></App>)

React.render(element, document.getElementById("root"))



函数组件的和标签组件有乱两个不同点: ​

  • 函数组件中的fiber没有DOM节点
  • children是通过运行函数得到的而不是props

2、功能实现

2.1 performUnitOfWork添加判断函数组件;

function performUnitOfWork(fiber) {
  // 判断是否为函数
  const isFunctionComponent =
    fiber.type instanceof Function
  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    // 更新普通节点
    updateHostComponent(fiber)
  }
  
  // 省略
  
}  


function updateFunctionComponent(fiber) {
  // TODO
}

function updateHostComponent(fiber) {
  // TODO
}

2.2 抽离performUnitOfWork中的reconcileChildren方法到updateHostComponent函数;

function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
  reconcileChildren(fiber, fiber.props.children)
}

2.3 处理函数组件,执行函数组件来获取children;

/**
 * 函数组件处理
 * @param {*} fiber 
 */
function updateFunctionComponent(fiber) {
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

2.4 接下来我们处理一下没有DOM的fiber;

function commitWork (fiber) {
  
    // 省略
  
    let domParentFiber = fiber.parent
    // 一直向上找直到找到有dom的节点
    while (!domParentFiber.dom) {
        domParentFiber = domParentFiber.parent
    }
    const domParent = domParentFiber.dom
    
    // 省略
    
}

2.5 当移除一个节点时,需要不断的向下找,直到找到一个具有 DOM 节点的子节点。

/**
 * 处理提交的fiber树
 * @param {*} fiber 
 * @returns 
 */
function commitWork(fiber) {
  
  // 省略
  
  // 处理删除节点标记
  } else if (fiber.effectTag === "DELETION") {
    commitDeletion(fiber, domParent)
  } 
  
  // 省略
}



/**
 * 删除情况下,不断的向下找,直到找到有dom的子节点
 * @param {*} fiber 
 * @param {*} domParent 
 */
function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom)
  } else {
    commitDeletion(fiber.child, domParent)
  }
}

3、实现效果

image.png

4、本节代码

代码地址:github.com/linhexs/min…

九、hooks

1、前言

前几节我们实现了React基本功能,本节我们来添加React16.8的核心功能hooks。 ​

编写一个计数器的功能;

// src/index
import React from '../react'

function Counter() {
    const [state, setState] = React.useState(1)
    return (
        <div>
            <h1 >
                Count: {state}
            </h1>
            <button onClick={() => setState(c => c + 1)}>+1</button>
        </div>
    )
}
const element = <Counter />

React.render(element, document.getElementById("root"))

2、实现步骤

2.1 添加useState函数

// react/react-dom.js

function useState(initial){
  // todo
}

2.2 初始化全局变量

我们知道函数每重新调用一次,内部状态就会丢失,所以我们需要一个记录内部内部状态的变量; ​

设置正在工作的fiber和添加一个数组来记录多次调用useState,使用索引来追踪;

let wipFiber = null
let hookIndex = null

/**
 * 函数组件处理
 * @param {*} fiber 
 */
function updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0
  wipFiber.hooks = []
  
	// 省略
  
}

2.3 添加useState函数的state

/**
 * @param {*} initial 传进来的初始值
 * @returns 
 */
function useState(initial) {
  // 检查是否有旧的hooks
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  // 如果有旧的,就复制到新的,如果没有初始化
  const hook = {
    state: oldHook ? oldHook.state : initial,
  }

  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state]
}

2.4 添加useState函数的setState

设置一个新的正在进行的工作根作为下一个工作单元,这样工作循环就可以开始一个新的渲染阶段;

// 设置hooks状态
  const setState = action => {
    debugger
    hook.queue.push(action)
    // 设置一个新的正在进行的工作根作为下一个工作单元,这样工作循环就可以开始一个新的渲染阶段
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    nextUnitOfWork = wipRoot
    deletions = []
  }

2.5 更新状态

function useState(initial) { 
  
	// 省略
  
	const actions = oldHook ? oldHook.queue : []
	actions.forEach(action => {
    hook.state = typeof action === 'function' ? action(hook.state) : action
 	})
  
 // 省略
  
}

3、运行结果

把useState函数结果导出; image.png

image.png

运行结果: image.png

4、本节代码

代码地址:github.com/linhexs/min…

十、Class组件

1、前言

本节我们添加一个class组件,来完善一下我们的react;

// src/index
import React from '../react'

class Counter extends React.Component {
    render() {
        return (
            <div>
                <h1>我是</h1>
                <h2>class组件</h2>
            </div>
        )
    }
}

const element = <Counter />

React.render(element, document.getElementById("root"))

2、实现步骤

2.1 Component.js

我们在写class组件时,都会继承一个React.Component,所以我们先创建一个Component.js文件,使用isReactComponent来标识类组件;

// react/Component
export function Component(props) {
  this.props = props;
}
Component.prototype.isReactComponent = true;

2.2 修改performUnitOfWork方法

修改performUnitOfWork方法,增加对类组件和函数组件的判断;

function performUnitOfWork(fiber) {
  // 函数组件类组件处理
  if (fiber.type && typeof fiber.type === 'function') {
    fiber.type.prototype.isReactComponent ? updateClassComponent(fiber) : updateFunctionComponent(fiber)
  } else {
    // 更新普通节点
    updateHostComponent(fiber)
  }
  
  // 省略
  
}

/**
 * 类组件处理
 * @param {*} fiber 
 */
updateClassComponent(fiber){
  // todo
}

2.3 updateClassComponent方法

/**
 * 类组件处理
 * @param {*} fiber 
 */
function updateClassComponent(fiber){
  const {type, props} = fiber;
  const children = [new type(props).render()];
  reconcileChildren(fiber, children)
}

3、实现效果

image.png

4、本节代码

代码地址:github.com/linhexs/min…

十一、参考链接:

  1. pomb.us/build-your-…

  2. react.iamkasong.com/