React 现已成为前端主流开发框架之一,在此之前,我们常选用原生 JS 或者 JQuery 进行开发,在之后的发展中,为什么会诞生诸如 React、Vue、Angular 的框架呢?原因有下:
最初我们完成前端开发中节点添加的功能,需要直接操作 DOM,原生 JavaScript 代码如下:
const divDom = document.createElement('div');divDom.textContent = 'Hello World!';document.body.appendChild(divDom);
JQuery 版本代码如下:
const $div = $('<div></div>');$div.text('Hello World');$(document.body).append($div);
以上是操作 DOM 的一个简单例子,如果场景更复杂,我们通过这种方式开发应用成本是很高的,此时,诸如 React 的框架应运而生,以上代码我们使用开发如下:
const App = <div>Hello World!</div>;ReactDOM.render(<App />, document.getElementById('app'));
render 方法被调用时,组件将会被挂载到 DOM 节点上,在 React 中,App 组件和 DOM 之间是有能在联系的。
上例中我们还发现有更有趣的地方,JavaScript 代码中可以直接书写 HTML 代码片段,这一特性我们称之为 JSX。但,如果你了解过 React 早期的项目,你也许还记得:
React.createClass();
之类的函数,为什么新版本的 React 中已不见其踪影?那是因为新版 React 应用大多需要编译,我们通常使用 babel 来编译前端代码,这也就是说我们所书写的 JSX,其实是面向于开发者的,最终 JSX 会被 babel 进行转换,我们以下为例:
<div title="header">hello</div>
这里抛出一个问题,如果你需要用 JavaScript 设计一种结构去存储这个节点,你会怎么做?带着这个问题,结合我们说到的 babel 转化 JSX 代码,createElement 登场。
Tip:jsx 代码使用 babel-preset-react 进行处理
上例经过转换后,生成如下代码:
React.createElement('div',{title: 'header',},'hello',);
到这里才真正触及 React 框架内容,我们按图索骥,从这个 api 着手,一瞥 React 设计思想。
React.createElement
方法调用返回的参数正是我们前面抛出问题的答案,这一段简短 JSX 需要用数据来进行描述,怎样描述呢?我们发现,每个 JSX 都能够抽象为一个对象,JSX 的嵌套可以使用 Tree 描述。我们先来看最简单结构调用 React.createElement
方法后的返回:
const element = {type: 'div',props: {title: 'header',children: 'hello',},};
既然有了这些描述信息,我们想渲染到 DOM 中就很容易了,怎么做呢?我们通过节点描述信息,将该节点挂载到 DOM 上:
const element = {type: 'div',props: {title: 'header',children: 'hello',},};const { type, props } = element;const dom = document.createElement(type);dom.setAttribute(props.title);const text = document.createTextNode(props.children);dom.appendChild(text);document.getElementById('app').appendChild(dom);
以上模拟了 React 框架最核心的内容,概括起来包括:
前面我们看到,JSX 经过 babel 转换后,还生成包含 React.createElement
片段的代码,这小节我们深入研究该 api 的实现。 首先我们来看一个例子,在开发中通常节点信息略微复杂,假设现在有这样一段 JSX:
<div title="header"><p title="name">name</p></div>
首先经过 babel 转化后,会生成如下代码:
React.createElement('div',{title: 'header',},React.createElement('p',{title: 'name',},'name',),);
发现规律了吗?createElement
接收三个参数,第一个参数表示当前 JSX 节点的类型,第二个参数表示当前 JSX 节点属性,第三个参数为嵌套内容,不过有时,不止三个参数,前两个参数是固定的,以后的参数为动态参数,个数未定,这点我们稍后介绍。 最终 React.createElement
函数调用后,如上结构会用以下结构描述:
const element = {type: 'div',props: {title: 'header',children: [{type: 'div',props: {title: 'name',children: 'hello',},},],},};
根据这个分析,我们再来个复杂些的 JSX 结构:
<div title="header"><p title="n1">n1 text</p><p title="n2">n2 text</p></div>
经过 JSX 转化后,会生成如下代码:
React.createElement('div',{title: 'header',},React.createElement('p',{title: 'n1',},'n1 text',),React.createElement('p',{title: 'n2',},'n2 text',),);
规律似乎已找到,根据总结的这些内容,我们进行初步尝试,封装 createElement
函数。封装该函数有几个要点,分别为:
好了,来试试吧!
function createElement(type, props, ...children) {return {type,props: {...props,children,},};}
这里 children 中的元素可能有不同类型的值,我们划分为 object 和 string 类型,在处理时,需要根据不同类型区别处理。进行优化后如下:
function createElement(type, props, ...children) {return {type,props: {...props,children: children.map(child =>typeof child === 'object' ? child : createTextElement(child),),},};}function createTextElement(text) {return {type: 'TEXT_ELEMENT',props: {children: text,text,},};}
不过,我们这里处理还是有漏洞的,例如布尔类型值 null 依然会创建节点并挂载,这与 React 设计是不一致的,我们这里先忽略。
处理完整步骤如下:
const App = (<div title="header"><p title="n1">n1 text</p></div>);
babel 转换为如下代码:
const App = React.createElement('div',{title: 'header',},React.createElement('p',{title: 'n1',},'n1 text',),);
该函数调用之后,返回如下对象:
const element = {type: 'div',props: {title: 'header',children: {type: 'p',props: {title: 'n1',children: 'n1 text',},},},};
Tip:当一个元素中嵌套一个以上节点时,children 为数组;当只有单个元素使,children 为对象;只有文字内容时,children 为字符串。本例以单个子元素进行分析。
通过该方法,我们就可以将节点挂载到 DOM 上了,该操作在 ReactDOM.render 函数中进行。
render 函数负责根据转化后的节点信息,创建对应节点,然后挂载 DOM,节点类型来自于节点信息的 type 字段。这样我们可以分析出最简单的函数体结构,如下:
function render(element, rootDom) {const dom = document.createElement(element.type);rootDom.appendChild(dom);}
在此基础上,我们还需要支持节点的嵌套,我们再审视一下节点信息,发现节点中的 children 属性构成自顶向下的树结构,我们可以使用当前节点可视为内层嵌套节点的父节点,所以这里可以使用递归实现 DOM 绑定,修改后的 render 如下:
function render(element, rootDom) {const dom = document.createElement(element.type);const { children } = element.props;children.forEach(child => render(child, dom));rootDom.appendChild(dom);}
此时,程序已初具雏形,但任无法运行,因为 children 类型存在对象和字符串,所以需要判断 type 以使用不同方式创建 DOM 节点,并在合适时机递归调用 render,修改后代码如下:
function render(element, rootDom) {const dom =element.type === 'TEXT_ELEMENT'? document.createTextNode(element.props.text): document.createElement(element.type);const { props: elProps } = element;/*** Assign all the values in props to the DOM as properties* 将 props 中的所有值,作为属性赋值给 DOM* */Object.keys(elProps).forEach(name => {if (name !== 'children' && element.type !== 'TEXT_ELEMENT') {dom.setAttribute(name, elProps[name]);}});const { children } = elProps;if (Array.isArray(children)) {children.forEach(child => render(child, dom));}rootDom.appendChild(dom);}
我们这里忽略了 children 为数组的情况。
完整代码展示如下:
function createElement(type, props, ...children) {return {type,props: {...props,children: children.map(child =>typeof child === 'object' ? child : createTextElement(child),),},};}function createTextElement(text) {return {type: 'TEXT_ELEMENT',props: {children: text,text,},};}function render(element, rootDom) {const dom =element.type === 'TEXT_ELEMENT'? document.createTextNode(element.props.text): document.createElement(element.type);const { props: elProps } = element;/*** Assign all the values in props to the DOM as properties* 将 props 中的所有值,作为属性赋值给 DOM* */Object.keys(elProps).forEach(name => {if (name !== 'children' && element.type !== 'TEXT_ELEMENT') {dom.setAttribute(name, elProps[name]);}});const { children } = elProps;if (Array.isArray(children)) {children.forEach(child => render(child, dom));}rootDom.appendChild(dom);}const HeReact = {createElement,render,};/** @jsx HeReact.createElement */const element = (<div title="header"><p title="n1">n1 text</p></div>);HeReact.render(element, document.getElementById('app'));
以上 @jsx 注释代码,使用 @babel/plugin-transform-react-jsx 即可完成转化。简单提示如下:
npm install --save-dev @babel/core @babel/cli
npm install --save-dev @babel/plugin-transform-react-jsx
,.babelrc 中配置 { "plugins": ["@babel/plugin-transform-react-jsx"] }
npx babel index.js --out-file index.prod.js
我们对计算机系统中的 CPU 调度可能并不陌生,我们期望 CPU 能够最大能力执行任务,这就需要协调资源从而充分利用。在 React 中,我们也期望实现这样一个调度系统,在合适时机去更新视图。
通常,需要触发视图更新的操作有以下几种:
以上三种为视图更新过程中的优先级顺序,我们期望按照这个顺序执行视图更新,因此需要定义更新优先级规则。这个过程我们称为调度(schedule),这是在 React 16 中额外加入的。
在 React 15 中,React 架构可归纳为:
而在 React 16 中,为了优化性能,更大程度使用资源,调整架构如下:
在 React 15 中使用 Stack Reconcilation(栈调和器)
最大的缺陷是调和过程不可中断,从而导致线程阻塞,造成动画卡顿、用户输入反馈迟钝等问题。在浏览器中的 js 没有进程的概念,我们只有从侧面完成抢占资源这一操作。
React 16 中,调度相关内容单独发布为 Scheduler 包,在早期,调度时间分片使用计算 expirationTime 实现,但这种方式的缺陷日益明显,因此重构使用了 lane 模型替换了 expirationTime 的方式。
由于调度实现略微复杂,并且早期 React 团队考虑了使用较为简单的做法(requestIdleCallback)实现,我们将以这种较为简单的方式实现。
考虑方案普适性与健壮性,我们将以上提到的调度方案进行排序:lane > expirationTime > requestIdleCallback
requestIdleCallback 方法和我们常见的 setTimeout 或者 requestAnimationFrame 类似,不同的是,requestIdleCallback 方法可以在浏览器主线程空闲时调用,以此实现资源抢占。
function workLoop() {// ...requestIdleCallback(workLoop)}requestIdleCallback(workLoop)
当浏览器主线程空闲时间,则会尝试调用 workLoop 方法,该方法中完成 React 组件更新优先级的评判,并且在需要的时,暂停当前更新以执行更紧急的任务,紧急任务执行完后,会回到中断点继续执行之前暂停任务。
我们需要定义一个链表,用于存储需要执行的任务,我们将该链表定义为 nextUnitOfWork,初始值为空。当浏览器空闲,则会检查链表是否有,有则需要执行处理并更新视图,改进后代码如下:
let nextUnitOfWork = null;function workLoop() {while(nextUnitOfWork) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork)}requestIdleCallback(workLoop)}requestIdleCallback(workLoop)function performUnitOfWork(nextUnitOfWork) {//...}
但我们发现,这里 performUnitOfWork 是无法中断的,怎么改进呢?其实很简单,加一个需要中断的标识,定义一个判断是否需要中断的方法,执行该方法来做检测:
// shouldYieldfunction shouldYield() {// ...return false;}while (nextUnitOfWork) {if (shouldYield()) {break;}nextUnitOfWork = performUnitOfWork(nextUnitOfWork);}
这样就能实现中断功能了,在 React 中使用 expirationTime 机制实现优先级,并且,很多面试中会考察,React 中 setState 是异步的吗?为什么?
其实就是因为 React 中 expirationTime 的计算机制,在一定范围类触发的更新任务会合并处理。
为了实现任务切片,React 引入了 Fiber。
Fiber 是一个数据结构用来描述任务与节点之间的关系,Fiber 还是一种架构方案用于实现 React 16 的 Reconciler。
我们先以数据结构角度分析 Fiber。Fiber 像一棵树,每个节点均存储了与相邻节点之间的关系。通常一个 Fiber 节点需要存储这些关系:
有了这样的数据结构,下一个执行单元的搜寻会变得更为简单 比如我们现在有这样的页面结构:
<div><p><span></span><a></a></p></div>
当我们从 div 开始时,会尝试访问 div 的子节点,以此来到了 p,在 p 节点尝试访问子节点,来到了 span, 此时 span 已然没有了子元素,只得访问兄弟节点,于是来到了 a。此时 a 没有子元素,也没有兄弟元素,则只能往上到达父节点 p,p 已遍历完所有后代且没有兄弟元素,则继续通过 parent 找到 div,我们这里的 div 是 root 节点,不再网上搜寻,至此完成 所有 performUnitOfWork 工作。
这里我们需要清楚一点,经过以上步骤后,才会进到 render,进行页面渲染,就像我们前面提到的 React 16 架构:Scheduler -> Reconciler -> Renderer。首先调整 render 中的代码,还记得之前的 nextUnitOfWork 吗?在初始化的时候,也就是在我们调用 render 函数的时候,首次将 nextUnitOfWork 设置为如下结构:
function render(element, dom) {nextUnitOfWork = {dom: container,props: {children: [element],},};}
这里可以看到的对象,就是简化后的 Fiber,其中包含了当前 unitOfWork 对应的 dom,以及 props 等信息,当然 React 中的 Fiber 结构比这更复杂,我们逐步完善。
有了 Fiber 结构,我们来改写 performUnitOfWork 方法。通常,我们需要将组件进行分类,比如属于浏览器的宿主组件(div、p)更新,再比如我们自定义的函数组件,我们需要通过 Fiber 的 type 属性区分,本例以这两种组件的更新为例:
function performUnitOfWork(fiber) {/*** Determines whether it is a functional component or a browser native tag component* 判断是函数组件,还是浏览器原生标签组件* */const isFunctionComponent = fiber.type instanceof Function;/*** Depending on the type, different update methods are called* 根据不同类型,调用不同更新方法* */if (isFunctionComponent) {updateFunctionComponent(fiber);} else {updateHostComponent(fiber);}/*** Return to the next unitOfWork* 返回下一个 unitOfWork* */if (fiber.child) {return fiber.child;}/*** If there is no point of its own, you need to follow the lookup we analyzed earlier up to the root level* 如果没有自己点,则需要按照我们前面分析的查找逐级往上直到 root* */let nextFiber = fiber;while (nextFiber) {if (nextFiber.sibling) {return nextFiber.sibling;}nextFiber = nextFiber.parent;}}function updateFunctionComponent(fiber) {/*TODO*/}function updateHostComponent(fiber) {/*TODO*/}
现在回到 render 函数定义,我们需要介绍另一个 React 核心思想:双缓存。通常,我们为了提升计算性能,会将计算的过程再内存中进行,计算得到结果后,一次性将结果渲染到页面,这样的机制我们称为双缓存。前面我们提到,React 应用在调度过程,借助 nextUnitOfWork 进行 workLoop 遍历,与此同时还有另外一个变量,用于备份 nextUnitOfWork ,我们且将这个变量定义为 wipRoot,与之形成双缓存机制的变量我们定义为 currentRoot,那么在应用初次渲染时,为该变量赋值。我们看改进后的 render 函数:
function render(element, dom) {wipRoot = {dom,props: {children: [element],},}nextUnitOfWork = wipRoot}let nextUnitOfWork = nulllet wipRoot = nulllet currentRoot = null;
我们已经清楚,当 nextUnitOfWork 为空时,表示已将触发更新后需要更新的节点遍历完成,这是就需要将 reconcile 的结果渲染到页面。渲染的过程,我们单独定义在 commitRoot
函数中。commitRoot 函数其实很简单,调用 commitWork 用于将每个节点单元渲染至页面这个函数在 workLoop 中调用,前提条件正是 nextUnitOfWork 为空并且 wipRoot 存在。代码示例如下:
function commitRoot() {// TODO}function workLoop(deadline) {// ...if (!nextUnitOfWork && wipRoot) {commitRoot();}// ...}
接下来,处理 commitRoot,该函数是提交更新的入口,各节点更新我们定义 commitWork 实现,示例如下:
function commitRoot() {commitWork(wipRoot.child)wipRoot = null}function commitWork(fiber) {if (!fiber) {return}const domParent = fiber.parent.domdomParent.appendChild(fiber.dom)/*** Recursively traverse the nodes that need to be updated* 递归遍历需要更新的节点* */commitWork(fiber.child)commitWork(fiber.sibling)}
介绍了整体内容,我们回到调和过程。调和其实就是为每个节点 Fiber 打标签,哪个更改了?哪个删除了?哪个替换了?这些情况均会在遍历到每个节点时,标记到 Fiber 的 effectTag 上,最后在 commitWork 中会根据这些 effectTag 进行渲染处理。我们先来看普通宿主组件的更新 reconcile 处理。普通宿主组件的 reconcile 处理入口为 updateHostComponent:
function updateHostComponent(fiber) {if (!fiber.dom) {fiber.dom = createDom(fiber);}reconcileChildren(fiber, fiber.props.children);}function reconcileChildren(wipFiber, elements) {}
对于子节点的遍历,我们通过 Fiber 上的缓存信息获取,也就是 alternate,获取到以后,遍历所有子节点,完成比对。reconcileChildren 函数中定义类型对比规则,以及生成新的 Fiber。
function reconcileChildren(wipFiber, elements) {let index = 0;let oldFiber = wipFiber.alternate && wipFiber.alternate.child;let prevSibling = null;while (index < elements.length || oldFiber != null) {const element = elements[index];let newFiber = null;const sameType = oldFiber && element && element.type == oldFiber.type;if (sameType) {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: null,parent: wipFiber,alternate: null,effectTag: 'PLACEMENT',};}if (oldFiber && !sameType) {oldFiber.effectTag = 'DELETION';deletions.push(oldFiber);}if (oldFiber) {oldFiber = oldFiber.sibling;}if (index === 0) {wipFiber.child = newFiber;} else if (element) {prevSibling.sibling = newFiber;}prevSibling = newFiber;index++;}}
映入眼帘的是 index 这个变量,我们知道 React 在遍历生成子组件时,都需要指定 key 以提高比对性能,这里我们使用 index 来模拟这个过程。 这里可以清晰看到,在对比了 child 后,会根据对比结果,生成对应新的 Fiber,每个 Fiber 中都给定了需要对节点做出什么处理:
这些流程都完成以后,表示 reconcile 完成,这是 nextUnitOfWork 为空,则进入 commit 阶段,从 commitRoot 方法进入,递归调用 commitWork 方法以更新 reconcile 对比出所有需要更新的节点。
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);}commitWork(fiber.child);commitWork(fiber.sibling);}function commitDeletion(fiber, domParent) {if (fiber.dom) {domParent.removeChild(fiber.dom);} else {commitDeletion(fiber.child, domParent);}}function updateDom(dom, prevProps, nextProps) {// TODO}
可以看到,commitWork 原理其实也比较简单,就是将 fiber 遍历,根据 reconcile 阶段对比出的差异,根据不同 effectTag 值执行对应操作:
updateDom 中定义更新规则,这里需要定义一些变量用于更新,示例如下:
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);function updateDom(dom, prevProps, nextProps) {/*** 删除旧的事件处理* */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]);});/*** Delete old props* 删除旧 props* */Object.keys(prevProps).filter(isProperty).filter(isGone(prevProps, nextProps)).forEach(name => {dom[name] = '';});/*** Set up a new props* 设置新的 props* */Object.keys(nextProps).filter(isProperty).filter(isNew(prevProps, nextProps)).forEach(name => {dom[name] = nextProps[name];});/*** Add a new event handler* 添加新的事件处理函数* */Object.keys(nextProps).filter(isEvent).filter(isNew(prevProps, nextProps)).forEach(name => {const eventType = name.toLowerCase().substring(2);dom.addEventListener(eventType, nextProps[name]);});}
到这里,我们的项目已具备了基础的 React 功能,接下来为了简单串联组件以及状态的更新处理,进一步封装函数组件处理以及 useState Hook。
定义函数组件的更新处理函数 updateFunctionComponent:
let hookIndex = null;function updateFunctionComponent(fiber) {wipFiber = fiber;hookIndex = 0;wipFiber.hooks = [];const children = [fiber.type(fiber.props)];reconcileChildren(fiber, children);}
函数组件的状态使用 hooks 进行存储,我们将这些状态放在 fiber 的 hooks 数组上。hook 存储状态的思想和 redux 很相近,当 setState 调用后,会将本次任务放到 nextUnitOfWork 上,通过 workLoop,在空闲时机执行。首先我们会通过 Fiber 的 alternate 获取到上次内容,如果存在则获取对应 Hook。每次调用 Hook 会将处理函数存放至对应 Fiber 中 hooks 对应 queue 中,每调用一次会更新 hookIndex。代码片段示例如下:
function useState(initial) {const oldHook =wipFiber.alternate &&wipFiber.alternate.hooks &&wipFiber.alternate.hooks[hookIndex];const hook = {state: oldHook ? oldHook.state : initial,queue: [],};const actions = oldHook ? oldHook.queue : [];actions.forEach(action => {hook.state = action(hook.state);});const setState = action => {hook.queue.push(action);wipRoot = {dom: currentRoot.dom,props: currentRoot.props,alternate: currentRoot,};nextUnitOfWork = wipRoot;deletions = [];};wipFiber.hooks.push(hook);hookIndex++;return [hook.state, setState];}
到这里,React 超级体验版就能跑起来了,先贴出完整代码:
function createElement(type, props, ...children) {return {type,props: {...props,children: children.map(child =>typeof child === 'object' ? child : createTextElement(child),),},};}function createTextElement(text) {return {type: 'TEXT_ELEMENT',props: {nodeValue: text,children: [],},};}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);function updateDom(dom, prevProps, nextProps) {//Remove old or changed event listenersObject.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]);});// Remove old propertiesObject.keys(prevProps).filter(isProperty).filter(isGone(prevProps, nextProps)).forEach(name => {dom[name] = '';});// Set new or changed propertiesObject.keys(nextProps).filter(isProperty).filter(isNew(prevProps, nextProps)).forEach(name => {dom[name] = nextProps[name];});// Add event listenersObject.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);}commitWork(fiber.child);commitWork(fiber.sibling);}function commitDeletion(fiber, domParent) {if (fiber.dom) {domParent.removeChild(fiber.dom);} else {commitDeletion(fiber.child, domParent);}}function render(element, container) {wipRoot = {dom: container,props: {children: [element],},alternate: currentRoot,};deletions = [];nextUnitOfWork = wipRoot;}let nextUnitOfWork = null;let currentRoot = null;let wipRoot = null;let deletions = null;function workLoop(deadline) {let shouldYield = false;while (nextUnitOfWork && !shouldYield) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork);shouldYield = deadline.timeRemaining() < 1;}if (!nextUnitOfWork && wipRoot) {commitRoot();}requestIdleCallback(workLoop);}requestIdleCallback(workLoop);function performUnitOfWork(fiber) {const isFunctionComponent = fiber.type instanceof Function;if (isFunctionComponent) {updateFunctionComponent(fiber);} else {updateHostComponent(fiber);}if (fiber.child) {return fiber.child;}let nextFiber = fiber;while (nextFiber) {if (nextFiber.sibling) {return nextFiber.sibling;}nextFiber = nextFiber.parent;}}let wipFiber = null;let hookIndex = null;function updateFunctionComponent(fiber) {wipFiber = fiber;hookIndex = 0;wipFiber.hooks = [];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,queue: [],};const actions = oldHook ? oldHook.queue : [];actions.forEach(action => {hook.state = action(hook.state);});const setState = action => {hook.queue.push(action);wipRoot = {dom: currentRoot.dom,props: currentRoot.props,alternate: currentRoot,};nextUnitOfWork = wipRoot;deletions = [];};wipFiber.hooks.push(hook);hookIndex++;return [hook.state, setState];}function updateHostComponent(fiber) {if (!fiber.dom) {fiber.dom = createDom(fiber);}reconcileChildren(fiber, fiber.props.children);}function reconcileChildren(wipFiber, elements) {let index = 0;let oldFiber = wipFiber.alternate && wipFiber.alternate.child;let prevSibling = null;while (index < elements.length || oldFiber != null) {const element = elements[index];let newFiber = null;const sameType = oldFiber && element && element.type == oldFiber.type;if (sameType) {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: null,parent: wipFiber,alternate: null,effectTag: 'PLACEMENT',};}if (oldFiber && !sameType) {oldFiber.effectTag = 'DELETION';deletions.push(oldFiber);}if (oldFiber) {oldFiber = oldFiber.sibling;}if (index === 0) {wipFiber.child = newFiber;} else if (element) {prevSibling.sibling = newFiber;}prevSibling = newFiber;index++;}}const Didact = {createElement,render,useState,};
使用方式如下:
/** @jsx Didact.createElement */function Counter() {const [state, setState] = Didact.useState(1);return (<h1 onClick={() => setState(c => c + 1)} style="user-select: none">Count: {state}</h1>);}const element = <Counter />;const container = document.getElementById('root');Didact.render(element, container);
这样在每次点击,页面都会更新渲染。
本文代码及主要思路来自于:build your own react