写在前面

本文继续上一节文章,来介绍下剩余的知识,如下:


  • Render和Commit阶段
  • 调和过程
  • 函数组件
  • Hooks

我们接下来的部分就依次介绍下这些知识点。

代码获取

本文涉及到的代码全部上传至码云,需要的同学请在下面地址中获取:

https://gitee.com/XuQianWen/zerocreate_react

Render和Commit阶段

我们在之前完成的代码中其实有一个问题,在workLook()中每次循环调用performUnitOfWork()方法时,我们都会往fiber父节点中添加一个新的dom元素,就像下面的代码:

05【React再造之旅】从零实现一个React(下)_react源码阅读

之前我们也介绍过,自从react引入fiber之后,我们的渲染任务是会被分割成若干个小的任务单元的,每次这些小的任务单元完成后如果有优先级高的任务,浏览器就会打断这些任务单元的执行,而是去执行优先级高的任务,等执行完之后再回来继续从头开始执行这些小的任务单元,所以在浏览器打断的这个过程中,我们在前端页面有时候会看到页面渲染空白、不完整等这样的情况,所以我们接下来优化一下我们之前的代码。

我们先在performUnitOfWork()方法中删除添加新dom元素到fiber上的这段代码,如下:

05【React再造之旅】从零实现一个React(下)_react_02

然后在render()方法中,我们给root fiber去一个别名,叫它wipRoot,然后将其赋值给nextUnitOfWork,代码如下:

let nextUnitOfWork = null;
let wipRoot = null;

function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
}
};
nextUnitOfWork = wipRoot;
}

接下来,如果"下一个任务单元"没有任何指向的时候就说明我们完成了所有的工作,所以在此时我们将整个fiber树提交给DOM,这就是渲染和提交阶段的一个简单介绍,代码如下:

let nextUnitOfWork = null;
let wipRoot = null;

function commitRoot() {

}

function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
}
};
nextUnitOfWork = wipRoot;
}
function workLoop(deadline) {
let shouldYield = false;
while(nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}

if(!nextUnitOfWork && wipRoot) {
commitRoot();
}

requestIdleCallback(workLoop);
}

接下来完善一下commitRoot()方法,在此处我们递归地将所有元素添加至dom,代码如下:

function commitRoot() {
commitWork(wipRoot.child);
wipRoot = null;
}

function commitWork(fiber) {
if(!fiber) {
return;
}
const domParent = fiber.parent.dom;
domParent.appendChild(fiber.dom);
commitWork(fiber.child);
commitWork(fiber.sibling);
}

这样一来我们就将开始时所描述的那种情形得到了优化,我们最终会将一整棵fiber树递归到添加到dom中,所以这就避免了渲染中被浏览器打断从而出现页面不完整的问题。

调和过程

到目前为止的话我们仅仅实现了DOM元素的渲染和添加这些过程,如果我们的元素要删除、更新的话应该怎么做呢,这就是接下来要介绍的,也就是调和过程。在此过程中我们需要对比两棵fiber树:render()方法接收的新fiber树和我们最后提交到DOM的旧fiber树。

所以在开始之前我们需要一个引用,用来存放最后一个提交的fiber树,而且还要为每一个fiber元素添加alternate属性,用来连接到旧的fiber上,代码如下:

let nextUnitOfWork = null;
let wipRoot = null;
let currentRoot = null;

function commitRoot() {
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
},
alternate: currentRoot
};
nextUnitOfWork = wipRoot;
}

接下来我们提取performUnitOfWork()方法中创建新fiber的代码片段到一个新的函数中,这个新的函数叫做reconcileChildren()方法,最后这两个方法中的代码如下所示:

