上面的代码虽然我们支持了多个useState,但是仍然只有一套全局变量,如果有多个函数组件,每个组件都来操作这个全局变量,那相互之间不就是污染了数据了吗?所以我们数据还不能都存在全局变量上面,而是应该存在每个fiber节点上,处理这个节点的时候再将状态放到全局变量用来通讯:
// 申明两个全局变量,用来处理useState // wipFiber是当前的函数组件fiber节点 // hookIndex是当前函数组件内部useState状态计数 let wipFiber = null; let hookIndex = null;因为useState只在函数组件里面可以用,所以我们之前的updateFunctionComponent里面需要初始化处理useState变量:
function updateFunctionComponent(fiber) { // 支持useState,初始化变量 wipFiber = fiber; hookIndex = 0; wipFiber.hooks = []; // hooks用来存储具体的state序列 // ......下面代码省略...... }因为hooks队列放到fiber节点上去了,所以我们在useState取之前的值时需要从fiber.alternate上取,完整代码如下:
function useState(init) { // 取出上次的Hook const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]; // hook数据结构 const hook = { state: oldHook ? oldHook.state : init // state是每个具体的值 } // 将所有useState调用按照顺序存到fiber节点上 wipFiber.hooks.push(hook); hookIndex++; // 修改state的方法 const setState = value => { hook.state = value; // 只要修改了state,我们就需要重新处理这个节点 workInProgressRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot } // 修改nextUnitOfWork指向workInProgressRoot,这样下次requestIdleCallback就会处理这个节点了 nextUnitOfWork = workInProgressRoot; deletions = []; } return [hook.state, setState] }上面代码可以看出我们在将useState和存储的state进行匹配的时候是用的useState的调用顺序匹配state的下标,如果这个下标匹配不上了,state就错了,所以React里面不能出现这样的代码:
if (something) { const [state, setState] = useState(1); }上述代码不能保证每次something都满足,可能导致useState这次render执行了,下次又没执行,这样新老节点的下标就匹配不上了,对于这种代码,React会直接报错:
用Hooks模拟Class组件这个功能纯粹是娱乐性功能,通过前面实现的Hooks来模拟实现Class组件,这个并不是React官方的实现方式哈~我们可以写一个方法将Class组件转化为前面的函数组件:
function transfer(Component) { return function(props) { const component = new Component(props); let [state, setState] = useState(component.state); component.props = props; component.state = state; component.setState = setState; return component.render(); } }然后就可以写Class了,这个Class长得很像我们在React里面写的Class,有state,setState和render:
import React from './myReact'; class Count4 { constructor(props) { this.props = props; this.state = { count: 1 } } onClickHandler = () => { this.setState({ count: this.state.count + 1 }) } render() { return ( <div> <h3>Class component Count: {this.state.count}</h3> <button onClick={this.onClickHandler}>Count+1</button> </div> ); } } // export的时候用transfer包装下 export default React.transfer(Count4);然后使用的时候直接:
<div> <Count4></Count4> </div>当然你也可以在React里面建一个空的class Component,让Count4继承他,这样就更像了。
好了,到这里我们代码就写完了,完整代码可以看我GitHub。
总结我们写的JSX代码被babel转化成了React.createElement。
React.createElement返回的其实就是虚拟DOM结构。
ReactDOM.render方法是将虚拟DOM渲染到页面的。
虚拟DOM的调和和渲染可以简单粗暴的递归,但是这个过程是同步的,如果需要处理的节点过多,可能会阻塞用户输入和动画播放,造成卡顿。
Fiber是16.x引入的新特性,用处是将同步的调和变成异步的。
Fiber改造了虚拟DOM的结构,具有父 -> 第一个子,子 -> 兄,子 -> 父这几个指针,有了这几个指针,可以从任意一个Fiber节点找到其他节点。
Fiber将整棵树的同步任务拆分成了每个节点可以单独执行的异步执行结构。
Fiber可以从任意一个节点开始遍历,遍历是深度优先遍历,顺序是父 -> 子 -> 兄 -> 父,也就是从上往下,从左往右。
Fiber的调和阶段可以是异步的小任务,但是提交阶段(commit)必须是同步的。因为异步的commit可能让用户看到节点一个一个接连出现,体验不好。
函数组件其实就是这个节点的type是个函数,直接将type拿来运行就可以得到虚拟DOM。
useState是在Fiber节点上添加了一个数组,数组里面的每个值对应了一个useState,useState调用顺序必须和这个数组下标匹配,不然会报错。
参考资料A Cartoon Intro to Fiber
妙味课堂大圣老师:手写react的fiber和hooks架构
React Fiber
这可能是最通俗的 React Fiber(时间分片) 打开方式
浅析 React Fiber
React Fiber架构