实现自己的mini-react
- 创建运行环境
- 实现最简单mini-react
- 渲染dom
- 封装创建虚拟dom节点
- 封装函数
- 封装render函数
- 对齐react 调用方式
- 使用 jsx
- 任务调度器&fiber架构
- 封装一个workLoop方法
- 统一提交&实现 function component
- 统一提交
- 实现支持 function component
- 进军 vdom 的更新
- 实现绑定事件
- 更新props
- 击杀 update children
- 搞定 useState
- 搞定 useEffect
创建运行环境
pnpm create vite
- 选择Vanilla创建项目 选择javascript就行
- 删除多余文件 保留最简单目录
实现最简单mini-react
渲染dom
- index.html
<div id="root"></div><script type="module" src="./main.js"></script>
- main.js代码
const dom = document.createElement("div");dom.id="app"document.querySelector("#root").appendChild(dom);const text = document.createTextNode("");text.nodeValue = "hello mini react";dom.append(text)
这样就可以在浏览器上看到hello mini react了
封装创建虚拟dom节点
- 首先抽离节点
const textNode = {type: "TEXT_ELEMENT",props: {nodeValue: "hello mini react",children: []}
}
const el = {type: "div",props: {id: "app",children: [textNode]}
}
- 渲染dom
const dom = document.createElement(el.type);dom.id=el.props.iddocument.querySelector("#root").appendChild(dom);const text = document.createTextNode("");text.nodeValue = textNode.props.nodeValue;dom.append(text)
可以看到结果是一样的
封装函数
- 把上面的el和textNode封装一下方便调用
function createTextNode(nodeValue) {return {type: "TEXT_ELEMENT",props: {nodeValue,children: [],},};
}
function createElement(type, props, ...children) {return {type,props: {...props,children,},};
}
- 渲染dom
const textNode=createTextNode("hello mini react");const el = createElement("div",{id:"app"},textNode)const dom = document.createElement(el.type);dom.id=el.props.iddocument.querySelector("#root").appendChild(dom);const text = document.createTextNode("");text.nodeValue = textNode.props.nodeValue;dom.append(text)
可以看到结果是一样的
封装render函数
- 创建一个DOM节点,根据el的类型来决定是创建一个文本节点还是一个元素节点
- 遍历el的props属性,将除了children之外的属性都赋值给dom节点
- 获取el的children属性 遍历children,对每个子元素调用render函数进行递归渲染
- 把子节点添加到父节点中
function render(el, container) {// 创建一个DOM节点,根据el的类型来决定是创建一个文本节点还是一个元素节点const dom =el.type === "TEXT_ELEMENT"? document.createTextNode(""): document.createElement(el.type);// 遍历el的props属性,将除了children之外的属性都赋值给dom节点Object.keys(el.props).forEach(key => {if (key !== "children") {dom[key] = el.props[key];}})// 获取el的children属性const children = el.props.children// 遍历children,对每个子元素调用render函数进行递归渲染children.forEach(child => {render(child, dom)})// 将dom添加到container中container.append(dom);
}
- 重构createElement函数 之前我们传递节点是createTextNode(“hello mini react”) 现在想之间写"hello mini react" 需要修改函数
function createElement(type, props, ...children) {return {type,props: {...props,children: children.map(child => {return typeof child === "string" ? createTextNode(child) : child}),},};
}
- 渲染dom
const el = createElement("div",{id:"app"},"hello mini react")render(el,document.querySelector("#root"))
对齐react 调用方式
- 创建core 文件夹 里面包含React.js和ReactDOM.js两个文件
- 创建src文件夹 里面包含App.js文件
- React.js
function createTextNode(nodeValue) {return {type: "TEXT_ELEMENT",props: {nodeValue,children: [],},};
}
function createElement(type, props, ...children) {return {type,props: {...props,children: children.map(child => {return typeof child === "string" ? createTextNode(child) : child}),},};
}function render(el, container) {const dom =el.type === "TEXT_ELEMENT"? document.createTextNode(""): document.createElement(el.type);Object.keys(el.props).forEach(key => {if (key !== "children") {dom[key] = el.props[key];}})const children = el.props.childrenchildren.forEach(child => {render(child, dom)})container.append(dom);
}
const React={render,createElement
}
export default React
- ReactDOM.js
import React from './React.js'
const ReactDOM = {createRoot(container) {return {render(App){React.render(App, container)}}}
}export default ReactDOM
- App.js
import React from '../core/React.js'
const App =<div>Hello mini react! <span>Hi React</span></div>export default App
- main.js
import App from './src/App.js'
import ReactDOM from './core/ReactDOM.js'
ReactDOM.createRoot(document.querySelector("#root")).render(App)
运行项目发现效果是一样的
使用 jsx
因为刚开始使用vite创建的项目 所以把App.js和main.js改成App.jsx和main.jsx 然后在index.hrml script引用 在运行项目即可
以上就是我们对mini-react的基本搭建
任务调度器&fiber架构
使用了 requestIdleCallback
为什么使用requestIdleCallback
因为 render 函数中执行大量dom 渲染的时候 会导致卡顿,我们需要对任务进行拆分,拆分成一个个小任务,然后依次执行,从而避免卡顿
封装一个workLoop方法
- React.js
// 工作循环函数
let nextWorkOfUnit = {};
function workLoop(deadline) {// 工作循环函数,用于不断执行任务直至满足条件let shouldDeadline = false; // 初始化一个变量用于判断是否需要满足截止时间while (!shouldDeadline && nextWorkOfUnit) {// 循环执行任务,直到满足截止时间条件或者没有任务可执行nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit); // 使用任务处理函数处理当前任务对象shouldDeadline = deadline.timeRemaining() < 1; // 判断当前任务的截止时间是否小于1}// 请求下一次执行该函数的时间间隔,并递归调用该函数requestIdleCallback(workLoop);
}requestIdleCallback(workLoop);
- 实现 filber 架构(把树结构转变成链表结构)
- 首现判断当前子节点(child)中有没有子节点(child)
- 如果当前节点没有子节点,就找当前节点的兄弟节点(sibling)
- 如果当前节点没有兄弟节点(sibling),就找当前节点的父节点(parent) 的兄弟节点(sibling)
- 如果当前节点的父节点(parent) 没有兄弟节点(sibling),就在往上找
- 如果当前节点没有 parent 那么就结束
- 实现performWorkOfUnit
function createDom(type) {return type === "TEXT_ELEMENT"? document.createTextNode(""): document.createElement(type);
}function updateProps(dom,props){Object.keys(props).forEach((key) => {if (key !== "children") {dom[key] = props[key];}})
}
function initChildren(fiber){const children = fiber.props.children;let prvChild = null;children.forEach((child, index) => {const newFiber = {type: child.type,props: child.props,parent: fiber,child: null,sibling: null,dom: null,};if (index === 0) {fiber.child = newFiber;} else {prvChild.sibling = newFiber;}prvChild = newFiber;});
}
function performWorkOfUnit(fiber) {if (!fiber.dom) {const dom = (fiber.dom =createDom(fiber.type));fiber.parent.dom.append(dom);updateProps(dom,fiber.props)}initChildren(fiber)if (fiber.child) {return fiber.child;}if (fiber.sibling) {return fiber.sibling;}return fiber.parent?.sibling;
}
- 修改render
function render(el, container) {nextWorkOfUnit = {dom: container,props: {children: [el],},};
}
统一提交&实现 function component
统一提交
我们使用 requestIdleCallback实现任务调度,但是它只有等待浏览器有空闲时间才会执行任务,如果任务很多,那页面渲染就只能看到一半渲染。
- React.js
let nextWorkOfUnit = {}
let root = null
function render(el, container) {nextWorkOfUnit = {dom: container,props: {children: [el],},};root=nextWorkOfUnit
}
function workLoop(deadline) {let shouldDeadline = false;while (!shouldDeadline && nextWorkOfUnit) {nextWorkOfUnit = sunWorkFun(nextWorkOfUnit);shouldDeadline = deadline.timeRemaining() < 1;}if(!nextWorkOfUnit&&root){commitRoot()}requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);function commitRoot(){commitWork(root.child)
}function commitWork(fiber){if(!fiber) return;if(fiber.dom){fiber.parent.dom.append(fiber.dom);}commitWork(fiber.child)commitWork(fiber.sibling)
}
实现支持 function component
现在我们渲染dom是这样
ReactDOM.createRoot(document.querySelector("#root")).render(App)
我们想改成ReactDOM.createRoot(document.querySelector("#root")).render(<App />)
- React.js
// 创建元素节点
/*** 创建一个元素* @param {string} type - 元素的类型* @param {Object} props - 元素的属性* @param {...any} children - 元素的子元素* @returns {Object} - 创建的元素对象*/
function createElement(type, props, ...children) {return {type,props: {...props,children: children.map((child) => {/*** 判断子元素是否为文本节点* @type {boolean}*/const isTextNode =typeof child === "string" || typeof child === "number";return isTextNode ? createTextNode(child) : child;}),},};
}
// 提交节点
function commitWork(fiber) {// 检查fiber是否存在if (!fiber) return;// 初始化fiber的父级节点let fiberParent = fiber.parent;// 循环找到有dom节点的父级节点while (!fiberParent.dom) {fiberParent = fiberParent.parent;}fiberParent.dom.append(fiber.dom);// 递归调用commitWork函数处理fiber的子节点commitWork(fiber.child);// 递归调用commitWork函数处理fiber的兄弟节点commitWork(fiber.sibling);
}
/*** 更新函数组件* * @param {Object} fiber - Fiber对象*/
function updateFunctionComponent(fiber) {const children = [fiber.type(fiber.props)];initChildren(fiber, children);
}function updateHostComponent(fiber) {// 如果fiber没有关联的dom节点if (!fiber.dom) {// 创建一个新的dom节点const dom = (fiber.dom = createDom(fiber.type));// 更新dom节点的属性updateProps(dom, fiber.props, {});}// 获取子元素const children = fiber.props.children;// 初始化子元素initChildren(fiber, children);
}
/*** 函数:performWorkOfUnit* 描述:用于渲染节点的函数* 参数:* - fiber:fiber对象,包含节点的信息*/
function performWorkOfUnit(fiber) {/*** 变量:isFunctionComponent* 类型:boolean* 描述:判断fiber.type是否为函数节点*/const isFunctionComponent = typeof fiber.type === "function";/*** 判断不是函数节点且fiber.dom不存在时,创建dom节点并更新属性*/if (isFunctionComponent) {updateFunctionComponent(fiber);} else {updateHostComponent(fiber);}/*** 判断fiber是否有子节点,返回子节点*/if (fiber.child) {return fiber.child;}/*** 变量:nextFiber* 类型:fiber对象* 描述:遍历fiber对象的父级节点*/let nextFiber = fiber;while (nextFiber) {/*** 判断nextFiber是否有兄弟节点,返回兄弟节点*/if (nextFiber.sibling) return nextFiber.sibling;nextFiber = nextFiber.parent;}
}
进军 vdom 的更新
实现绑定事件
- App.jsx
function App() {function handleClick(){console.log("🚀 ~ App ~ App:")}return (<div><button onClick={handleClick}>click</button> </div>);
}
- 修改updateProps函数
function updateProps(dom,props){Object.keys(props).forEach(key=>{if(key.startsWith('on')){// 事件名const eventType = key.slice(2).toLowerCase();// 或.substring(2);dom.addEventListener(eventType,props[key]);}else{dom[key] = props[key];}})
}
更新props
对比 new vdom tree VS old vdom tree,找出差异,更新dom
- 创建 update 函数 使用currentRoot变量来存放当前的根节点
let currentRoot=null;
/*** 递归地提交根节点的子节点工作*/
function commitRoot() {commitWork(root.child);currentRoot = rootroot = null
}function update() {// 将 el 元素作为子元素,将 container 元素作为容器,创建 nextWorkOfUnit 对象root = {dom: currentRoot.dom,props: currentRoot.props};root=nextWorkOfUnit
}
- 找到老的节点
function update() {// 将 el 元素作为子元素,将 container 元素作为容器,创建 nextWorkOfUnit 对象root = {dom: currentRoot.dom,props: currentRoot.props,alternate: currentRoot //老的节点};root=nextWorkOfUnit
}
/*** 初始化子fiber对象* @param {Object} fiber - 当前fiber对象* @param {Array} children - 子子fiber对象数组*/
function initChildren(fiber, children) {let prvChild = null; // 上一个子fiber对象let oldFiber = fiber.alternate?.child; // 备用fiber对象children.forEach((child, index) => { // 遍历子fiber对象数组const isSameType = oldFiber && oldFiber.type === child.type; // 判断是否为相同类型let newFiber = null; // 创建新的fiber对象if (isSameType) { // 如果是相同类型newFiber = {type: child.type, // 设置fiber类型为子fiber类型props: child.props, // 设置fiber属性为子fiber属性parent: fiber, // 设置fiber父fiber为当前fiberchild: null, // 设置fiber子fiber为nullsibling: null, // 设置fiber兄弟fiber为nulldom: oldFiber.dom, // 设置fiber的dom为备选fiber的domeffectTag: "update", // 设置fiber效果标签为"update"alternate: oldFiber // 设置fiber备选fiber为备选fiber};} else { // 如果不是相同类型newFiber = {type: child.type, // 设置fiber类型为子fiber类型props: child.props, // 设置fiber属性为子fiber属性parent: fiber, // 设置fiber父fiber为当前fiberchild: null, // 设置fiber子fiber为nullsibling: null, // 设置fiber兄弟fiber为nulldom: null, // 设置fiber的dom为nulleffectTag: "placement" // 设置fiber效果标签为"placement"};}if (oldFiber) { // 如果备选fiber存在oldFiber = oldFiber.sibling; // 将备选fiber指向下一个fiber}if (index === 0) { // 如果是第一个子fiberfiber.child = newFiber; // 设置当前fiber的子fiber为新fiber} else {prvChild.sibling = newFiber; // 设置前一个子fiber的兄弟fiber为新fiber}prvChild = newFiber; // 更新前一个子fiber为新fiber});
}
- 比较diff props
- old 有 new 没有 删除
- new 有 old 没有 添加
- old new 都有 更新 2和3 可以合并成一个处理
// 提交节点
function commitWork(fiber) {// 检查fiber是否存在if (!fiber) return;// 初始化fiber的父级节点let fiberParent = fiber.parent;// 循环找到有dom节点的父级节点while (!fiberParent.dom) {fiberParent = fiberParent.parent;}// 如果fiber的effectTag为'update',则更新fiber的dom节点的属性if (fiber.effectTag === 'update') {updateProps(fiber.dom, fiber.props, fiber.alternate?.props)}// 如果fiber的effectTag为'placement',则将fiber的dom节点添加到fiber的父级节点中else if (fiber.effectTag === 'placement') {if (fiber.dom) {fiberParent.dom.append(fiber.dom);}}// 递归调用commitWork函数处理fiber的子节点commitWork(fiber.child);// 递归调用commitWork函数处理fiber的兄弟节点commitWork(fiber.sibling);
}
/*** 更新属性* @param {Object} dom - DOM元素* @param {Object} nextProps - 新的属性* @param {Object} prvProps - 旧的属性*/
function updateProps(dom, nextProps, prvProps) {Object.keys(prvProps).forEach((key) => {if (key !== "children") { // 排除children属性if (!(key in nextProps)) { // 如果新属性中不存在该键dom.removeAttribute(key); // 移除该属性}}});Object.keys(nextProps).forEach((key) => {if (key !== "children") { // 排除children属性if (nextProps[key] !== prvProps[key]) { // 如果新旧属性值不相同if (key.startsWith("on")) { // 如果属性名以"on"开头const eventType = key.slice(2).toLowerCase() // 获取事件类型dom.removeEventListener(eventType, prvProps[key]) // 移除事件监听器dom.addEventListener(eventType, nextProps[key]) // 添加事件监听器} else {dom[key] = nextProps[key]; // 更新属性值}}}});
}
function updateHostComponent(fiber) {// 如果fiber没有关联的dom节点if (!fiber.dom) {// 创建一个新的dom节点const dom = (fiber.dom = createDom(fiber.type));// 更新dom节点的属性updateProps(dom, fiber.props, {});}// 获取子元素const children = fiber.props.children;// 初始化子元素initChildren(fiber, children);
}
- 变量重命名 为了跟react保持一致性 root改为wipRoot initChildren改为reconcileChildren
- 更新以下代码
function render(el, container) {// 将 el 元素作为子元素,将 container 元素作为容器,创建 nextWorkOfUnit 对象wipRoot = {dom: container,props: {children: [el],},};nextWorkOfUnit = wipRoot
}
function update() {// 将 el 元素作为子元素,将 container 元素作为容器,创建 nextWorkOfUnit 对象wipRoot = {dom: currentRoot.dom,props: currentRoot.props,alternate: currentRoot};nextWorkOfUnit = wipRoot
}
- 导出update
- 查看效果 App.jsx
import React from "../core/React.js";let count = 10;let props = {id:'container'}function Counter({num}) {function handleClick(){count++;props = {}React.update()}return <div {...props}>Hi React <button onClick={handleClick}>count:{count}</button></div>;}function App(){return <div>Hello mini react!<Counter num={10} /></div>}export default App;
击杀 update children
- 创建和删除(type不一致的时候 删除旧的 创建新的)
let deletions = [];
let wipFiber = null;
/*** 递归地提交根节点的子节点工作*/
function commitRoot() {deletions.forEach(commitDeletions);commitWork(wipRoot.child);currentRoot = wipRoot;wipRoot = null;deletions = [];
}function commitDeletions(fiber) {if (fiber.dom) {// 初始化fiber的父级节点let fiberParent = fiber.parent;// 循环找到有dom节点的父级节点while (!fiberParent.dom) {fiberParent = fiberParent.parent;}fiberParent.dom.removeChild(fiber.dom);} else {commitDeletions(fiber.child);}
}
/*** 初始化子fiber对象* @param {Object} fiber - 当前fiber对象* @param {Array} children - 子子fiber对象数组*/
function reconcileChildren(fiber, children) {let prvChild = null; // 上一个子fiber对象let oldFiber = fiber.alternate?.child; // 备用fiber对象children.forEach((child, index) => {// 遍历子fiber对象数组const isSameType = oldFiber && oldFiber.type === child.type; // 判断是否为相同类型let newFiber = null; // 创建新的fiber对象if (isSameType) {// 如果是相同类型newFiber = {type: child.type, // 设置fiber类型为子fiber类型props: child.props, // 设置fiber属性为子fiber属性parent: fiber, // 设置fiber父fiber为当前fiberchild: null, // 设置fiber子fiber为nullsibling: null, // 设置fiber兄弟fiber为nulldom: oldFiber.dom, // 设置fiber的dom为备选fiber的domeffectTag: "update", // 设置fiber效果标签为"update"alternate: oldFiber, // 设置fiber备选fiber为备选fiber};} else {if (child) {// 如果不是相同类型newFiber = {type: child.type, // 设置fiber类型为子fiber类型props: child.props, // 设置fiber属性为子fiber属性parent: fiber, // 设置fiber父fiber为当前fiberchild: null, // 设置fiber子fiber为nullsibling: null, // 设置fiber兄弟fiber为nulldom: null, // 设置fiber的dom为nulleffectTag: "placement", // 设置fiber效果标签为"placement"};}if (oldFiber) {deletions.push(oldFiber); // 将备选fiber添加到删除数组}}if (oldFiber) {// 如果备选fiber存在oldFiber = oldFiber.sibling; // 将备选fiber指向下一个fiber}if (index === 0) {// 如果是第一个子fiberfiber.child = newFiber; // 设置当前fiber的子fiber为新fiber} else {prvChild.sibling = newFiber; // 设置前一个子fiber的兄弟fiber为新fiber}if (newFiber) {prvChild = newFiber; // 更新前一个子fiber为新fiber}});while (oldFiber) {deletions.push(oldFiber); // 将备选fiber添加到删除数组oldFiber = oldFiber.sibling;}
}
// 工作循环函数
function workLoop(deadline) {// 工作循环函数,用于不断执行任务直至满足条件let shouldDeadline = false; // 初始化一个变量用于判断是否需要满足截止时间while (!shouldDeadline && nextWorkOfUnit) {// 循环执行任务,直到满足截止时间条件或者没有任务可执行nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit); // 使用任务处理函数处理当前任务对象if (wipRoot?.sibling?.type === nextWorkOfUnit?.type) {nextWorkOfUnit = undefined;}shouldDeadline = deadline.timeRemaining() < 1; // 判断当前任务的截止时间是否小于1}if (!nextWorkOfUnit && wipRoot) {// 如果没有任务可执行且存在根对象,则提交根对象commitRoot();}// 请求下一次执行该函数的时间间隔,并递归调用该函数requestIdleCallback(workLoop);
}
/*** 更新函数组件** @param {Object} fiber - Fiber对象*/
function updateFunctionComponent(fiber) {wipFiber = fiber;const children = [fiber.type(fiber.props)];reconcileChildren(fiber, children);
}
function update() {const currentFiber = wipFiber;return () => {// 将 el 元素作为子元素,将 container 元素作为容器,创建 nextWorkOfUnit 对象wipRoot = {...currentFiber,alternate: currentFiber,};nextWorkOfUnit = wipRoot;};
}
- App.jsx
function Bar() {const update = React.update()function handleClick(){countBar++;update()}return <div><h1>Foo</h1>{countBar}<button onClick={handleClick}>click</button></div>;
}
搞定 useState
- 首先经常见到 react更新状态的时候使用useState
- App.jsx
function Foo() {const [count,setCount] = React.useState(1)function handleClick(){setCount((c)=>c+1)}return <div><h1>Foo</h1>{count}<button onClick={handleClick}>click</button></div>;
}
- main.js
/*** useState(initialState)函数用于在当前fiber中创建一个新的stateHook。* @param {any} initialState - 初始状态值* @returns {Array} - 返回包含当前状态和setState方法的数组*/
function useState(initialState) {const currentFiber = wipFiber;const oldHooks = currentFiber.alternate?.stateHook;const stateHook = {state: oldHooks ? oldHooks.state : initialState};currentFiber.stateHook = stateHook;/*** setState(actions)函数用于更新stateHook.state。* @param {Function|Object} actions - 更新状态的函数或要更新的状态对象*/function setState(actions) {stateHook.state = actions(stateHook.state)wipRoot = {...currentFiber,alternate: currentFiber,};nextWorkOfUnit = wipRoot;}return [stateHook.state, setState];
}
这样我们在使用上面dom的时候就可以正常使用了 但是还有问题 就是现在只有一个hook 假如有多个的话最后一个执行结果就会替换前面的 所有还需要把之前的存起来 并且批量执行函数
let stateHooks=[];
let stateHookIndex=0;/*** 更新函数组件** @param {Object} fiber - Fiber对象*/
function updateFunctionComponent(fiber) {stateHooks = [];stateHookIndex = 0;wipFiber = fiber;const children = [fiber.type(fiber.props)];reconcileChildren(fiber, children);
}
/*** useState(initialState)函数用于在当前fiber中创建一个新的stateHook。* @param {any} initialState - 初始状态值* @returns {Array} - 返回包含当前状态和setState方法的数组*/
function useState(initialState) {const currentFiber = wipFiber;const oldHooks = currentFiber.alternate?.stateHooks[stateHookIndex];const stateHook = {state: oldHooks ? oldHooks.state : initialState,queue: oldHooks ? oldHooks.queue : [],};// 执行stateHook.queue中的所有action,更新stateHook.statestateHook.queue.forEach((action) => {stateHook.state = action(stateHook.state);});stateHook.queue = [];stateHookIndex++;stateHooks.push(stateHook);currentFiber.stateHooks = stateHooks;/*** setState(actions)函数用于更新stateHook.state。* @param {Function|Object} actions - 更新状态的函数或要更新的状态对象*/function setState(actions) {// 将action添加到stateHook.queue中stateHook.queue.push(actions);wipRoot = {...currentFiber,alternate: currentFiber,};nextWorkOfUnit = wipRoot;}return [stateHook.state, setState];
}
这样的话就解决了上面的问题 但是假如说我们调用
setCount(11)
还是有问题 因为我们存入的不是function 所有还需要修改useState
function useState(initialState) {// 省略...function setState(actions) {const isFunction = typeof actions === "function";// 将action添加到stateHook.queue中stateHook.queue.push(isFunction ? actions : () => actions);// 省略...}return [stateHook.state, setState];
}
- 提前检测 减少不必要的更新 假如说当前值是11,setCount的还是11 这样情况就没必要更新
function useState(initialState) {// 省略...function setState(actions) {const isFunction = typeof actions === "function";const eagerState = isFunction ? actions(stateHook.state) : actions;if (eagerState === stateHook.state) return;// 将action添加到stateHook.queue中stateHook.queue.push(isFunction ? actions : () => actions);// 省略...}return [stateHook.state, setState];
}
搞定 useEffect
- useEffect 调用的时机是在 React 完成对 DOM 的渲染之后,并且浏览器完成绘制之前
- useEffect 的第二个参数是依赖数组,不指定的时候副作用指挥在组件渲染后执行一次,如果指定了依赖数组,那么只有当依赖数组中的值发生变化时,副作用才会执行
- App.jsx
function Foo() {console.log("🚀 ~ Foo ~ Foo:")const [count,setCount] = React.useState(1)function handleClick(){setCount((c)=>c+1)}React.useEffect(()=>{console.log('useEffect: ');},[])return <div><h1>Foo</h1>{count}<button onClick={handleClick}>click</button></div>;
}
- React.js
/*** 递归地提交根节点的子节点工作*/
function commitRoot() {deletions.forEach(commitDeletions);commitWork(wipRoot.child);commitEffectHooks();currentRoot = wipRoot;wipRoot = null;deletions = [];
}function useEffect(callback, deps) {// 创建一个effectHook对象,包含callback、deps和cleanup属性const effectHook = {callback,deps,cleanup: undefined,};// 将effectHooks赋值给wipFiber的effectHooks属性wipFiber.effectHook = effectHook;
}
function commitEffectHooks() {// 开始执行effectHooks的回调函数function run(fiber) {if (!fiber) return;fiber.effectHook?.callback()// 递归执行run函数run(fiber.child);run(fiber.sibling);}// 开始执行effectHooks的回调函数run(wipRoot);
}
- 上面函数初始化的时候就可以监听到了
- 更新监听
function commitEffectHooks() {// 开始执行effectHooks的回调函数function run(fiber) {if (!fiber) return;if (!fiber.alternate) {fiber.effectHook?.callback()} else {const oldHooks = fiber.alternate?.effectHook;const needUpdate = oldHooks.deps.some((olDep, idx) => {return olDep !== fiber.effectHook.deps[idx];});needUpdate && fiber.effectHook.callback();}// 递归执行run函数run(fiber.child);run(fiber.sibling);}run(wipRoot);
}
更新已经实现 但是还有问题 他现在只能监听一个 所有监听多个需要修改代码
/*** 更新函数组件** @param {Object} fiber - Fiber对象*/
function updateFunctionComponent(fiber) {stateHooks = [];stateHookIndex = 0;effectHooks = [];wipFiber = fiber;const children = [fiber.type(fiber.props)];reconcileChildren(fiber, children);
}
let effectHooks = [];
function useEffect(callback, deps) {// 创建一个effectHook对象,包含callback、deps和cleanup属性const effectHook = {callback,deps};// 将effectHook添加到effectHooks数组中effectHooks.push(effectHook);// 将effectHooks赋值给wipFiber的effectHooks属性wipFiber.effectHooks = effectHooks;
}function commitEffectHooks() {// 开始执行effectHooks的回调函数function run(fiber) {if (!fiber) return;if (!fiber.alternate) {// 初始化effectHooks的cleanup方法fiber.effectHooks?.forEach((hook) => hook.callback());} else {// 更新effectHooks的cleanup方法fiber.effectHooks?.forEach((newHooks, index) => {if (newHooks.deps.length > 0) {const oldHooks = fiber.alternate?.effectHooks[index];const needUpdate = oldHooks.deps.some((olDep, idx) => {return olDep !== newHooks.deps[idx];});needUpdate && newHooks.callback();}});}// 递归执行run函数run(fiber.child);run(fiber.sibling);}// 开始执行effectHooks的回调函数run(wipRoot);
}
- cleanup 实现
- cleanup在调用useEffect之前调用 当deps为空的时候不会返回cleanup
- 作用就是清空副作用
- App.jsx 表现形式
function Foo() {console.log("🚀 ~ Foo ~ Foo:")const [count,setCount] = React.useState(1)function handleClick(){setCount((c)=>c+1)setBar('bar11')}React.useEffect(()=>{console.log('useEffect: ');return ()=>{console.log('return-cleanup: ');}},[])React.useEffect(()=>{console.log('count: ',count);return ()=>{console.log('return-cleanup: ');}},[count])return <div><h1>Foo</h1>{count}<button onClick={handleClick}>click</button></div>;
}
- React.js
function useEffect(callback, deps) {// 创建一个effectHook对象,包含callback、deps和cleanup属性const effectHook = {callback,deps,cleanup: undefined,};// 将effectHook添加到effectHooks数组中effectHooks.push(effectHook);// 将effectHooks赋值给wipFiber的effectHooks属性wipFiber.effectHooks = effectHooks;
}
function commitEffectHooks() {// 开始执行effectHooks的回调函数function run(fiber) {if (!fiber) return;if (!fiber.alternate) {// 初始化effectHooks的cleanup方法fiber.effectHooks?.forEach((hook) => {hook.cleanup = hook.callback();});} else {// 更新effectHooks的cleanup方法fiber.effectHooks?.forEach((newHooks, index) => {if (newHooks.deps.length > 0) {const oldHooks = fiber.alternate?.effectHooks[index];const needUpdate = oldHooks.deps.some((olDep, idx) => {return olDep !== newHooks.deps[idx];});needUpdate && (newHooks.cleanup = newHooks.callback());}});}// 递归执行run函数run(fiber.child);run(fiber.sibling);}// 执行effectHooks的cleanup方法function runCleanup(fiber){// 如果fiber为空,直接返回if (!fiber) return;// 如果fiber的alternate属性存在,且effectHooks属性存在fiber.alternate?.effectHooks?.forEach(hook=>{// 如果hook的deps数组长度大于0if (hook.deps.length > 0) {// 如果hook有cleanup方法,则执行cleanup方法hook.cleanup && hook.cleanup();}})// 递归执行runCleanup方法,传入fiber的child属性作为参数runCleanup(fiber.child);// 递归执行runCleanup方法,传入fiber的sibling属性作为参数runCleanup(fiber.sibling);}// 开始执行effectHooks的回调函数runCleanup(wipRoot);run(wipRoot);
}
到此我们最简单的mini-react 就完成了
文中仅是自己在学习过程的总结,很多地方可能文笔有误,个人思考能力也不足。仅作为个人的学习结课文章,谢谢大家。
github