function performUnitOfWork(fiber) {
if(!fiber.dom) {
fiber.dom = createDom(fiber);
}

// if(fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom);
// }

const elements = fiber.props.children;
reconcileChildren(fiber, elements);

if(fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while(nextFiber) {
if(nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
function reconcileChildren(wipFiber, elements) {
let index = 0;
let prevSibling = null;

while(index < elements.length) {
const element = elements[index];
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}

if(index == 0) {
wipFiber.child = newFiber;
}else {
prevSibling.sibling = newFiber;
}

prevSibling = newFiber;
index++;
}
}

在reconcileChildren()方法中实现旧fiber和新元素的调和过程,不过在此处我们目前的reconcileChildren()方法是不能直接运行的,接下来还要优化。

我们同时遍历旧fiber的children(wipFiber.alternate)和要协调的元素数组。在此过程中我们忽略掉一些其他的信息之后,其实仅仅关心oldFiber和element。element是我们要添加到DOM的元素,oldFiber是我们最后一次提交渲染过的fiber,通过比较我们可以了解到是否需要对DOM进行更改,所以reconcileChildren()方法中的代码可以暂时优化成这样:

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;

//比较旧fiber和新元素

if(oldFiber) {
oldFiber = oldFiber.sibling;
}
}
}

以上的代码中并没有添加对比的过程,所以接下来我们按如下规则添加对比的代码片段:


  • 如果旧fiber和新元素有相同的类型,我们只需要用新的属性去更新这个dom即可;
  • 如果类型不同,就说明它是一个新元素,所以我们要增加这个新的dom节点;
  • 如果类型不同并且它是一个旧fiber,我们需要删除这个旧的dom节点。

按照上述的规则,我们来编写代码,如下:

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) {
//更新dom
}
if(element && !sameType) {
//添加dom
}
if(oldFiber && !sameType) {
//删除dom
}

if(oldFiber) {
oldFiber = oldFiber.sibling;
}
}
}

在上述过程中,react中同时也用了key,以便有一个更好地调和过程,但在本文中为了简单,我们不做介绍。

对于要更新的dom节点,我们可以这样来做:通过旧的fiber创建一个新的fiber,它的props属性从新的element元素赋值,并且为这个新的fiber添加一个effectTag属性,我们在后期提交阶段来使用,代码如下:

if(sameType) {
//更新dom
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: 'UPDATE',
}
}

对于要添加dom的情况,更上述类似,直接看代码:

if(element && !sameType) {
//添加dom
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: 'PLACEMENT',
}
}

对于要删除的dom节点,我们没有必要再创建一个新的fiber,我们只需要给原来的fiber添加一个effectTag标记即可,但是当我们将fiber树提交给dom的时候它是从正在工作的root fiber中进行的,root fiber并没有旧的fiber,所以我们需要一个数组去存这些要删除的dom节点,所以还需要定义一个数组,代码如下:

if(oldFiber && !sameType) {
//删除dom
oldFiber.effectTag = 'DELETION';
deletions.push(oldFiber);
}
let deletions = null;

function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
},
alternate: currentRoot
};
deletions = [];
nextUnitOfWork = wipRoot;
}

然后,我们将变化后的fiber提交至dom时,我们也要用这个数组中的fiber,所以还需要优化一下commitRoot()方法,代码如下:

function commitRoot() {
deletions.forEach(commitWork);
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}

接下来我们在commitWork()方法中处理一下我们新增的effectTag标签。如果effectTag标签标记的是增加dom,我们的操作还是和原来一样,将这个dom节点添加至父fiber中,代码如下:

function commitWork(fiber) {
if(!fiber) {
return;
}

const domParent = fiber.parent.dom;

if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
domParent.appendChild(fiber.dom);
}

commitWork(fiber.child);
commitWork(fiber.sibling);
}

如果标记为删除dom,我们就将这个dom从它的父fiber中删除,代码如下:

function commitWork(fiber) {
if(!fiber) {
return;
}

const domParent = fiber.parent.dom;

if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
domParent.appendChild(fiber.dom);
}else if(fiber.effectTag === 'DELETION') {
domParent.removeChild(fiber.dom);
}

