# React
# 1 Redux 中 reducer 不能做异步操作的原因
- 先从 Redux 的设计层面来解释为什么 Reducer 必须是纯函数
如果你经常用 React + Redux
开发,那么就应该了解 Redux
的设计初衷。Redux
的设计参考了 Flux
的模式,作者希望以此来实现时间旅行,保存应用的历史状态,实现应用状态的可预测。所以整个 Redux
都是函数式编程的范式,要求 reducer
是纯函数也是自然而然的事情,使用纯函数才能保证相同的输入得到相同的输入,保证状态的可预测。所以 Redux
有三大原则:
- 单一数据源,也就是
state
state
是只读,Redux
并没有暴露出直接修改state
的接口,必须通过action
来触发修改- 使用纯函数来修改
state
,reducer
必须是纯函数
- 下面在从代码层面来解释为什么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
都是挂载到 Fiber
的 hooks
上的链表,每次 render
后的时候都会按照链表执行副作用。用来在数据发生变化后,执行相关的操作。
useEffect
本身有两个阶段:mountEffect
和 updateEffect
(本身内部还有对应实现方法,mountEffectImpl
),mount
阶段会挂载到 Fiber
当前 Hook
的 memoizedState
上,如果有多个 effect
,hook
的 memoizedState
中会存储一个链表。
renconciler
阶段会将所有 effect
打 tag
并且生成
在 commit
阶段会依次执行(这个过程会进行依赖比较)
在 destory
阶段,执行 return
的回调函数