React 浅析

273 阅读7分钟

React是一个函数

构建页面应用

function App() {
  return (
    <div>
      App
    </div>
  );
}

调用<App/>, 返回一个嵌套对象Object(Fiber节点),对象的构成如下所示:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // Fiber
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  // Effects
  this.effectTag = NoEffect;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  this.expirationTime = NoWork;
  this.childExpirationTime = NoWork;

  this.alternate = null;

  // ... ... 省略其他属性 ... ... //
}

通过sibling、child构建树结构,也就是currentTree

ReactDOM将对象节点,渲染到页面上

ReactDOM.render(<App />, 'root');

如何渲染的呢?具体React中通过函数legacyRenderSubtreeIntoContainer,将树结构渲染到页面上。

如何渲染的,这个过程是可以猜测的,无非就是对对象进行遍历解析,然后进行dom更新操作,所以没必要进行深究。如果仅仅到这里,React还不能算是一个完整的框架,因为这只是渲染一个静态的html,没有动态的交互,那么React是如何处理更新操作的?

React通过this.setState/hooks中setState进行更新操作。

所以到这里,我们可以看一下React的整个机制。

在动态更新中,无非对html需要做的就是增加、更新、删除等操作,所以最重要的是途中标红的区域做了什么?

React Fiber的更新逻辑

首先说明下,为什么会有两个Tree:current和workInProcessTree,current就是当前渲染在界面上的FiberTree,workInProcessTree是接下来进行渲染的tree,当更新结束,workInprocessTree就变成currentTree。从程序的角度来讲,当你更新一个变量,就需要存储一个中间变量,然后优化下计算过程,最后再更新,workInProcressTree就是这个中间变量。

我们看一下Fiber内部是如何进行更新的。先直接上图,然后再慢慢解释。

  • step1: 更新进入一个while循环,执行当前更新单元
  • step2: 更新单元,执行更新beginWork,判断当前节点的类型,执行不同的更新: 1)如果是类组件,先从getStateFromUpdate中获得最新的state,然后执行类组件的render函数,返回当前节点;
// getStateFromUpdate
// prevState就是workInProgress中的state
// 这就是为什么setState中是一个函数,会返回最新的,因为取的tree是workInprocessTree中的state,而workInProcessTree是最新的state

if (typeof payload === 'function') {
  if (__DEV__) {
    enterDisallowedContextReadInDEV();
    if (
      debugRenderPhaseSideEffectsForStrictMode &&
      workInProgress.mode & StrictMode
    ) {
      payload.call(instance, prevState, nextProps);
    }
  }
  const nextState = payload.call(instance, prevState, nextProps);
  if (__DEV__) {
    exitDisallowedContextReadInDEV();
  }
  return nextState;
}

2)如果是函数组件,就执行对应的函数,如果遇到hooks就执行updateHooks 3)... ... 执行结束后,返回当前最新节点,newChild

  • step3: 对节点,进行打标签,决定是更新还是删除,还是新增,这些例子通过effectList链表的形式存在(链表的形式存在)
  • step4: 完成工作,返回下一个节点。

到这里,reactFiber的更新逻辑基本上讲清楚了,但是还存在几个小地方没讲清楚,也是我们经常面临的几个问题。

  • 1、react的生命周期是如何执行的?

    • a、class组件的生命周期是写在class的原型上的
    • b、要执行生命周期,需要在不同阶段执行实例(也就是Fiber节点)的原型方法
    • c、在初始生成的时候,执行componentDidMount
    • d、在更新的时候,执行shouldComponentUpdate、componentDidMount等
    • e、在删除节点的时候,执行componentWillUnMount
    • f、还有其他生命周期,这里就不做赘述了
  • 2、多次执行一个更新,内部机制是怎么运行的 比如:在class组件中执行两次setState

state = {
  count: 0
};
// click中执行如下代码
this.setState({
  count: this.state.count + 1
}, () => {
  console.log('first update');
});
this.setState({
  count: this.state.count + 1
}, () => {
  console.log('second update');
});

最终结果:

count: 1

原理解释: 1、在click中的函数执行是batchUpdate的,所以执行的时候,拿到的this,还是前一个节点的current,所以当时的count是0,无论多少次都是一样的。并不是之前的合并的概念,是每次都会执行,当具有对应回调的时候,会执行两次回调。 2、但是,如果是

this.setState(c => c + 1);

这样的,取得就是workInprogressTree,获取的是最新的 3、最后都更新后,执行commit

  • 3、异步代码中执行多个更新?
