环境
- 使用Vite创建一个React项目
- 修改vite.config.js
import { defineConfig } from "vite";
export default defineConfig({
plugins: [],
esbuild: {
jsxFactory: "Art.createElement",
jsxFragment: "Art.Fragment",
},
});
- 删掉src总所有文件,新建
main.jsx,Art.js
import Art from "./Art";
const element = <h1>Hello Art!</h1>;
function createElement() {}
const Art = {
createElement,
};
export default Art;
npm run dev, 打开浏览器,可以看到已经成功编译成指定的函数了
import Art from "/src/Art.js";
const element = /* @__PURE__ */
Art.createElement("h1", null, "Hello Art!");
V1 Basic
createElement
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) =>
typeof child !== "object" ? createTextElement(child) : child
),
},
};
}
// 统一结构,方便处理
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
}
使用上述的代码,编译后能得到下面的vdom
{
"type": "h1",
"props": {
"children": [
{
"type": "TEXT_ELEMENT",
"props": {
"nodeValue": "Hello Art!",
"children": []
}
}
]
}
}
render函数
在Art中新建render函数,作用是将vdom tree => DOM
function render(element, container) {
// 将vdom tree => DOM
// base case
const { type, props } = element;
const dom =
type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(type);
// 复制属性
const isProperty = (key) => key !== "children";
Object.keys(props)
.filter(isProperty)
.forEach((name) => {
dom[name] = props[name];
});
// make progrss, 递归渲染chidren
element.props.children.map((child) => render(child, dom));
// 添加到DOM中
container.appendChild(dom);
}
V2 Concurrent
V1版本的的问题是,当VDOM树很大时, 会阻塞主线程
- requestIdleCallback可以在浏览器空闲的时候指向回调函数,类似raf(react用这个)
- 我们需要把渲染任务分割成一个个更小的子任务,完成一个子任务后可以被中断, 然后继续下一个任务
let nextUnitOfWork = null
function workLoop(deadline){
let shouldYield = false
while (nextUnitOfWork&&!shouldYield){
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
// 否则会一直循环
shouldYield = deadline.timeRemaining() < 1
}
}
requestIdleCallback(workLoop)
performUnitOfWork(nextUnitOfWork)
- 疑问暂未解决
为什么要使用workLoop这种形式呢,而不是直接在原来的render函数中使用requestIdleCallback?
function render(element, container) {
// 将vdom tree => DOM
// base case
const { type, props } = element;
const dom =
type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(type);
// 复制属性
const isProperty = (key) => key !== "children";
Object.keys(props)
.filter(isProperty)
.forEach((name) => {
dom[name] = props[name];
});
// make progrss, 递归渲染
element.props.children.forEach((child) =>
requestIdleCallback(() => render(child, dom))
);
// 添加到DOM中
container.appendChild(dom);
}
Fiber tree
render
performUnitOfWork
- 创建该fiber对应的dom
- 创建该fiber对应elment的children的fiber
- 设置下一个nextUnitOfWork
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDOM(fiber);
}
// 添加到DOM
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom);
}
const elements = fiber.props.children;
let index = 0;
let previousSibling = null;
while (index < elements.length) {
const ele = elements[index];
const { type, props } = ele;
const newFiber = {
type,
props,
dom: null,
parent: fiber,
child: null,
sibling: null,
};
if (index === 0) {
fiber.child = newFiber;
} else {
previousSibling.sibling = newFiber;
}
previousSibling = newFiber;
index++;
}
/* 寻找下一个unit的过程和rendre递归的过程是一样的*/
// child
if (fiber.child) {
return fiber.child;
}
// sibling
if (fiber.sibling) {
return fiber.sibling;
}
// parent
let next = fiber.parent;
while (next) {
if (next.sibling) {
return next.sibling;
}
next = next.parent;
}
}
Render and Commit
刚刚我们所写的performUnitWork创建好dom, 会添加到DOM, 因为我们的workLoop是可以被浏览器中断的,因此可能看到不完整的UI。我们可以使用一个wipRoot记录fiber tree根节点,等所有的工作完全之后,一次性commit到DOM中
完整代码
//
let nextUnitOfWork = null;
let wipRoot = null;
function render(element, container) {
// 创建fiber tree根节点
wipRoot = {
type: null,
props: {
children: [element],
},
dom: container,
parent: null,
child: null,
sibling: null,
};
nextUnitOfWork = wipRoot;
}
function workLoop(deadline) {
// 一个workLoop可以完成多个
let shoudYield = false;
while (nextUnitOfWork && !shoudYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
// Idle < 1时,callback返回, 一个workLoop可以完成多个unitOf work。
// 如果没有这行,会一直循环直到所有任务完成,这样就相当于渲染整颗vdom了
shoudYield = deadline.timeRemaining() < 1;
}
// 此时rendering工作已经完成,进入commit阶段
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDOM(fiber);
}
const elements = fiber.props.children;
let index = 0;
let previousSibling = null;
while (index < elements.length) {
const ele = elements[index];
const { type, props } = ele;
const newFiber = {
type,
props,
dom: null,
parent: fiber,
child: null,
sibling: null,
};
if (index === 0) {
fiber.child = newFiber;
} else {
previousSibling.sibling = newFiber;
}
previousSibling = newFiber;
index++;
}
/* 寻找下一个unit的工作和rendre递归的过程是一样的*/
// child
if (fiber.child) {
return fiber.child;
}
// sibling
if (fiber.sibling) {
return fiber.sibling;
}
// parent
let next = fiber.parent;
while (next) {
if (next.sibling) {
return next.sibling;
}
next = next.parent;
}
}
function commitRoot() {
// 注意 这里是child
commitWork(wipRoot.child);
wipRoot = null;
}
function commitWork(fiber) {
// base case
if (!fiber) {
return;
}
const domParent = fiber.parent.dom;
domParent.appendChild(fiber.dom);
// make progress
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function createDOM(fiber) {
const { type, props } = fiber;
const dom =
type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(type);
// 复制属性
const isProperty = (key) => key !== "children";
Object.keys(props)
.filter(isProperty)
.forEach((name) => {
dom[name] = props[name];
});
return dom;
}
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) =>
typeof child !== "object" ? createTextElement(child) : child
),
},
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
}
const Art = {
createElement,
render,
};
export default Art;
V3 Reconciliation
主要完成更新操作,diff
新增currentRoot记录旧的fiber tree根节点,
// 下一个要处理的根节点
let nextUnitOfWork = null;
// working in progrss fiber tree/正在处理的fiber tree根节点
let wipRoot = null;
// 当前已经commit到DOM里面的的fiber tree根节点
let currentRoot = null;
将wipRoot commit之后,currentRoot = wipRoot
function commitRoot(){
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
重构performUnitOfWork, 将创建fiber的逻辑抽离出来。每个节点新增alternate属性,用于记录相应的旧fiber
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDOM(fiber);
}
// diff/reconcile
const elements = fiber.props.children;
reconcileChildren(fiber, elements);
/* 寻找下一个unit的工作和rendre递归的过程是一样的*/
// child
if (fiber.child) {
return fiber.child;
}
// sibling
if (fiber.sibling) {
return fiber.sibling;
}
// parent
let next = fiber.parent;
while (next) {
if (next.sibling) {
return next.sibling;
}
next = next.parent;
}
}
effectTag用于commit phase判断应该进行什么操作
function reconcileChildren(wipFiber, elements) {
// 用于判断是不是第一个孩子
let index = 0;
// 记录前面一个sibling
let previousSibling = null;
// 定位到第一个旧的child(因为是reconcileChildren)
let oldFiber = wipFiber.alternate?.child;
/**
* 需要比较elements和oldFibers, elements是新的, oldFibers是旧的。 一个遍历数组,一个遍历链表
* 相当于两个数组的比较
*/
while (index < elements.length || oldFiber) {
const element = elements[index];
let newFiber = null;
const sameType = element?.type === oldFiber?.type;
if (sameType) {
// 可以复用dom
newFiber = {
type: element.type,
props: element.props,
dom: oldFiber.dom,
alternate: oldFiber,
parent: wipFiber,
child: null,
sibling: null,
effectTag: "UPDATE",
};
}
if (!sameType && element) {
// 有新的element
newFiber = {
type: element.type,
props: element.props,
dom: null,
alternate: oldFiber,
parent: wipFiber,
child: null,
sibling: null,
effectTag: "PLACEMENT",
};
}
if (!sameType && oldFiber) {
// 删除旧的
// 因为我们正在新建的这颗wipRoot不会保存被删除的节点,
// 因此需要放在其它地方
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
// 切换到下一个sibling
oldFiber = oldFiber?.sibling;
if (index === 0) {
wipFiber.child = newFiber;
} else if (element) {
previousSibling.sibling = newFiber;
}
previousSibling = newFiber;
index++;
}
}
完整代码
// 下一个要处理的根节点
let nextUnitOfWork = null;
// working in progrss fiber tree/正在处理的fiber tree根节点
let wipRoot = null;
// 当前已经commit到DOM里面的的fiber tree根节点
let currentRoot = null;
// 记录需要删除的
let deletions = null;
function render(element, container) {
// 创建fiber tree根节点
wipRoot = {
type: null,
props: {
children: [element],
},
dom: container,
alternate: currentRoot,
parent: null,
child: null,
sibling: null,
};
// 初始化
deletions = [];
nextUnitOfWork = wipRoot;
}
requestIdleCallback(workLoop);
function workLoop(deadline) {
// 一个workLoop可以完成多个
let shoudYield = false;
while (nextUnitOfWork && !shoudYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
// Idle < 1时,callback返回, 一个workLoop可以完成多个unitOf work。
// 如果没有这行,会一直循环直到所有任务完成,这样就相当于渲染整颗vdom了
shoudYield = deadline.timeRemaining() < 1;
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
function reconcileChildren(wipFiber, elements) {
// 用于判断是不是第一个孩子
let index = 0;
// 记录前面一个sibling
let previousSibling = null;
// 定位到第一个旧的child(因为是reconcileChildren)
let oldFiber = wipFiber.alternate?.child;
/**
* 需要比较elements和oldFibers, elements是新的, oldFibers是旧的。
* 一个遍历数组,一个遍历链表
* 相当于两个数组的比较
*/
while (index < elements.length || oldFiber) {
const element = elements[index];
let newFiber = null;
const sameType = element?.type === oldFiber?.type;
if (sameType) {
// 可以复用dom
newFiber = {
type: element.type,
props: element.props,
dom: oldFiber.dom,
alternate: oldFiber,
parent: wipFiber,
child: null,
sibling: null,
effectTag: "UPDATE",
};
}
if (!sameType && element) {
// 有新的element
newFiber = {
type: element.type,
props: element.props,
dom: null,
alternate: oldFiber,
parent: wipFiber,
child: null,
sibling: null,
effectTag: "PLACEMENT",
};
}
if (!sameType && oldFiber) {
// 删除旧的
// 因为我们正在新建的这颗wipRoot不会保存被删除的节点,因此需要放在其它地方
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
// 切换到下一个sibling
oldFiber = oldFiber?.sibling;
if (index === 0) {
wipFiber.child = newFiber;
} else if (element) {
previousSibling.sibling = newFiber;
}
previousSibling = newFiber;
index++;
}
}
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDOM(fiber);
}
// diff/reconcile
const elements = fiber.props.children;
reconcileChildren(fiber, elements);
/* 寻找下一个unit的工作和rendre递归的过程是一样的*/
// child
if (fiber.child) {
return fiber.child;
}
// sibling
if (fiber.sibling) {
return fiber.sibling;
}
// parent
let next = fiber.parent;
while (next) {
if (next.sibling) {
return next.sibling;
}
next = next.parent;
}
}
function commitRoot() {
// 删掉旧的
deletions.forEach(commitWork);
// commit wipRoot
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
function commitWork(fiber) {
// base case
if (!fiber) {
return;
}
const domParent = fiber.parent.dom;
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
// TODO, 为什么这里要判断dom
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
// alternate属性用在这里
updateDOM(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom);
}
// make progress
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function updateDOM(dom, oldProps, newProps) {
const isEvent = (key) => key.startsWith("on");
const isProperty = (key) => key !== "children" && !isEvent(key);
const isNew = (key) => oldProps[key] !== newProps[key];
const isGoneProperty = (key) => !(key in newProps);
const isGoneEvent = (key) => isGoneProperty(key) || isNew(key);
// 删除旧的属性
Object.keys(oldProps)
.filter(isProperty)
.filter(isGoneProperty)
.forEach((key) => {
// delete
dom[key] = "";
});
// 移除旧的事件监听, 事件handle变化了就要移除,
// 和普通的属性稍微不一样
Object.keys(oldProps)
.filter(isEvent)
.filter(isGoneEvent)
.forEach((key) => {
const eventType = key.toLowerCase().substring(2);
dom.removeEventListener(eventType, oldProps[key]);
});
// 更新/新增的属性, 不变的保持不变
Object.keys(newProps)
.filter(isProperty)
.filter(isNew)
.forEach((key) => {
dom[key] = newProps[key];
});
// 注册事件监听
Object.keys(newProps)
.filter(isEvent)
.filter(isNew)
.forEach((key) => {
const eventType = key.toLowerCase().substring(2);
dom.addEventListener(eventType, newProps[key]);
});
}
function createDOM(fiber) {
const { type, props } = fiber;
const dom =
type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(type);
// 使用updateDOM
updateDOM(dom, {}, props);
return dom;
}
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) =>
typeof child !== "object" ? createTextElement(child) : child
),
},
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
};
}
const Art = {
createElement,
render,
};
export default Art;
V4 函数组件
函数组件有个特殊的地方是children. React几乎所有东西都是Props.
考虑一下2与7-8的区别,
- 2 只有运行App之后才能得到
- 7-8仅仅是element(组件App的
实例)的props中的children, 该elment的实际要渲染的children是运行App函数后得到的children = App(element.props)
function App(children){
return <h1>{children}</h1>
}
const element =
<App>
<h2></h2>
<h3></h3>
</App>
函数组件有对应的element(vdom), 但该element没有相应的dom, 因此遇到这种element时候,需要特殊处理。
function performUnitOfWork(fiber) {
// TODO 需要重构
if (!fiber.dom) {
fiber.dom = createDOM(fiber);
}
// ...
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDOM(fiber);
}
const elements = fiber.props.children;
reconcileChildren(fiber, elements);
}
function updateFunctionComponent(fiber) {
// 函数式组件, 需要运行之后才能得到children
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
function performUnitOfWork(fiber) {
if (typeof fiber.type === "string") {
updateHostComponent(fiber);
} else {
updateFunctionComponent(fiber);
}
/* 寻找下一个unit的工作和rendre递归的过程是一样的*/
// child
if (fiber.child) {
return fiber.child;
}
// sibling
if (fiber.sibling) {
return fiber.sibling;
}
// parent
let next = fiber.parent;
while (next) {
if (next.sibling) {
return next.sibling;
}
next = next.parent;
}
}
下一步需要处理的是commit, 因为函数组件没有dom, 需要特殊处处理。因此,当一个fiber要挂载(将dom挂到容器里面),需要找到其最近的具有dom的fiber节点。
同样的,如果需要删除一个组件fiber, 需要递归的移除所有子fiber
function commitRoot() {
// 删掉旧的
deletions.forEach(commitWork);
// commit wipRoot
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
function commitWork(fiber) {
// base case
if (!fiber) {
return;
}
const parentFiber = fiber.parent;
while (parentFiber && !parentFiber.dom) {
parentFiber = parentFiber.parent;
}
const domParent = parentFiber.dom;
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
// alternate属性用在这里
updateDOM(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent);
}
// make progress
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom);
} else {
commitDeletion(fiber.child, domParent);
}
}
perfect, main.jsx测试一下经典的Counter组件
import Art from "./Art";
const container = document.querySelector("#root");
let count = 0;
const inc = () => {
count += 1;
renderer();
};
function Counter() {
return (
<h1>
{count} <button onClick={() => inc()}>click</button>
</h1>
);
}
function renderer() {
const element = (
<div>
<Counter />
<Counter />
<Counter />
</div>
);
Art.render(element, container);
}
renderer();
V5 useState
V4版本的Counter没有state, 现在添加一个useState
wipFiber = null
hookIndex = null
function updateFunctionComponent(fiber) {
// 函数式组件, 需要运行之后才能得到children
wipFiber = fiber;
wipFiber.hooks = [];
hookIndex = 0;
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}
export function useState(initial) {
const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
// 判断是不是初次调用,如果不存在旧的状态
const currentHook = {
state: oldHook ? oldHook.state : initial,
// 将每次调用缓存到queue
queue: [],
};
// 转移到新的fiber
wipFiber.hooks.push(currentHook);
hookIndex++;
// 在调用uesState之前先执行上次的actions, 更新curr
const actions = oldHook ? oldHook.queue : [];
actions.forEach((action) => {
currentHook.state = action(currentHook.state);
});
const setState = (action) => {
currentHook.queue.push(action);
/* 触发更新,类似render函数 */
wipRoot = {
type: null,
props: currentRoot.props,
dom: currentRoot.dom,
alternate: currentRoot,
parent: null,
child: null,
sibling: null,
effectTag: "",
};
nextUnitOfWork = wipRoot;
deletions = [];
};
return [currentHook.state, setState];
}