mini版的React,掌握React的基本设计思想
一、总体实现目标
本文通过手写一个简易版 React,来了解 Fiber 架构到底做了些什么?从而对 React 基本原理有一个直观的认识;
学习建议:下载本节代码,对照着文章查看,尽量动手实现一遍;
实现的版本为16.8,基于pomb.us/build-your-…;
实现目标如下:
createElement;
render;
并发模式;
Fibers;
渲染和提交 ;
协调;
函数组件;
hooks;
类组件
二、createElement实现
学习建议:下载本节代码,对照着文章查看,尽量动手实现一遍。
1、前言
在React17之前,我们写React代码的时候都会去引入React,不引入代码就会报错,而且自己的代码中没有用到React,这是为什么呢?带着这个问题我们向下学习;
import React from 'react' 复制代码
2、element变量解析
我们先创建一个element变量,将本段代码放到babel上查看编译结果:
const element = <h1 title="foo">Hello</h1> 复制代码
通过babel会编译成下面这种形式:
经过编译后的代码为:
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 复制代码
创建项目目录:
配置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', }), ], } 复制代码
加入启动命令:
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) 复制代码
打印结果:
简化一下,将其他属性刨除(其他属性我们不关心):
const element = { type: "h1", props: { title: "foo", children: "Hello", }, } 复制代码
简单总结一下,React.createElement 实际上是生成了一个 element 对象,包含两个属性对象 type 和 props ,该对象拥有以下属性:
element对象参数:
type:标签名称
props:属性
title:标签属性
children:子属性
5、render简单流程
提前了解一下render的简单流程:
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 复制代码
打印结果:
7、本节代码
地址:gitee.com/linhexs/rea…
三、render实现
本节目标实现ReactDOM.render,只关心向 DOM 添加内容,之后处理更新和删除内容;
本节代码实现目录为:react/react-dom.js文件;
有了虚拟 DOM 数据结构,接下来要把它转换成真实的DOM结构渲染到页面上,基本分为四步:
创建不同类型节点
添加属性 props
遍历 children,递归调用 render
将生成的节点 append 到 root 根节点上
具体实现步骤:
1、新建react-dom.js文件
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文件中
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的节点:
修改webpack配置,添加html模板:
6.4 运行
运行命令 npm run start 启动,可以看到已经成功显示出结果:
7、总结
使用流程图简单总结一下2、3小节:
8、本节代码
代码地址:gitee.com/linhexs/rea…
四、并发模式&fiber导读
1、出现的问题
上节的递归调用会出现一个问题,也就是一旦开始渲染,就不能停止了,直到渲染出完整的树结构。也就是说会造成主线程被持续占⽤,造成的后果就是主线程上的布局、动画等周期性任务就⽆法立即得到处理,造成视觉上的卡顿,影响⽤户体验。
在浏览器中,页面是一帧一帧绘制出来的,主流浏览器刷新频率为60Hz,即每(1000ms / 60Hz)16.6ms浏览器刷新一次,在这一帧中浏览器要完成JS脚本执行、样式布局、样式绘制,如果在某个阶段执行时间很长,超过 16.6ms,那么就会阻塞页面的渲染,从而出现卡顿现象,也就是常说的掉帧!。
2、如何解决
使用增量渲染(把渲染任务拆分成块,匀到多帧),将把工作分解成小单元,在完成每个单元之后,如果还有其他任务需要完成,我们将让浏览器中断渲染,也就是经常听到的fiber。
关键点:
增量渲染
更新时能够暂停,终止,复⽤渲染任务
给不同类型的更新赋予优先级
3、什么是fiber
用一张非常经典的图:
简单说一下:fiber是指组件上将要完成或者已经完成的任务,每个组件可以⼀个或者多个。
4、window.requestIdleCallback()
实现fiber的核心是window.requestIdleCallback(),window.requestIdleCallback()⽅法将在浏览器的空闲时段内调⽤的函数队列。关于window.requestIdleCallback()请点击查看文档。
你可以把 requestedlecallback 想象成一个 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、本节代码
代码地址:gitee.com/linhexs/rea…
五、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; 复制代码
2、渲染过程
在渲染中,我们将创建根fiber并将其设置为 nextUnitOfWork。剩下的工作将在 perforformunitofwork 功能上进行,每个fiber做了三件事:
将元素添加到 DOM
为元素的子元素创建fiber
选择下一个工作单元
实现目的是为了查找下一个工作单元变得更容易,使用的深度优先遍历,先查找子节点,在查找兄弟节点。
上图渲染过程详细描述如下:
当完成root的fiber工作时,如果有孩子,那么fiber是下一个工作的单元,root的子节点是div
当完成div的fiber工作时,下一个工作单元是h1
h1的节点是p,继续下一个工作单元p
p没有子节点,去找兄弟节点a
a兄弟节点和子节点都没有,返回到父亲节点h1
h1的子节点都已经工作完成了,去找h1的兄弟节点h2
h2既没有兄弟节点,也没有子节点,返回到父亲节点div
同上,div在返回到父亲节点root
至此已经完成了所有的渲染工作
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文件
实现效果:
5、本节代码
代码地址:gitee.com/linhexs/rea…
六、渲染和提交
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、运行结果
运行结果没有问题
4、本节代码
代码地址:gitee.com/linhexs/rea…
七、协调
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") 复制代码
运行:
4、本节代码
代码地址:gitee.com/linhexs/rea…
八、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、实现效果
4、本节代码
代码地址:gitee.com/linhexs/rea…
九、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函数结果导出;
运行结果:
4、本节代码
代码地址:gitee.com/linhexs/rea…
十、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、实现效果
作者:lin嘟嘟嘟
链接:https://juejin.cn/post/7023319961883901988