state = {
  count: 0
};
// 在异步代码中执行

setTimeout(() => {
  this.setState({
    count: this.state.count + 1
  });
  this.setState({
    count: this.state.count + 1
  });
}, 1000);

输出结果:

count: 2

原理解释: 1、异步代码执行的时候,不会批量执行,在每次执行的时候,都会执行commit 2、整体流程就是,第一次update -> commit -> 第二次update -> commit,所以每次更新拿到的this.state都是最新的。

  • 4、如果调用ReactDOM.unstable_batchedUpdates,执行是怎么样的?
state = {
  count: 0
};
// 在异步代码中执行

setTimeout(() => {
  ReactDOM.unstable_batchedUpdates(() => {
    this.setState({
      count: this.state.count + 1
    });
    this.setState({
      count: this.state.count + 1
    });
  })
}, 1000);

输出结果:

count: 1

原理解释: 1、批量更新,流程同click事件的处理

  • 5、hook的内部执行逻辑是怎么样的?

原理解释:

hooks其实是个状态机,触发React更新,然后执行函数,内部再拿到最新的状态

可以通过下面函数进行模拟一个useState函数

function useState(initialState) {
  function* dispatchState() {
    let state = initialState;
    while(true) {
      state = yield state;
    }
  }
  const dispatch = dispatchState();
  const { value, done } = dispatch.next();
  const setState = (newState) => {
    dispatch.next(newState);
  };
  return [value, setState];
}

但是useState被多次调用,会存在一个问题:

  • 在同一个函数中多次调用,不同调用方,useState怎么管理怎么管理? 既然有多个,我们可以开辟一个数组,然后把状态存储起来:
function useState(initialState) {
  function* dispatchState() {
    let pointer = 0;
    const stateArr = [];
    stateArr[pointer] = initialState;
    while(true) {
      pointer++;
      state = yield stateArr[pointer - 1];
    }
  }
  const dispatch = dispatchState();
  const { value, done } = dispatch.next();
  const setState = (newState) => {
    dispatch.next(newState);
  };
  return [value, setState];
}

上面就可以解决,多次调用useState,状态存储的问题,虽然还存在二次更新的问题没有解决,也算是基本模拟了。在React官方文档中,hooks是不能被放在条件判断中的,必须放在函数组件的顶层作用域。 当然React官方不是通过数组来存储对应的状态,而是通过链表的形式。 假如,有下面的Function Component节点:

function useMyHook(initial) {
  const [my, setMy] = useState(initial);
  const [self, setSelf] = useState('self');
  const name = useMemo(() => 'name', []);
  return my + self + name;
}

function hookChild1() {

  const [tag, setTag] = useState('hook');
  const [name, setName] = useState('child1');
  const myHook = useMyHook('my');

  const hook = (
    <div>
      {`${tag} ${name} ${myHook}`}
    </div>
  );
  console.log('hook child1', hook);
  return hook;
}

hooks的链表是如何存储的呢,如下图所示?

可以看到,React hooks是通过链表的形式链接在一起,当每次初始化或者更新hooks的时候,

const [state, setState] = useState('state');

都会从链表中,获得对应的值。

通常来讲,链表的数据结构,会存在查找对应节点值的效率问题,但是Hooks不存在,hooks定义在顶层作用域,每次一定会执行一遍,那自然而然,React hooks去更新对应的值的时候,会更新链表节点的方式,读者你觉得会是怎么样呢?

注意

千万不要试图去记住这些函数,要理解整个流程框架,因为函数名字会经常变化的,但是机制一般是不会变化的。

React的事件机制

文中没有对React的事件机制进行一个说明,主要是觉得和本文相关,但是后面不太想写一个事件的主题,所以就简略的写在下面了。

  • React事件是通过委托的形式实现的(依赖于浏览器原生的冒泡事件)
  • React在初始化开始的时候,进行事件注册
  • 当发生事件的时候,冒泡到document,通过一个分配器,查找注册的方法,形成处理事件的一个数组
  • 批处理数组中的事件,批处理事件中的方法

说一个我实际编程中遇到的坑吧:React的事件是所有的都可以冒泡,包括原生不能冒泡的blur事件。

参考文档中有篇还不错的关于事件的文章,大家可以看下。以后如果遇到问题了再去查看具体的内容。和大家说一句,一定要有目的,带着问题去读源码。

参考文档

很好的一篇文章
React hooks实战
react事件机制