# React

# 1 Redux 中 reducer 不能做异步操作的原因

  1. 先从 Redux 的设计层面来解释为什么 Reducer 必须是纯函数

如果你经常用 React + Redux开发,那么就应该了解 Redux 的设计初衷。Redux 的设计参考了 Flux 的模式,作者希望以此来实现时间旅行,保存应用的历史状态,实现应用状态的可预测。所以整个 Redux 都是函数式编程的范式,要求 reducer 是纯函数也是自然而然的事情,使用纯函数才能保证相同的输入得到相同的输入,保证状态的可预测。所以 Redux 有三大原则:

  • 单一数据源,也就是 state
  • state 是只读,Redux 并没有暴露出直接修改 state 的接口,必须通过 action 来触发修改
  • 使用纯函数来修改 statereducer 必须是纯函数
  1. 下面在从代码层面来解释为什么reducer必须是纯函数

那么reducer到底干了件什么事,在Redux的源码中只用了一行来表示:

currentState = currentReducer(currentState, action)

这一行简单粗暴的在代码层面解释了为什么 currentReducer 必须是纯函数。currentReducer 就是我们在 createStore 中传入的 reducer(至于为什么会加个 current 有兴趣的可以自己去看源码),reducer 是用来计算 state 的,所以它的返回值必须是 state,也就是我们整个应用的状态,而不能是 promise 之类的。

要在 reducer 中加入异步的操作,如果你只是单纯想执行异步操作,不会等待异步的返回,那么在 reducer 中执行的意义是什么。如果想把异步操作的结果反应在 state 中,首先整个应用的状态将变的不可预测,违背 Redux 的设计原则,其次,此时的 currentState 将会是 promise 之类而不是我们想要的应用状态,根本是行不通的。

# 2 React 中 fiber 是用来做什么的

因为 JavaScript 单线程的特点,每个同步任务不能耗时太长,不然就会让程序不会对其他输入作出相应,React 的更新过程就是犯了这个禁忌,而 React Fiber 就是要改变现状。 而可以通过分片来破解 JavaScript 中同步操作时间过长的问题。

把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。

React Fiber 把更新过程碎片化,每执行完一段更新过程,就把控制权交还给React负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。

维护每一个分片的数据结构,就是 Fiber。

# 3 React 生命周期

初始化阶段:

  • getInitialState 获取每个实例的初始化状态
  • getDefaultProps 获取实例的默认属性
  • componentWillMount 组件即将被装载、渲染到页面上
  • render 组件在这里生成虚拟的DOM节点
  • componentDidMount 组件真正在被装载之后

运行中状态:

  • componentWillReceiveProps 组件将要接收到属性的时候调用
  • shouldComponentUpdate 组件接受到新属性或者新状态的时候(可以返回 false,接收数据后不更新,阻止 render 调用,后面的函数不会被继续执行了)
  • componentWillUpdate 组件即将更新不能修改属性和状态
  • render 组件重新描绘
  • componentDidUpdate 组件已经更新

卸载过程:

  • componentWillUnmount 组件即将销毁

# 4 shouldComponentUpdate 函数有什么作用

shouldComponentUpdate 是一个允许我们自行决定某些组件(以及他们的子组件)是否进行更新的生命周期函数,Reconciliation 的最终目的是尽可能以最有效的方式去根据新的 state 更新UI,如果你已经知道 UI 的哪些状态无需进行改变,就没必要去让 React 去判断它是否该改变。 让 shouldComponentUpdate 返回 false, React 就会让当前的组件和其子组件保持不变。

# 5 当组件的setState函数被调用之后,发生了什么

在代码中调用 setState 函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程(Reconciliation)。

经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个UI界面。在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。

# 6 在生命周期中的哪一步你应该发起 AJAX 请求

React 下一代调和算法 Fiber 会通过开始或停止渲染的方式优化应用性能,其会影响到 componentWillMount 的触发次数。对于 componentWillMount 这个生命周期函数的调用次数会变得不确定,React 可能会多次频繁调用 componentWillMount。如果我们将 AJAX 请求放到 componentWillMount 函数中,那么显而易见其会被触发多次,自然也就不是好的选择。

如果我们将 AJAX 请求放置在生命周期的其他函数中,我们并不能保证请求仅在组件挂载完毕后才会要求响应。如果我们的数据请求在组件挂载之前就完成,并且调用了setState 函数将数据添加到组件状态中,对于未挂载的组件则会报错。而在 componentDidMount 函数中进行 AJAX 请求则能有效避免这个问题。

# 7 createElement 与 cloneElement 的区别是什么

createElement 函数是 JSX 编译之后使用的创建 React Element 的函数,而 cloneElement 则是用于复制某个元素并传入新的 Props。

# 8 传入 setState 函数的第二个参数的作用是什么

该函数会在 setState 函数调用完成并且组件开始重渲染的时候被调用,我们可以用该函数来监听渲染是否完成:

this.setState(
  { username: 'tylermcginnis33' },
  () => console.log('setState has finished and the component has re-rendered.')
)

# 9 什么是useEffect

useEffect 每次 render 后都会执行,本身每个 effect 都是挂载到 Fiberhooks 上的链表,每次 render 后的时候都会按照链表执行副作用。用来在数据发生变化后,执行相关的操作。

useEffect 本身有两个阶段:mountEffectupdateEffect(本身内部还有对应实现方法,mountEffectImpl),mount 阶段会挂载到 Fiber 当前 HookmemoizedState 上,如果有多个 effecthookmemoizedState 中会存储一个链表。

renconciler 阶段会将所有 effecttag 并且生成

commit 阶段会依次执行(这个过程会进行依赖比较)

destory 阶段,执行 return 的回调函数