commitWork(fiber.child);
commitWork(fiber.sibling);
}

如果标记为更新dom,我们就要用目前的元素属性去更新现有的dom节点,所以我们在此处直接调用一个更新节点的函数,这个函数我们稍后定义,代码如下:

function commitWork(fiber) {
if(!fiber) {
return;
}

const domParent = fiber.parent.dom;

if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
domParent.appendChild(fiber.dom);
}else if(fiber.effectTag === 'DELETION') {
domParent.removeChild(fiber.dom);
}else if(fiber.effectTag === 'UPDATE' && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}

commitWork(fiber.child);
commitWork(fiber.sibling);
}

接下来定义updateDom()这个方法,要实现这个方法,我们其实是在做新旧fiber的对比操作,进而去删除没用的属性或者更新、设置改变后的属性。所以我们在定义updateDom()方法之前还要定以几个额外的方法来辅助我们进行判断,进而在updateDom()方法中来实现属性删除和更新操作,如下:

const isProperty = key => key != 'children';
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(isProperty).filter(isGone(prevProps, nextProps)).forEach(name => {
dom[name] = '';
});

//设置新属性或者改变属性
Object.keys(nextProps).filter(isProperty).filter(isNew(prevProps, nextProps)).forEach(name => {
dom[name] = nextProps[name];
});
}

上述代码在处理属性的时候,我们其实还遗漏了节点上挂载的事件,所以我们要继续优化一下前面几个辅助判断的方法,对前缀是"on"的属性我们要做特殊处理,代码如下:

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);

进而,我们还需要在updateDom()方法中做一下优化,最后代码如下所示:

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]);
});

//删除旧属性
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]);
})
}

到此为止,我们就完成了调和过程的介绍,其实调和就是在做dom节点的更新和删除等操作,对应到我们的代码中的话,它其实就是对新旧fiber进行的操作,我们现在保存代码在前端查看时,可以看到原来的输出,代码也并没有任何报错。我们改变一下之前的JSX编写的组件,为其添加一个href属性,我们在前端页面可以看到它是相应的进行了更新,并且这个超链接也是工作正常的,如下:

/** @jsx XbcbLib.createElement */
const element = (
<div id='xbcb'>
<a href="http://www.xbcb.top">X北辰北</a>
<br />
</div>
);

05【React再造之旅】从零实现一个React(下)_react工作原理_03

到目前为止,所有的index.js文件代码如下,大家可以参考对比一下:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map( value => {
//typeof value == "object" ? value : createTextElement(value)
if(typeof value == 'object') {
return value;
}else {
return createTextElement(value)
}
})
}
}
}
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);

const isProperty = key => key != 'children';
Object.keys(fiber.props).filter(isProperty).forEach(name => {
dom[name] = fiber.props[name];
});

return dom;

// element.props.children.forEach(child => {
// render(child, dom);
// });

// container.appendChild(dom);
}

let nextUnitOfWork = null;
let wipRoot = null;
let currentRoot = null;
let deletions = null;

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]);
});

//删除旧属性
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]);
})
}

function commitRoot() {
deletions.forEach(commitWork);
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}

function commitWork(fiber) {
if(!fiber) {
return;
}

const domParent = fiber.parent.dom;

if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
domParent.appendChild(fiber.dom);
}else if(fiber.effectTag === 'DELETION') {
domParent.removeChild(fiber.dom);
}else if(fiber.effectTag === 'UPDATE' && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}

commitWork(fiber.child);
commitWork(fiber.sibling);
}

function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
},
alternate: currentRoot
};
deletions = [];
nextUnitOfWork = wipRoot;
}

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) {
if(!fiber.dom) {
fiber.dom = createDom(fiber);
}

// if(fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom);
// }

const elements = fiber.props.children;
reconcileChildren(fiber, elements);

if(fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while(nextFiber) {
if(nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}

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) {
//更新dom
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: 'UPDATE',
}
}
if(element && !sameType) {
//添加dom
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: 'PLACEMENT',
}
}
if(oldFiber && !sameType) {
//删除dom
oldFiber.effectTag = 'DELETION';
deletions.push(oldFiber);
}

