打造你自己的React(翻译)

特别声明:该翻译仅供自己参考。如有兴趣请阅读原文。原文地址

我们将依照React的架构,构造一个精简版的React,主要包括下面的内容:

  1. createElement函数
  2. render函数
  3. Concurrent Mode(?并发模式)
  4. Fibers
  5. 渲染和提交(Render and Commit Phases)
  6. Reconciliation(?对比)
  7. 函数式组件
  8. Hooks

首先,一些基础理论

让我们先来回顾一些基础理论。如果你对React,JSX,DOM很熟悉,你可以跳过这一小节。

const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)

一个三行代码的简单的React程序,第一行定义了一个React元素,第二行拿到了作为容器的Dom元素,第三行把React元素渲染到容器里,也就是渲染到真实Dom里。

接下来,让我们用纯JS代码替换掉所有React相关的代码。

上面第一行代码,使用了JSX。不是JS,需要替换掉。 JSX转换为JS通常是靠Babel这类工具,做的事也很简单,将JSX代码用createElement函数替换掉。 比如上面的代码替换完了就是

React.createElement(
  "h1",
  {title:"foo"},
  "hello",
)

除了一些验证以外createElement从它的参数里创建出一个对象。所以我们可以暂时用返回的对象替换掉这个函数。

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}//这个就是由createElement创建的对象
const container = document.getElementById("root")
ReactDOM.render(element, container)

这就是createElement返回的对象,拥有type和props两个属性(我们暂时只关注这两个,其它属性后面再说)。

type是一个string类型,指代一个你想创建的真实Dom类型,也就是你创建HTML元素时,传递给document.createElement的tagName。但是type也可能时一个function,我们放到后面在说。 props则是一个对象,它拥有所以JSX中定义的属性,以及一个特殊的属性children。在上述列子中,children只是一个string,但通常情况下,它是一个代表着更多元素的array,这也是为什么elements是树形结构的。

剩下需要替换的是ReactDOM.render。这个函数是React渲染真实Dom的地方。让先暂时用浏览器原生API手动实现一下。

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}

const container = document.getElementById("root")

const node = document.createElement(element.type)
node["title"] = element.props.title

const text = document.createTextNode("")
text["nodeValue"] = element.props.children

node.appendChild(text)
container.appendChild(node)

首先我们根据type创建真实的DOM节点,上面的例子中就是H1。然后我们把所有属性添加上,例子中就只是title。 ~~为了避免歧义,下面提到node就代表真实的Dom元素,而element代表React元素。~~

image.png
image.png
{
	type : "TEXT_ELEMENT",
  props : {
  	nodeValue : "Hello"
  }
}

最后,我们只需要将textNode添加到h1,将h1添加到container容器里,然后就完了。现在我们拥有了一个和之前功能一样的App,而且没有使用React。

Step1: createElement函数

const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

我们从另一个简单的ReactApp开始。这次我们将用自己写的React替换掉React的代码。 从实现createElement开始,把JSX转换为JS,然后我们就能看到craeteElement内部到底发生了什么。

正如上面提到过的,createElement返回一个拥有type 和 props属性的对象。所以我们只需要写一个返回这个对象的函数即可。

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children,
    },
  }
}

我们使用rest parameter syntax*,来保证_prop.children_总是一个_array。 * 比如createElement('div'),createElement('div',null,a),createElement('div',null,a,b)。返回

{
  type : "div",
  props : {
  	children : []
  }  
}//createElement('div')

{
  type : "div",
  props : {
  	children : [a]
  }  
}//createElement('div',null,a)

{
  type : "div",
  props : {
  	children : [a,b]
  }  
}//createElement('div',null,a,b)

当children中包含文本节点时,children中可能包含原始值string类型。我们需要对原始值进行单独处理,为文本节点创建一个单独的类型TEXT_ELEMENT。

需要注意的时,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: []
    }
  };
}

现在有了我们自己的createElement函数,可以把React.createElement替换掉了,首先得取一个名字,Didact。

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

const Didact = {
  createElement,
}

const element = Didact.createElement(
  "div",
  { id: "foo" },
  Didact.createElement("a", null, "bar"),
  Didact.createElement("b")
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

上面的代码看起来仍然没有JSX直观,我们仍想使用JSX。可不可以让Babel直接翻译成我们自己写的createElement函数,而不是React的呢?答案是可以的。

/** @jsx Didact.createElement */
const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)

我们可以使用@jsx标记,这样babel遇到jsx语法就会调用我们的Didact.createElement。

Step2: render函数

