先来看看一个简单的React页面渲染
import React from "react";
import ReactDOM from "react-dom";
const element = (
<div id="contentWrap">
<a> a simple React Application </a>
<p> React version -- 16.8 </p>
</div>
);
const container = document.getElementById("root");
ReactDOM.render(element, container);
一个React应用简单概括可以分为三步:
- 创建JSX元素
- 获取应用挂载的根节点
- 将元素渲染到节点上--(ReactDom.render)
所以实现一个简易版本的React,看起来主要要实现如何将JSX转译
和ReactDOM.render
这两个方法
React元素是被babel用React.createElement()方法进行一层转译,会被解析为如下代码:
"use strict";
const element = /*#__PURE__*/ React.createElement(
"div",
{
id: "contentWrap",
},
/*#__PURE__*/ React.createElement("a", null, "a simple React Application"),
/*#__PURE__*/ React.createElement("p", null, " React version -- 16.8 ")
);
React.createElement()会返回一个虚拟Dom结构,大致如下
const element = {
type: 'div',
props: {
id: 'contentWrap',
children: [
{
type: 'a',
props: {
children: ['a simple React Application']
}
},
{
type: 'p',
props: {
children: [' React version -- 16.8 ']
}
},
]
}
}
实现自己的React.createElement()版本
type MyReactElement = {
type: string | Function;
props: {
children: MyReactElement[];
[props: string]: any
}
}
/**
*
* @param type 元素类型
* @param props 元素属性
* @param children 子级元素
* @returns MyReactElement
*/
function createElement(
type: string | Function,
props: Record<string, any>,
...children: MyReactElement[]
): MyReactElement {
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: [],
},
};
}
实现一个render
render函数就是根据虚拟dom来生成真实的Dom,并将其挂载在根节点上。
function render(element: MyReactElement, container: HTMLElement ) {
// 根据类型创建对应的DOM
const dom =
element.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement((element.type as string));
// 追加属性
Object.keys(element.props)
.filter(key => key !== "children")
.forEach(name => {
(dom as HTMLElement)[name] = element.props[name]
})
// 递归,将每一个孩子节点渲染出来
element.props.children.forEach(child =>
render(child, dom as HTMLElement)
);
// 挂载
container.append(dom)
};
查看效果
import MyReact from "./MyReact2";
/** @jsxRuntime classic */
/** @jsx MyReact.createElement */
const element = (
<div>
<h1 title="this is a simple react">
MyReact
<div>It is important for me</div>
</h1>
</div>
)
const root = document.getElementById('root');
MyReact.render(element, root);
运行之后就能看到
至此 一个最简单的React版本实现了(也就仅仅实现了JSX->DOM的转换),但是React16之后的核心Fiber架构却完全没涉及。
并且这个最简单版本的React一旦开始render,直到渲染完成整个DOM之前,是没有办法暂停的,而浏览器中是每16ms进行一次绘制,如果render函数执行时间过长,超过16ms就会造成卡顿掉帧的现象。(此处用window.requestIdleCallback()模拟解决)
React16之后的Fiber架构就是为了解决主线占用时间过长的问题
而Fiber架构的处理办法
- 将整个虚拟DOM树分成一个个节点即工作单元来执行
- 渲染的过程可以中断,可以将控制权交回浏览器,让浏览器及时的响应用户
关于Fiber的一些信息
Fiber应该是React16版本之后的核心了,它既是一种数据结构,也是一个工作单元
Fiber作为数据结构
Fiber结构是一个树形结构,树中的每一个节点都是Fiber,每个节点的child指针只指向自己的第一个孩子,sibling指向自己的第一个兄弟节点,parent指向自己的父节点
对此, Fiber架构的组成如下
- Scheduler(调度器)-- 找出高优先级的任务,高优先级先进入Reconciler
- Reconciler(协调器)-- 负责找出变化的组件,可以被中断
- Renderer -- 负责将变化的组件渲染到页面上,不被中断
初步改造--单元化render
将原先的递归式render拆分成单个Fiber单元式的render,这样每个Fiber执行完之后,如果主线程有其他高优先级操作需要执行时,可以中断,那么这就需要一个变量记住当前的Fiber,以便中断后继续渲染
type Fiber = {
type?: string | Function;
props: {
children: MyReactElement[];
[props: string]: any
}
}
type Deadline = {
timeRemaining: () => number; // 当前剩余的可用时间,即该帧剩余时间
didTimeout: boolean; // 是否超时
}
let nextUnitOfWork: Fiber | null = null;
function workLoop(deadline: Deadline) {
let shouldYield = false;
// 每次执行完一个工作单元,则判断是否需要中断
// 当没有工作单元或到了中断时间 则跳出循环
while(nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop);
function performUnitOfWork() {
// code
}
在render中只执行构建根节点的Fiber,然后将它赋值给nextUnitOfWork,剩余的操作将在workLoop函数中执行(该函数在初始化的时候就被requestIdleCallback函数注册执行了), workLoop函数从最初的根节点开始,以工作单元的方式构建整个Dom树。
performUnitOfWork函数主要是将element转换成Fiber结构,并返回下一个Fiber
主要做三件事
- 创建node并添加到DOM中
- 为props中的children,建立Fiber
- 选择下一个Fiber
确定Fiber的顺序
- 若有children,则返回第一个children
- 若无children,则返回sibling(即第一个兄弟节点)
- children和sibiling都无的情况下,则返回父节点的sibling,若父节点也无sibling,则继续往上寻找,直到祖先节点的sibling或根节点(全部转换完毕)
type Fiber = {
type?: string | Function;
props: {
children: MyReactElement[];
[props: string]: any
},
dom: HTMLElement | Text | null;
parent?: Fiber;
child?: Fiber;
sibling?: Fiber;
}
function performUnitOfWork(fiber: Fiber) {
//1. 创建node
if (!fiber.dom) fiber.dom = createDom(fiber)
//
if (fiber.parent) fiber.parent.dom?.appendChild(fiber.dom)
// 为children创建fiber
const elements = fiber.props.children
let index = 0,
prevSibling: Fiber | null = null;
while( index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
// 构建child指针
if (index === 0) {
fiber.child = newFiber
} else {
// 构建sibling指针 上一个children Fiber 的sibling指向当前children Fiber
prevSibling && ((prevSibling as Fiber).sibling = newFiber)
}
prevSibling = newFiber;
index++;
}
// 按照 child -> sibling -> uncle的顺序 寻找下一个进入工作单元的fiber
if (fiber.child) return fiber.child
let nextFiber: Fiber | undefined = fiber
while(nextFiber) {
if (nextFiber.sibling) return nextFiber.sibling
// 往上找父辈的兄弟节点
nextFiber = nextFiber.parent
}
}
function createDom(fiber: Fiber) {
const dom = fiber.type === 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement((fiber.type) as string);
const isProperty = (key: string ) => key !== 'children';
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})
return dom;
}
截止目前的版本,在workLoop中每个工作单元结束后,浏览器都主线程都可以打断渲染,这样很可能让用户看到不完整的ui。
为了解决这个问题,需要一个全局变量wipRoot记录当前构造的Fiber树的根节点。 所以不能生成一个node,就将其添加到dom中,要当整个workLoop中已经没有新的工作单元了,才将生成的节点添加到dom中,因此要对performUnitOfWork和render函数进行改造
// 用wipRoot存储当前树的Root, 当workLoop中探测到整棵树都渲染完成(没有下一个work unit)则从root fiber开始便历,添加每个fiber内的dom到DOM树中
let wipRoot: Fiber | undefined = undefined;
function workLoop(deadline: Deadline) {
let shouldYield = false;
// 每次执行完一个工作单元,则判断是否需要中断
// 当没有工作单元或到了中断时间 则跳出循环
while(nextUnitOfWork && !shouldYield) {
// 构建Fiber
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
// 所有树都渲染完 则commit 进入Render阶段
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
function commitRoot() {
const childs = (wipRoot as Fiber).child
childs && commitWork(childs)
wipRoot = undefined;
}
function commitWork(fiber: Fiber | undefined) {
if (!fiber) return
const domParent = fiber.parent?.dom
domParent?.appendChild((fiber.dom) as HTMLElement | Text)
commitWork(fiber.child);
commitWork(fiber.sibling)
}
function render(element: MyReactElement, container: HTMLElement ) {
// 保持构建的root节点
wipRoot = {
dom: container,
props: {
children: [element]
}
}
nextUnitOfWork = wipRoot
};
function performUnitOfWork(fiber: Fiber) {
//1. 创建node
if (!fiber.dom) fiber.dom = createDom(fiber)
// 不需要在构建Fiber的时候 添加dom
// if (fiber.parent) fiber.parent.dom?.appendChild(fiber.dom)
// 为children创建fiber
const elements = fiber.props.children
let index = 0,
prevSibling: Fiber | null = null;
while( index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
// 构建child指针
if (index === 0) {
fiber.child = newFiber
} else {
// 构建sibling指针 上一个children Fiber 的sibling指向当前children Fiber
prevSibling && ((prevSibling as Fiber).sibling = newFiber)
}
prevSibling = newFiber;
index++;
}
// 按照 child -> sibling -> uncle的顺序 寻找下一个进入工作单元的fiber
if (fiber.child) return fiber.child
let nextFiber: Fiber | undefined = fiber
while(nextFiber) {
if (nextFiber.sibling) return nextFiber.sibling
// 往上找父辈的兄弟节点
nextFiber = nextFiber.parent
}
}
至此实现了dom的新增,但是没实现dom的删除和更新
删除和更新节点
这两个操作需要将当前commit的Fiber树和上一次渲染的Fiber树进行对比,所以需要两颗Fiber树,一颗是Current Fiber
(对应当前渲染的Fiber tree),一颗是WorkInProgress Fiber
(对应构建中的Fiber tree),两棵树中的对应节点通过Fiber上的alternate指针连接,以便比较每一个节点的变化(是新增还是删除还是修改),这样也提高了渲染效率
新旧节点的对比规则如下: 1.如果旧的 fiber 元素 和 新元素具有相同的类型,那么属于更新,复用旧fiber的dom,只更新属性 2.如果类型不同,且有一个新元素,则需要创建dom 3.类型不同,且有一个旧fiber,则移除旧的节点
type Fiber = {
type?: string | Function;
props: {
children: MyReactElement[];
[props: string]: any
},
dom: HTMLElement | Text | undefined;
parent?: Fiber;
child?: Fiber;
sibling?: Fiber;
alternate: Fiber | undefined;
effectTag?: 'UPDATE' | 'PLACEMENT' | 'DELETION'
}
function performUnitOfWork(fiber: Fiber) {
//1. 创建node
if (!fiber.dom) fiber.dom = createDom(fiber)
// 不需要在构建Fiber的时候 添加dom
// if (fiber.parent) fiber.parent.dom?.appendChild(fiber.dom)
// 为当前fiber的children elements 对比 old fiber执行新增、删除、更新的操作
const elements = fiber.props.children
reconcileChildren(fiber, elements)
// 按照 child -> sibling -> uncle的顺序 寻找下一个进入工作单元的fiber
if (fiber.child) return fiber.child
let nextFiber: Fiber | undefined = fiber
while(nextFiber) {
if (nextFiber.sibling) return nextFiber.sibling
// 往上找父辈的兄弟节点
nextFiber = nextFiber.parent
}
}
function reconcileChildren(wipFiber: Fiber, elements: MyReactElement[]) {
// elements是当前想要渲染到dom中的, oldFiber是上一次渲染到dom中的
let index = 0,
oldFiber: Fiber | undefined = wipFiber.alternate && wipFiber.alternate.child,
prevSibling: Fiber | undefined = undefined;
while(index < elements.length || oldFiber !== undefined) {
const element = elements[index],
sameType = oldFiber && element && element.type === oldFiber.type;
let newFiber: Fiber | undefined = undefined;
if (sameType) {
// 相同类型 则复用dom,只更新props
newFiber = {
type: oldFiber?.type,
props: element.props,
dom: oldFiber?.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: 'UPDATE'
}
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: undefined,
parent: wipFiber,
alternate: undefined,
effectTag: 'PLACEMENT'
}
}
// 删除后新的树中就不能有该Fiber节点,所以只能在老树中的Fiber节点存储标记
if (oldFiber && !sameType) {
oldFiber.effectTag = 'DELETION';
deletions?.push(oldFiber);
}
if (index === 0 ) {
wipFiber.child = newFiber
} else if (element) {
(prevSibling as Fiber).sibling = newFiber
}
prevSibling = newFiber;
if (oldFiber) oldFiber = oldFiber.sibling;
index++
}
}
function commitRoot() {
// 每次提交前删除不需要的Fiber
deletions?.forEach(commitWork)
const childs = (wipRoot as Fiber).child
childs && commitWork(childs)
currentRoot = wipRoot; // 记录上一次commit的Fiber树
wipRoot = undefined;
}
// 根据不同的effectTag进行不同的操作
function commitWork(fiber: Fiber | undefined) {
if (!fiber) return
const domParent = fiber.parent?.dom,
effectTag = fiber.effectTag;
if (effectTag === 'PLACEMENT' && fiber.dom !== undefined) {
domParent?.appendChild(fiber.dom)
} else if (effectTag === 'UPDATE' && fiber.dom !== undefined) {
updateDom(
fiber.dom,
fiber.alternate?.props,
fiber.props
)
} else if (effectTag === 'DELETION') {
domParent?.removeChild((fiber.dom as HTMLElement | Text))
}
commitWork(fiber.child);
commitWork(fiber.sibling)
}
更新节点的操作则需要一些判断,如果新props中没有一些属性则删除,若有则更新,并且React中的事件是以on开头
const isEvent = (key: string) => key.startsWith("on");
const isProperty = (key: string) => key !== "children" && !isEvent(key);
const isNew = (prev: Record<string, any>, next: Record<string, any>) => (key: string) => prev[key] !== next[key];
const isGone = (prev: Record<string, any>, next: Record<string, any>) => (key: string) => !(key in next);
function updateDom(dom: HTMLElement, prevProps: Record<string, any>, nextProps: Record<string, any>) {
// 删除或改变事件
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]);
});
// 删除旧的属性
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];
});
// 新增事件
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
}
函数式组件
函数式组件有两个特点
- 生成的Fiber没有dom
- 函数式组件的childern是通过运行函数得到的
所以需要在performUnitOfWork函数中判断是否是函数式组件,因为函数式组件没有dom,所以commitWork中也需要进行对应的改变,需要在fiber树上找到一个含有dom的节点,然后将函数式组件中children的dom同样增添加进去。
// 根据不同的effectTag进行不同的操作
function commitWork(fiber: Fiber | undefined) {
if (!fiber) return
let domParentFiber = fiber.parent;
// 函数组件没有dom(即 函数式组件 在从jsx -> element这一步构建的时候,其最外层结构中对应的dom为null,type是该函数本身),所以需要沿fiber tree向上寻找一个有dom的fiber,将函数式组件children的dom添加进去。
while(!domParentFiber?.dom) {
domParentFiber = domParentFiber?.parent
}
const domParent = domParentFiber.dom,
effectTag = fiber.effectTag;
if (effectTag === 'PLACEMENT' && fiber.dom !== undefined) {
domParent?.appendChild(fiber.dom)
} else if (effectTag === 'UPDATE' && fiber.dom !== undefined) {
updateDom(
(fiber.dom as HTMLElement),
(fiber.alternate?.props as Record<string, any>),
fiber.props
)
} else if (effectTag === 'DELETION') {
commitDeletion(fiber, (domParent as HTMLElement | Text
))
}
commitWork(fiber.child);
commitWork(fiber.sibling)
}
hooks
需要添加一个全局hooks array实现一个component中多次使用useState 在react在每次setState会重新渲染,这是因为会设置一个新的wipRoot作为下一次的工作单元
function useState<T>(initial: T) {
const oldHook =
wipFiber?.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
const hook: selfHook = {
state: oldHook ? oldHook.state : initial,
queue: []
}
const actions = oldHook ? oldHook.queue : []
actions.forEach((action: any) => {
hook.state = action(hook.state)
})
// 执行此函数后,会触发重新构建Fiber树
const setState = (action: (value: T) => T) => {
// 上一步中执行的就是action方法,此处会将其推入queue
hook.queue.push(action);
// 执行更新,做的工作和render方法相似,因此setState是update的入口
currentRoot &&
(wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
})
nextUnitOfWork = wipRoot;
deletions = [];
};
// 执行后将该hook推入新树的hooks数组
wipFiber?.hooks?.push(hook);
// 为处理下一个hook作准备
hookIndex++;
// 注意此时返回了一个函数setState
// setState用到了函数中的局部变量hook,因此形成了一个闭包
return [hook.state, setState];
}
总结
至此也算基本完成了对Own React代码的学习,虽然这个版本的React是一个简化版,但也好比自己啥都不清楚就一脑子钻进去看React源码,算是一个个浅浅的入门。