if(oldFiber) {
oldFiber = oldFiber.sibling;
}

if(index == 0) {
wipFiber.child = newFiber;
}else {
prevSibling.sibling = newFiber;
}

prevSibling = newFiber;
index++;
}
}

const XbcbLib = {
createElement,
render
};

/** @jsx XbcbLib.createElement */
const element = (
<div id='xbcb'>
<a href="http://www.xbcb.top">X北辰北</a>
<br />
</div>
);

const container = document.getElementById('root');
XbcbLib.render(element, container);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

函数组件

介绍完以上部分之后,我们这部分内容介绍一下函数组件。因为目前我们添加的JSX语法组件都是正常的HTML标记,并不是自定义的组件,所以接下来我们继续优化我们的项目,使其能够支持函数组件。

我们先改写原来编写的element组件代码,让它变成一个函数组件,如下:

/** @jsx XbcbLib.createElement */
function App(props) {
return <h1>Hi, {props.name}</h1>;
}

const element = <App name="X北辰北" />;
const container = document.getElementById('root');
XbcbLib.render(element, container);

我们这时候直接保存代码的时候,前端页面会报错,因为目前代码中并不支持函数组件渲染。但是我们知道,如果此时将这个函数组件的JSX向JS转换的时候,它应该会做以下的转变:

/** @jsx XbcbLib.createElement */
function App(props) {
return XbcbLib.createElement(
'h1',
null,
'Hi',
props.name
)
}

const element = XbcbLib.createElement(App, {
name: 'X北辰北',
});

在开始之前我们要知道两点:


  • 函数组件的fiber没有DOM节点
  • children属性并不是直接来自于props,而是来自于函数的调用

所以我们要对之前的performUnitOfWork()方法做一个优化,在它里面对fiber的类型做一下判断,然后再决定使用不同的更新方法来进行fiber的更新调和操作。如果是函数组件,我们使用函数组件的更新方法,如果不是函数组件,我们使用原来的更新方法,代码如下:

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;
}
}

function updateFunctionComponent(fiber) {

}

function updateHostComponent(fiber) {
if(!fiber.dom) {
fiber.dom = createDom(fiber);
}

const elements = fiber.props.children;
reconcileChildren(fiber, elements);
}

在函数组件的更新方法中,我们主要是去获取children属性。比如在我们的示例代码中,它的fiber.type就是一个App函数,所以我们调用它之后会返回一个h1的dom元素。如果我们拿到了children属性,那接下来的过程就是调和了,调和的过程跟我们之前的代码是没有任何差别的,代码如下:

function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}

因为有了没有dom节点的fiber树,所以我们要更改一下commitWork()方法。在这里我们主要改两部分,第一部分就是我们首先要找到dom节点的父节点,我们需要沿着fiber树一直往上找,直到找到带有dom节点的fiber为止,所以要修改的第一部分代码如下:

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 === 'DELETION') {
domParent.removeChild(fiber.dom);
}else if(fiber.effectTag === 'UPDATE' && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}

commitWork(fiber.child);
commitWork(fiber.sibling);
}

第二部分就是节点删除部分,我们需要找到具有dom节点的子节点为止,代码如下:

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 === 'DELETION') {
//domParent.removeChild(fiber.dom);
commitDeletion(fiber, domParent);
}else if(fiber.effectTag === 'UPDATE' && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}

commitWork(fiber.child);
commitWork(fiber.sibling);
}

function commitDeletion(fiber, domParent) {
if(fiber.dom) {
domParent.removeChild(fiber.dom);
}else {
commitDeletion(fiber.child, domParent);
}
}

