React的基本认知

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的整个机制。
2020-02-01-15-52-04.png

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

React Fiber的更新逻辑

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

我们看一下Fiber内部是如何进行更新的。先直接上图,然后再慢慢解释。
2020-02-02-13-51-29.png
2020-02-02-13-52-52.png

  • 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、componentDidUpdate等
  • e、在删除节点的时候,执行componentWillUnMount
  • f、还有其他生命周期,这里就不做赘述了

但是大家通常还有一个疑问,嵌套组件的生命周期如何执行?比如下面的两个组件

// Component
class Son extends React.Component {
  componentWillUnMount() {
    console.log('son unmount');
  }
  componentDidMount() {
    console.log('son mount');
  }
  componentDidUpdate() {
    console.log('son update);
  }
}
class Father extends React.Component {
  componentWillUnMount() {
    console.log('father unmount');
  }
  componentDidMount() {
    console.log('father mount');
  }
  componentDidUpdate() {
    console.log('father update);
  }
}

// App

function App() {
  return (
    <Fahter>
      <Son/>
    </Father>
  );
}

输出结果

son mount
father mount

// 如果父组件更新
son update
father update

// 如果组件卸载
father unmount
son unmount

为什么update和mount,子组件优先于父组件?而unmount父组件优先于子组件?
原理解释:
1、节点的遍历是深度优先遍历,可以通过performUnitOfWorkcompleteWork看到,首先是子节点->兄弟节点->父节点

function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
  workInProgress = unitOfWork;
  do {
    // 执行React内部逻辑判断,do some work
    // 完成后,如果有兄弟节点,返回兄弟节点
    const siblingFiber = workInProgress.sibling;
    if (siblingFiber !== null) {
      return siblingFiber;
    }
    // 没有兄弟节点,返回父节点
    workInProgress = returnFiber;
  } while (workInProgress !== null);

  // 如果到根节点了
  if (workInProgressRootExitStatus === RootIncomplete) {
    workInProgressRootExitStatus = RootCompleted;
  }
  return null;
}

2、整个过程会构建EffectList(副作用)列表,构建的列表如下所示:
2020-02-03-18-19-42.png
a) Fiber节点中有三个节点:

fiberNode {
  firstEffect,
  nextEffect,
  lasteffect
}

通过相互关系,构建了如图所示的链表结构。

nextEffect是从第一个更新的子节点,不停回溯到最后一个子节点,而lastEffect是逆向从第一个删除的节点->最后一个删除的节点。

b)commit阶段,会经历两个阶段:

  • 阶段一:执行更新的生命周期,然后执行删除的生命周期,你会看到unmount一定在更新之后执行,整个过程是这样的,通过nextEffect寻找更新的节点,到达最后一个节点后,通过lastEffect.next执行删除的操作
  • 阶段二:执行完对应的生命周期后,操作DOM结构,增、删、更新。
  • 阶段三:执行componentDidMount/componentDidUpdate/useEffect/的回调函数

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的内部执行逻辑是怎么样的?

2020-02-02-16-37-10.png
原理解释:

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的链表是如何存储的呢,如下图所示?
2020-02-02-22-40-17.png
可以看到,React hooks是通过链表的形式链接在一起,当每次初始化或者更新hooks的时候,

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

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

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

注意

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

React的事件机制

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

  • React事件是通过委托的形式实现的(依赖于浏览器原生的冒泡事件)
  • React在初始化开始的时候,进行事件注册
  • 当发生事件的时候,冒泡到document,通过一个分配器,查找注册的方法,形成处理事件的一个数组
  • 批处理数组中的事件,批处理事件中的方法
说一个我实际编程中遇到的坑吧:React的事件是所有的都可以冒泡,包括原生不能冒泡的blur事件。

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

参考文档

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


joytime
44 声望2 粉丝