现在来实现ReactDOM.render函数。让我们暂时只关心增加dom元素,稍后再处理更新和删除。

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 render(element, container) {
	const dom = element.type == "TEXT_ELEMENT"
  			?document.createTextNode("")
        :document.createElement(element.type);
  const isProperty = key=>key!=="children";
  Object.keys(element.props)
  	.filter(isProperty)
  	.forEach(name=>{
  		dom[name] = element.props[name];
  	});
  
  element.props.children.forEach(child=>render(child,dom));

  container.appendChild(dom)
}

const Didact = {
  createElement,
  render,
}

/** @jsx Didact.createElement */
const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)
const container = document.getElementById("root")
Didact.render(element, container)

render函数接收两个参数,element是JSX翻译过后产生的对象树,container是dom元素需要挂载的节点或者说容器。 render函数做的事如下: 1.render函数首先根据element的type值创建出对应的DOM元素。 2.然后设置props里除了children外的所有属性到DOM元素上。 3.遍历children元素,依次调用render函数,并把当前元素的DOM作为container传递进去。 4.添加DOM元素到container。

现在我们有了一个能把JSX渲染为DOM的库了。代码地址

Step3: Concurrent Mode(并发模式)

在开始下一步之前,还需要处理一个问题。如果元素嵌套(元素树)很深,我们的代码开始渲染过后,就不会停下来,直到渲染完毕才会去做其他事。这样就可能导致阻赛主线程过久,进而导致浏览器无法响应用户的操作、流畅的绘制动画。所以需要重构一下代码。

我们将把渲染工作分成许多更小的单元,每当完成一个单元的渲染,让浏览器能打断渲染去处理一些其它必要的事,然后再继续渲染。

let nextUnitOfWork = null

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

我们使用requestIdleCallback来制造循环,你可以把requestIdleCallback当成setTimeout,但是需要注意,不是我们决定什么时候去执行回调函数,而是浏览器空闲(IDLE)的时候会去主动调用。 React不使用requestIdleCallback ,现在使用scheduler package。但是在理论上,对于我们现在的情况下,他们都是一样的。 requestIdleCallback有一个deadline参数,它代表距离下一次浏览器获得控制权的时间。

为了让循环能跑起来,我们需要手动设置第一个单元的渲染工作。然后实现一个performUnitOfWork函数,该函数不仅执行渲染而且返回下一个单元的渲染工作。

Step4: Fibers

为了组织我们的单元/渲染工作,我们需要一个数据结构:fiber tree。 我们会给每一个元素设置一个fiber,每一个fiber将代表一个单元的渲染工作。 看一个例子: 假如我们渲染一个元素树结构如下:

Didact.render(
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>,
  container
)
image.png
image.png

在render函数里,我们会创建root fiber,并把它设置为nextUnitOfWork。剩下的工作将由performUnitOfWork来实现,我们将要做三件事:

  1. 添加element到dom。
  2. 为element的子元素创建fibers。
  3. 确定下一个unit of work。

注:这里遍历树,采用的是先序遍历

每个fiber都存有对first child,next sibling, parent的链接。

当我们完成一个fiber,它的child的fiber会成为下一个nextUnitOfWork。 在我们例子中,当我们完成div的fiber,下一个渲染的就是h1。

如果fiber没有child,我们会使用sibling(兄弟元素)作为下一个nextUnitOfWork。 比如,上述p元素没有child,则下一个fiber就是a元素。

如果fiber没有child和sibling,我们将把“uncle”:the sibling of the parent 作为下一个fiber,像上述例子中的a 和 h2。如果parent也没有sibling,我们继续往上查找parent's,直到我们找到一个带有sibling的或者直到我们达到root节点。如果我们达到了root节点,也就意味着我们完成了所有渲染工作。

代码实现如下: 首先,把render函数原有内容提取成createDom成为一个单独的函数。

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
}

function render(element, container) {
  // TODO set next unit of work
}

在render函数,设置第一个nextUnitWork,也就是root fiber。

let nextUnitWork = null;

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

然后实现performUnitOfWork。再确认一次,这个函数做了以下三件事

  1. 添加element到dom。
  2. 为element的子元素创建fibers。
  3. 确定下一个unit of work。
function performUnitOfWork(fiber) {
  //创建dom,并储存在fiber.dom上
  if(!fiber.dom){
		fiber.dom = createDom(fiber);
	}
  //添加dom到父元素
  if(fiber.parent){
  	fiber.parent.dom.appendChild(fiber.dom);
  }
  
  //生成子元素的fiber并关联
  const elements = fiber.props.children;
  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){
    	fiber.child = newFiber;
    }else{
    	prevSibling.sibling = newFiber
    }
    prevSibling = newFiber;
    index++;
  }
  
  //返回fiber的child
  if(fiber.child){
  	return fiber.child
  }
  //返回sibling或者上一级的sibling
  let nextFiber = fiber;
  while(nextFiber){
  	if(nextFiber.sibling){
    	return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent
  }
}