至此为止我们就完成了函数组件的支持,我们定义一个组件,然后将其渲染到页面上,如下:

/** @jsx XbcbLib.createElement */
function AppFunction(props) {
return <h1>Hi, {props.name}</h1>;
}

const element = <AppFunction name="X北辰北" />;
const container = document.getElementById('root');
XbcbLib.render(element, container);

05【React再造之旅】从零实现一个React(下)_react_04

Hooks

我们自己的react目前已经支持函数组件,但是还缺少state的支持,所以接下来我们看看如何添加state的支持。在此处我们使用hooks来维护函数组件中的state。所以我们先改写一下示例代码,就用最经典的计数器例子,每次点击的时候它的次数会增加1,代码如下:

const XbcbLib = {
createElement,
render,
useState,
};

/** @jsx XbcbLib.createElement */
function AppFunction(props) {
const [state, setState] = XbcbLib.useState(1);
return (
<h1 onClick={() => setState(c => c + 1)}>
H1, {props.name}。你点击的次数为{state}。
</h1>
)
}

const element = <AppFunction name="X北辰北" />;
const container = document.getElementById('root');
XbcbLib.render(element, container);

然后定义useState()方法,并且在定义此方法之前我们还需要定义一些全局变量,以便后续在此方法中使用,各个变量的初始化工作在函数组件更新的方法中完成,如下:

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) {

}

上述代码中我们将work设置为进行中的fiber,同时还向fiber增加了一个hooks数组,以便于支持在同一组件中多次调用useState()。同时我们跟踪当前的hook索引。

当函数组件调用useState()时我们检查它是否有旧的hook。用hook索引去检查fiber的alternate属性。如果有旧的hook,我们将state从旧的hook复制到新的hook,否则我们将初始化state。然后将新的hook添加到fiber,并且将hook索引增加1之后返回state,代码如下:

function useState(initial) {
const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : initial,
}

wipFiber.hooks.push(hook);
hookIndex++;
return [hook.state];
}

此时我们保存代码后可以在前端页面看到预期的效果,但是当我们点击时并没有任何反应,这是因为useState()中还需要返回一个函数去更新state,所以我们要在此方法里面定义一个setState()函数来接收一个操作,我们将这个操作放到一个队列中,然后就执行与渲染过程中类似的操作,将新的进行中的工作单元设置为下一个工作单元,以便可以循环进行新的渲染阶段,代码如下:

function useState(initial) {
const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
}

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];
}

但是目前我们还不能运行上述代码中的action操作,我们是在下一次渲染组件时运行这些的,首先是从旧的hook队列中拿到所有的action,然后将它们逐一应用到新的hook中的state上,所以我们会在它更新完成后返回state,代码如下:

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,点击效果如下:

05【React再造之旅】从零实现一个React(下)_从零实现一个React_05

结尾

这篇文章仅仅是帮助我们了解react的工作流程,同时也是为我们后期阅读react源码做了铺垫,所以在我们的代码里使用了和react中同样名称的变量和方法。但是在我们的代码中没有包括很多React的功能和优化。例如,我们可以看看react中有些操作它是怎么做的:




  • 在XbcbLib中,我们在渲染阶段遍历整棵树。相反,React遵循一些提示和试探法,以跳过没有任何更改的整个子树。
  • 我们还在提交阶段遍历整棵树。React仅保留有影响的fiber并仅访问那些fiber的链表。
  • 每次我们建立一个新的进行中的工作树时,都会为每个fiber创建新的对象。React回收了先前树中的fiber。
  • 当XbcbLib在渲染阶段收到新的更新时,它将丢弃进行中的工作树,然后从根开始重新进行。React使用过期时间戳标记每个更新,并使用它来决定哪个更新具有更高的优先级。
  • 类似的还有很多…

你自己也可以添加如下的功能:


  • 使用对象作为样式属性
  • 展平子数组
  • useEffect hook
  • 密钥对帐