Build your own React

Tags
Web Dev
React
Published
October 20, 2020
Author
HJS
/** * 为避免混淆,使用 element 来指代 React 元素,并使用 node 来指代 DOM 元素 */ // 根据参数创建 React ELement function createElement(type, props, ...children) { return { type, props: { ...props, children: children.map(child => typeof child === 'object' ? child : createTextElement(child) ), }, }; } // 文本 React Element function createTextElement(text) { return { type: 'TEXT_ELEMENT', props: { nodeValue: text, children: [], }, }; } /** * 根据 fiber 构造 node * @param fiber * @returns {Text|*} */ function createDom(fiber) { const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode('') : document.createElement(fiber.type); updateDom(dom, {}, fiber.props); return dom; } const isEvent = key => key.startsWith('on'); const isProperty = key => key !== 'children' && !isEvent(key); const isNew = (prev, next) => key => prev[key] !== next[key]; const isGone = (prev, next) => key => !(key in next); // 更新 Dom function updateDom(dom, prevProps, nextProps) { //移除或改变 event listener 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] ); }); // 移除旧的 props Object.keys(prevProps) .filter(isProperty) .filter(isGone(prevProps, nextProps)) .forEach(name => { dom[name] = ''; }); // 设置新的 props 或改变旧 props 的值 Object.keys(nextProps) .filter(isProperty) .filter(isNew(prevProps, nextProps)) .forEach(name => { dom[name] = nextProps[name]; }); // 添加新的 event listener Object.keys(nextProps) .filter(isEvent) .filter(isNew(prevProps, nextProps)) .forEach(name => { const eventType = name .toLowerCase() .substring(2); dom.addEventListener( eventType, nextProps[name] ); }); } function commitRoot() { deletions.forEach(commitWork); commitWork(wipRoot.child); currentRoot = wipRoot; wipRoot = null; } function commitWork(fiber) { if (!fiber) { return; } let domParentFiber = fiber.parent; while (!domParentFiber.dom) { domParentFiber = domParentFiber.parent; } const domParent = domParentFiber.dom; if ( fiber.effectTag === 'PLACEMENT' && fiber.dom != null ) { domParent.appendChild(fiber.dom); } else if ( fiber.effectTag === 'UPDATE' && fiber.dom != null ) { updateDom( fiber.dom, fiber.alternate.props, fiber.props ); } else if (fiber.effectTag === 'DELETION') { commitDeletion(fiber, domParent); } /** * 递归 commit fiber.child、fiber.sibling */ commitWork(fiber.child); commitWork(fiber.sibling); } function commitDeletion(fiber, domParent) { if (fiber.dom) { domParent.removeChild(fiber.dom); } else { commitDeletion(fiber.child, domParent); } } /** * wipRoot赋值,构造fiber root。赋值全局变量nextUnitOfWork,赋值fiber root * @param element root element:Virtual DOM * @param container root node */ const render = (element, container) => { wipRoot = { dom: container, props: { children: [element], }, alternate: currentRoot, }; deletions = []; nextUnitOfWork = wipRoot; }; let nextUnitOfWork = null; // 指向下一个工作单元 let currentRoot = null; // 指向页面视图当前的fiber root节点(React root container构造出的fiber节点) let wipRoot = null; // 指向workInProgress fiber root节点 let deletions = null; // 保存render阶段 reconcile 计算出的要删除的fiber集合,在commit的阶段删除 /** * render函数中赋值 nextUnitOfWork,performUnitOfWork开始执行,构造fiber tree */ function workLoop(deadline) { let shouldYield = false; // 循环构建出fiber tree while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // IdleDeadline.timeRemaining():Web API:提供一个当前空闲时间的毫秒数 shouldYield = deadline.timeRemaining() < 1; } // 构建完毕(render阶段结束),开始commit阶段 if (!nextUnitOfWork && wipRoot) { commitRoot(); } requestIdleCallback(workLoop); } /** * 时间切片注册workLoop事件。程序的入口 */ requestIdleCallback(workLoop); /** * 处理每一个工作单元(fiber),并返回下一个要处理的fiber * @param fiber 当前任务处理的fiber节点 * @returns {*} 返回下一轮任务要处理的fiber节点 */ function performUnitOfWork(fiber) { /** * Function components 和 Host(原生:即 DOM 类型的)component: * 1. function components 的 fiber 中没有对应的 DOM 节点 * 2. function components 的要渲染的是其 children,来自运行该 function,而不是直接从 props 上取 * * Fuction component编 译后的fiber节点,对应的 type 属性是一个 function components 构造函数 */ const isFunctionComponent = fiber.type instanceof Function; if (isFunctionComponent) { updateFunctionComponent(fiber); } else { updateHostComponent(fiber); } /** * 1. child作为下一个构建单元 * 2. 如果没有child,则用sibling * 3. 否则则查看parent的sibling,如果没有,则向上便利,直到找到parent的sibling * 4. 如果到达root,意味着构建完毕(最后一次root.parent是undefined,终止遍历) */ if (fiber.child) { return fiber.child; } let nextFiber = fiber; while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling; } nextFiber = nextFiber.parent; } } let wipFiber = null; // 正在处理的 function component fiber节点 let hookIndex = null; // wipFiber 当前处理的 hook 索引 function updateFunctionComponent(fiber) { wipFiber = fiber; hookIndex = 0; wipFiber.hooks = []; /** * fiber 对应 React Element 的 props.children */ const children = [fiber.type(fiber.props)]; reconcileChildren(fiber, children); } function useState(initial) { const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]; const hook = { state: oldHook ? oldHook.state : initial, action: (value) => value }; /** * React的 hook 对应的状态不是绑定在 hook 上,而是和 hook 所在的链表一样,也有对应的线性表储存状态,运行时会按顺序作为 hook 的状态。 */ const action = oldHook ? oldHook.action : (initial) => initial; hook.state = action(hook.state); /** * 函数式更新,接收一个 action */ const setState = action => { hook.action = action; /** * setState 初始化,与当前的 fiber 绑定;即 setState 时,useState 所在的组件重渲染 */ wipRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot, }; nextUnitOfWork = wipRoot; deletions = []; }; wipFiber.hooks.push(hook); hookIndex++; return [hook.state, setState]; } /** * 如果是第一次渲染还没有fiber.dom,则创建dom。根据fiber,reconcile计算更新组件的规则 * @param fiber */ function updateHostComponent(fiber) { if (!fiber.dom) { fiber.dom = createDom(fiber); } reconcileChildren(fiber, fiber.props.children); } /** * 构建当前 fiber 的 child fiber * 顺序构造出 fiber 对应 React Element 的子元素的 fiber,用 sibling 指针相连 * Ps. 在这份源码中,用React root container构造fiber root,实际上需要遍历构造的是从 React root(<App/>)开始,所以这里构造的是 child fiber * @param fiber 当前 fiber节 点 * @param elements 当前 fiber 对应的 React Element 的子元素集合 */ function reconcileChildren(fiber, elements) { // fiber.alternate:上一轮 commit 的 fiber,新增节点则为 null // oldFiber:fiber.alternate.child,此时指向 旧 fiber 的第一个子 fiber 节点 let oldFiber = fiber.alternate && fiber.alternate.child; let prevSibling = null; // 记录处理的 element 的索引 let index = 0; // 1. 分层对比:遍历 elements 和 oldFiber 对比 // 2. 构造当前 fiber 节点的 fiber.child,fiber.child.sibling while (index < elements.length || oldFiber != null) { const element = elements[index]; // 当前处理的 element let newFiber = null; // 每一轮循环重新初始化 newFiber = null const sameType = // 标识 element 是否和 oldFiber 的类型一致 oldFiber && element && element.type === oldFiber.type; // 如果类型相同,更新节点 if (sameType) { newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, parent: fiber, alternate: oldFiber, effectTag: 'UPDATE', }; } if (element && !sameType) { newFiber = { type: element.type, props: element.props, dom: null, parent: fiber, alternate: null, effectTag: 'PLACEMENT', }; } if (oldFiber && !sameType) { oldFiber.effectTag = 'DELETION'; // 用 deletions 保存 fiber,在commit阶段去除 deletions.push(oldFiber); } if (oldFiber) { oldFiber = oldFiber.sibling; } // 如果当前处理的 element 是第一个 if (index === 0) { fiber.child = newFiber; } else if (element) { prevSibling.sibling = newFiber; } prevSibling = newFiber; index++; } } const MyReact = { createElement, render, useState, }; /** * 提示 babel 使用自定义的 createElement 转译 JSX */ /** @jsx MyReact.createElement */ function Counter(props) { const [state, setState] = useState(0); const [count, setCount] = useState(0); return ( <div> <h1 onClick={() => setState(c => c + 1)}> {props.children[0]} <div>State: {state}</div> <div>Count:{count}</div> </h1> <button onClick={() => setState(s => s + 1)}> State 加一</button> <button onClick={() => setCount(c => c - 1)}> Count 减一</button> </div> ); } /** * element 经过编译后,是用 createElement 生成的 React Element 对象,也就是 Virual DOM * @type {JSX.Element} */ const element = <Counter><h1>123</h1></Counter>; const container = document.getElementById('root'); MyReact.render(element, container);
 

参考