Step1-4代码地址

Step5: 渲染和提交阶段

现在我们有另一个问题了。我们处理fiber的时候,把生成的node实际添加到了dom里。而且,请记住,在我们渲染完整个dom树之前,浏览器可以随时打断我们的工作,那就意味着,用户可能看到一个不完整的UI,我们需要避免这个问题。

因此,我们需要先删除更改DOM的部分。然后用一个新变量wipRoot来保存对fiber tree的追踪。 然后,当我们完成所有fiber的dom节点创造过后,我们再统一提交到真正的dom上。我们在commitRoot里做这件事,遍历fiber树,把节点提交到真实dom上。

//这段代码需要删除
 if(fiber.parent){
  fiber.parent.dom.appendChild(fiber.dom);
 }

//新变量
let wibRoot = null;

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



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

function workLoop(deadLine) {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadLine.timeRemaining() < 1;
  }

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

Step6:Reconciliation

目前为止,我们只是往DOM里添加,还没有处理更新和删除,接下来我们就处理这个。我们需要把render函数收到的elemets与已有的fiber tree进行比对。所以我们需要一个变量来保存上一次提交到DOM树的fiber tree,currentRoot。我们还需要在每个fiber上增加一个alternate属性,值为上一次提交阶段的老fiber。

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


我们先来把创建new fiber的代码提取出来,单独成为一个函数reconcileChildren。~~我们在这个函数里协调旧的fibers 和 新的 elements。~~Here we will reconcile the old fibers with the new elements.

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
  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: wipFiber,
      dom: null,
    }

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

    prevSibling = newFiber
    index++
  }
}

我们在reconcileChildren函数里,遍历元素时,同时遍历old fiber。除开遍历和取值的模版代码,我们需要关心的是old fiber 和 当前的element。对比出它们两个之间的差异。

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;
    //todo compare oldFiber to element
		if(oldFiber){
    	oldFiber = oldFiber.sibling;
    }
    if (index === 0) {
      wipFiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    index++
  }
}

我们使用type来进行比对。

  1. old fiber 和 element 的 type值一样,我们继续使用原有DOM,只对它进行props的更新。
  2. old fiber 和 element 的 type值不一样,且element存在,old fiber不存在,我们将创建一个新的DOM。
  3. old fiber 和 element 的 type值不一样,且element不存在,old fiber存在,我们将删除旧的DOM。

React使用keys来进行更好的比对。例如它能检测到列表中元素顺序的变动。(不理解)

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) {
      // TODO update the node
    }
    if (element && !sameType) {
      // TODO add this node
    }
    if (oldFiber && !sameType) {
      // TODO delete the oldFiber's node
    }
		if(oldFiber){
    	oldFiber = oldFiber.sibling;
    }
    if (index === 0) {
      wipFiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    index++
  }
}

当old fiber type和 element type一样。我们创建一个new fiber,但是dom元素沿用old fiber的,props则更新为element的。与此同时,我们需要添加一个effectTag,我们会在提交阶段用到这个。

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

对于第二种情况,我们需要新创建DOM,并打上一个PLACEMENT effectTag。

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

对于第三种情况,我们需要删除DOM。但是并不存在一个新的fiber,所以我们需要在老的fiber上打上标记。 然后还有一个问题是,我们提交阶段使用的是wipRoot,但是该变量上并不存在old fibers。所以我们需要一个额外的数组来保存我们需要删除的old fibers。最后在提交阶段使用这个数组去做删除工作。

let deletions = null;

function render(){
	...
  deletions = [];
  ...
}

function commitRoot(){
	deletions.forEach(commitWork);
  ...
}


if (oldFiber && !sameType) {
  oldFiber.effectTag = "DELETION"
  deletions.push(oldFiber)
}

接下来,我们来更改commitWork,使之能处理effectTags。 如果fiber有一个PLACEMENT effectTag,我们添加一个新dom。

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

如果fiber有一个DELETION effectTag,我们执行删除操作。

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 === "PLACEMENT"){
		domParent.removeChild(fiber.dom)
	}
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

最后,如果是UPDATE effectTag,我们需要用新的props更新存在的DOM节点。我们将在updateDom里做属性更新。

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 === "UPDATE" &&
    fiber.dom != null
  ) {
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  }else if(fiber.effectTag === "PLACEMENT"){
		domParent.removeChild(fiber.dom)
	}
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

