基于这个网站实现的一个简易版react
https://pomb.us/build-your-own-react/
学完之后大概能懂得react的一个工作原理。 附代码如下:
const TEXT_ELEMENT = "TEXT_ELEMENT";
// 第一步 完成creaateElement
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) => {
// 将普通元素比如字符串数字等封装成对象
return typeof child === "object" ? child : createTextElement(child);
}),
},
};
}
function createTextElement(text) {
return {
type: TEXT_ELEMENT,
props: {
nodeValue: text,
children: [],
},
};
}
// 我们将旧 Fiber 的 props 与新 Fiber 的 props 进行比较,移除掉掉的 props,设置新的或更改的 props。
const isProperty = (key) => key !== "children";
// 判断两者是否有变化,有变化才返回true
const isNew = (prev, next) => {
return (key) => {
return prev[key] !== next[key];
};
};
// 判断key在不在新的props中,不在才返回true
const isGone = (prev, next) => {
return (key) => {
return !(key in next);
};
};
// 处理事件
const isEvent = (key) => key.startsWith("on");
function updateDom(dom, prevProps, nextProps) {
// 如若需要, 删除老的事件处理程序
Object.keys(prevProps)
.filter(isEvent)
.filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key)) //key不在新props或者是两个事件处理函数变化了
.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创建节点
function createDom(fiber) {
const dom =
fiber.type === TEXT_ELEMENT
? document.createTextNode("")
: document.createElement(fiber.type);
// 处理props,props.nodeValue会覆盖值
updateDom(dom, {}, fiber.props);
return dom;
}
// render创建一个root fiber,其余工作交给performUnitOfWork 给nextUnitOfWork赋值,准备开始工作
function render(element, container) {
wipRoot = {
// 第一个nextUnitOfWork,也可以算是root fiber
dom: container,
props: {
children: [element], // 他的子节点就是render的react元素,如<div></div>
},
parent: null,
child: null,
sibling: null,
alternate: currentRoot, //添加root fiber的alternamte
};
deletions = [];
nextUnitOfWork = wipRoot;
}
let nextUnitOfWork = null; // 下一个工作单元
let currentRoot = null; // 指向当前的节点工作的fiber tree
let wipRoot = null; // 指向当前的节点工作的root fiber
let deletions = null;
// 开始调度
function workLoop(deadline) {
let shouldYield = false;
//当前是否应该渲染
// 当还有需要工作的节点和不需要渲染的时候
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1; //
}
// 一旦完成了所有的工作,那么就将整颗fiber树交给dom
if (!nextUnitOfWork && wipRoot) {
// commit阶段无法中断的
commitRoot();
}
requestIdleCallback(workLoop); //浏览器会在主线程空闲的时候执行该回调。,react没有使用这个api,但这个案例,概念上是相通的。
// requestIdleCallback执行的时候,传入了一个截止日期参数deadline。我们可以使用它来检查在浏览器需要再次控制之前我们还有多少时间。
}
requestIdleCallback(workLoop);
//1 将元素添加到 DOM
//2 为元素的子元素创建fiber
//3 选择下一个工作单元
function performUnitOfWork(fiber) {
// type可能是类或者函数或者字符串
const isFunctionComponent = fiber.type instanceof Function;
if (isFunctionComponent) {
// 处理组件的时候为fiber创建dom和为节点创建fiber。
updateFunctionComponent(fiber); //处理函数组件
} else {
updateHostComponent(fiber); //处理原生组件
}
// 3 寻找下一个工作单元,遵循原则,大儿子,没大儿子就算完成,到下一个兄弟节点,没有兄弟节点的话,父级节点就算完成,到父级节点的兄弟节点,叔叔节点。
if (fiber.child) {
return fiber.child;
}
// 没有child,自己算完成了,到兄弟节点。
let currentFiber = fiber;
while (currentFiber) {
const currentFiberSibling = currentFiber.sibling;
if (currentFiberSibling) {
//有兄弟节点
return currentFiberSibling;
}
// 没有兄弟节点,父亲节点就算完成了,就要从父亲开始找叔叔节点了,如果没有叔叔节点,爷爷节点就算完成了,就要从爷爷开始找老叔叔节点了。依此类推。
// 直到找到root fiber,他没有parent。
currentFiber = currentFiber.parent;
}
}
// 处理原生
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
//需要为每个孩子创建一个新的fiber,并且将其加入fiber树
const elements = fiber.props.children;
reconcileChildren(fiber, elements); // 协调fiber和elements
}
// 在调用函数组件之前需要初始化一些全局变量,方便在useState中使用。
let wipFiber = null; //当前的函数组件
let hookIndex = null; // hooks索引
// 处理函数组件
function updateFunctionComponent(fiber) {
wipFiber = fiber;
hookIndex = 0;
wipFiber.hooks = []; // 支持useState在同一个组件中多次引用,并且通过hookIndex跟踪当前的钩子索引。
// 函数组件可以直接执行,获取return的vdom
const children = [fiber.type(fiber.props)]; // App({name: 'xxx'})
// 给返回的chilren创建fiber,调度children
reconcileChildren(fiber, children);
}
// 函数组件调用useState的时候,检查是否有旧的钩子,用alternate使用hookIndex来检查fiber
function useState(initial) {
// 是否第一次执行函数组件,如果是更新,那么可以通过wipFiber拿到老的状态,否则就用初始化的状态
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
const hook = { state: oldHook ? oldHook.state : initial, queue: [] }; //每次的状态
// 如果是更新阶段,那么第二次执行该工作单元的时候,就从存储的queue中取出函数执行,返回最新的状态给函数组件。
const actions = oldHook ? oldHook.queue : [];
actions.forEach((action) => {
hook.state = action(hook.state);
}); // 多个setState()的值是一样的,因为他们接受的参数都是同一个值。
const setState = (action) => {
hook.queue.push(action);
// 新建一个root fiber节点,然后赋值给nextUnitOfWork,将一个新的正在进行的工作根设置为下一个工作单元,工作循环就可以开始一个新的渲染阶段。从头开始
wipRoot = {
dom: currentRoot.dom, //当前的fiber树
props: currentRoot.props,
alternate: currentRoot,
parent: null,
child: null,
sibling: null,
};
nextUnitOfWork = wipRoot;
deletions = [];
};
wipFiber.hooks.push(hook);
hookIndex++; //指向最新的状态
return [hook.state, setState];
}
// 调度子节点
// effecttage的类型
const UPDATE = "UPDATE"; // 更改
const PLACEMENT = "PLACEMENT"; //新增加
const DELETION = "DELETION"; //删除
// 在这里,我们将调和新fiber上一个节点旧fiber的子节点与新fiber的子节点。
// 迭代旧 Fiber ( wipFiber.alternate) 的子节点和我们想要协调的元素数组
function reconcileChildren(wipFiber, elements = []) {
let index = 0;
console.log("elements", elements);
// oldFiber和element。这element是我们想要渲染到 DOM 的东西,oldFiber也是我们上次渲染的东西。
let oldFiber = wipFiber.alternate && wipFiber.alternate.child; //老的fiber的大儿子节点, elements是新的fiber的子节点。
let prevSibling = null; //指向上一个子节点,如果不是大儿子,他们新创建的子fiber,要作为上一个子fiber的兄弟节点,才能被加入fiber树。
while (index < elements.length || oldFiber) {
const element = elements[index]; // 当前的子节点
let newFiber = null;
// 比对element和oldFiber
// react这里使用了key,可以更好的reconciler,比如检测元素数组中某个元素位置的改动。
const sameType = oldFiber && element && element.type === oldFiber.type;
if (sameType) {
// 1 旧的fiber的child和当前fiber的大儿子节点一样类型 我们可以保留 DOM 节点并使用新的 props 更新它
// 创建一个新的的fiber,保留旧fiber的dom和元素的props,并且为fiber添加一个新的属性,effectTag,这个在commit阶段会使用到。
newFiber = {
type: oldFiber.type,
props: element.props, // 更新props
dom: oldFiber.dom, //保留dom
parent: wipFiber, //父节点指向新的fiber。
alternate: oldFiber, //在这里给新的fiber加上alternate属性
effectTag: UPDATE,
child: null,
sibling: null,
};
}
if (element && !sameType) {
// 2 类型不同并且有一个新元素,则意味着我们需要创建一个新的 DOM 节点 ,有可能是第一次渲染
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null, //新创建的fiber没有旧节点
effectTag: PLACEMENT,
child: null,
sibling: null,
};
}
if (oldFiber && !sameType) {
// 类型不同并且有旧fiber,就得删除。,这里不需要新的fiber,需要的话也在第二个判断创建完毕了,所以将删除标签添加到旧节点。
oldFiber.effectTag = DELETION;
deletions.push(oldFiber); // 所以需要一个数组来跟踪删除的节点
}
// oldFiber要指向兄弟节点了,接着是兄弟节点的比较
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
// 将新的fiber加入到fiber树中呢,将其设置为当前节点的子节点或者兄弟节点,主要取决于他是否是第一子节点。
if (index === 0) {
wipFiber.child = newFiber;
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
// 这个阶段一旦开始不能中断
function commitRoot() {
deletions.forEach(commitWork); // 删除的节点需要先完成
// 递归调用commitWork,无法中断
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
function commitWork(fiber) {
if (!fiber) {
return;
}
// render阶段完毕,到了commit阶段。
// 功能组件如函数组件。类组件的fiber没有dom
let domParentFiber = fiber.parent;
while (!domParentFiber.dom) {
// 父fiber有dom就退出。
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.dom;
// 根据不同的effecttag来做不同的处理
if (fiber.effectTag === PLACEMENT && fiber.dom !== null) {
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === DELETION) {
// 删除,如果删除的是一个功能组件,那么要往儿子查找,因为功能组件没有dom
if (fiber.dom) {
domParent.removeChild(fiber.dom);
} else {
// 功能组件
commitDeletion(fiber, domParent);
}
} else if (fiber.effectTag === UPDATE && fiber.dom !== null) {
//更新dom的props
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}
// 递归地将所有节点附加到 dom。
commitWork(fiber.child);
commitWork(fiber.sibling);
}
// 递归往下查找具有dom的fiber
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom);
} else {
commitDeletion(fiber.child, domParent);
}
}
const React = {
createElement,
useState,
};
const ReactDOM = {
render,
};
/** @jsx Didact.createElement */
function Counter() {
const [state, setState] = React.useState(1);
return React.createElement(
"h1",
{ onClick: () => setState((c) => c + 1) },
"Count:",
state
);
// return (
// <h1 onClick={() => setState(c => c + 1)}>
// Count: {state}
// </h1>
// )
}
const element = React.createElement(Counter, null);
const container = document.getElementById("root");
ReactDOM.render(element, container);
总结:
-
每个vdom对应一个fiber -
render的时候会创建一个root fiber,其余的工作由performUnitOfWork完成, -
performUnitOfWork主要完成三件事情
- 1 为当前的fiber节点创建dom
- 2 为每一个子节点创建fiber。reconcilerChildren,并且将fiber组成树。
- 3 选择下一个工作单元(顺序是儿子=》兄弟=〉叔叔节点)
-
react15的时候,更新是递归更新,无法中断, -
react16主要实现了可中断的异步更新,工作架构采用了fiber架构,主要分为两个阶段,render阶段和commit阶段,render阶段,只有当浏览器有空余的时间,才会执行workloop函数去进行调度,当全部的fiber调度完毕之后,才会到达commit阶段。commitRoot方法是commit阶段工作的起点,commit阶段一旦开始无法中断,根据fiber的effectTag和effectList来进行更新。可以简单的理解为render阶段先找出所有的变化,然后commit阶段统一更新,找变化可以中断,更新无法中断。 -
useState的状态挂在了fiber上面,每次更新的时候可以通过alternate属性获取老的状态,还有actions,然后遍历执行actions,传入老的状态,去更新新的状态,由于遍历执行的acitons的入参都是老状态,所以多个setState的值还是一样的。如setState(c=>c+1);setState(c=>c+1);这个c永远是1,所以调用了两个也算是2。
以上仅是个人学习所得笔记,如有错误欢迎在评论区提出。
|