function updateDom(){
	// TODO
}

我们比对新老props,移除已经不存在的props,添加新的prop,更新都有的prop。

还需要处理一类特殊的属性,事件监听器listener,所以如果prop 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)
function updateDom(dom, prevProps, nextProps) {
  //Remove old or changed event listeners
  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]
      )
    })
  // Remove old properties
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach(name => {
      dom[name] = ""
    })
  // Set new or changed properties
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      dom[name] = nextProps[name]
    })
  // Add event listeners
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.addEventListener(
        eventType,
        nextProps[name]
      )
    })
}

STEP1-6代码地址

Step7:函数组件

我们接下来要实现函数组件,首先我们更改demo为简单的函数组件。

function App(props) {
  return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
Didact.render(element, container)

如果把JSX写成JS是这样的

function App(props) {
  return Didact.createElement(
    "h1",
    null,
    "Hi ",
    props.name
  )
}
//注意这个APP,不是一个tag了,是一个函数了
const element = Didact.createElement(App, {
  name: "foo",
})

函数组件在两个方面有不同:

我们需要修改performUnitOfWork,在函数内判断当前fiber的type是不是function,来走不同的 update function。 在updateHostComponent我们保持和之前一致。 在updateFunctionComponent我们运行函数并得到children元素。

function performUnitOfWork(fiber){
	...
  const isFunctionComponent =
    fiber.type instanceof Function
  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }
	...
}  

function updateFunctionComponent(fiber) {
  // TODO
}

function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
  reconcileChildren(fiber, fiber.props.children)
}

在我们的列子中,fiber.type 是App函数,当我们执行App函数,它返回H1元素。当函数执行完毕,拿到children我们就可以按之前的流程继续执行下去了。

然后我们需要更改一下commitwork函数,因为现在有没有DOM节点的fiber存在。需要做两件事如下: 1.我们需要向上查找父级元素,直到找到一个拥有DOM节点的fiber。 2.当我们删除元素的时候,也需要向下查找到一个带有DOM节点的子元素进行删除。

function commitWork(fiber){
  ...
  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") {
    commitDeletion(fiber, domParent)
  }
  ...
}
  
 function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom)
  } else {
    commitDeletion(fiber.child, domParent)
  }
}

STEP1-7代码地址

Step8:hooks

最后,让我们实现一下state。

先改造一下demo,经典的counter。

const Didact = {
  createElement,
  render,
  useState,
}

function useState(){
	// TODO
}

function Counter() {
  const [state, setState] = Didact.useState(1)
  return (
    <h1 onClick={() => setState(c => c + 1)}>
      Count: {state}
    </h1>
  )
}
const element = <Counter />;

我们是在updateFunctionComponent里执行Counter函数的,现在我们还需要在这调用useState。

我们需要一些额外的全局的变量,wipFIber,hookIndex,以便在useState里使用。我们首先设置wipFiber为当前处理的fiber。然后我们需要在fiber添加一个hooks数组,储存在同一个组件中多次调用useState产生的hook。hookIndex按调用顺序一一对应上useState。

let wipFiber = null;
let hookIndex = null;

function updateFunctionComponent(fiber){
	wipFiber = fiber;
  //每执行一个函数组件前,就重置index,保证顺序对应。
  hookIndex = 0;
  wipFiber.hooks = [];
  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children)
}

function useState(initial){
	//TODO
}

当函数组件内调用useState时,我们在wipFiber的alternate的hooks用hookIndex检查是否含有一个老的hook存在。如果存在就赋值给新hook,不存在就使用初始值。然后把新hook添加到wibFiber的hooks数组里。最后把新hook的值返回出去。

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

useState还应该返回一个setState函数去更新state的值。这个setState接收的参数为暂时称之为action。我们需要把这个action放在hook里的quene存起来。然后模仿render函数,生成一个新的nextUnitOfWork,让数据能及时更新到页面上。

function useState(initial){
	const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];
  const hook = {
  	state : oldHook ? oldHook.state : initial;
  	queue: []
  }
  wibFiber.hooks.push(hook);
  hookIndex++;
  const setState = (action)=>{
  	hook.queue.push(action);
    wipRoot = {
    	dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    nextUnitOfWork = wipRoot;
    deletions = [];
  }
  
  return [hook.state,setState];
}

但是我们现在还没有执行action(作者默认action为函数)。 我们在下一次更新组件的时候执行它,我们从老hook中的queue中取出所有actions。然后依次执行,赋值给当前hook的state。这样返回出去的就是最新的state了。

STEP1-8代码地址

使用 Discussions 讨论 Github 上编辑