【面试题】697- React万字长文面试题梳理
共 17272字,需浏览 35分钟
·
2020-08-27 13:02
原文地址:www.imooc.com/article/309371
1、React 中 keys的作用是什么
Keys
是 React
用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识
在开发过程中我们需要保证某个元素的 key
在其同级元素中具有唯一性。在 React Diff
算法中React
会借助元素的 Key
值来判断该元素是新近创建的还是被移动而来的元素从而减少不必要的元素重渲染。此外React 还需要借助 Key
值来判断元素与本地状态的关联关系因此我们绝不可忽视转换函数中 Key
的重要性
2、传入 setState 函数的第二个参数的作用是什么
该函数会在 setState 函数调用完成并且组件开始重渲染的时候被调用我们可以用该函数来监听渲染是否完成
this.setState(
{ username: 'tylermcginnis33' },
() => console.log('setState has finished and the component has re-rendered.')
)
this.setState((prevState, props) => {
return {
streak: prevState.streak + props.count
}
})
3、React 中 refs 的作用是什么
Refs
是 React
提供给我们的安全访问 DOM
元素或者某个组件实例的句柄可以为元素添加ref属性然后在回调函数中接受该元素在 DOM
树中的句柄该值会作为回调函数的第一个参数返回
4、在生命周期中的哪一步你应该发起 AJAX 请求
我们应当将AJAX
请求放到 componentDidMount
函数中执行主要原因有下
React
下一代调和算法 Fiber
会通过开始或停止渲染的方式优化应用性能其会影响到 componentWillMount
的触发次数。对于 componentWillMount
这个生命周期函数的调用次数会变得不确定React 可能会多次频繁调用 componentWillMount
。如果我们将 AJAX 请求放到 componentWillMount
函数中那么显而易见其会被触发多次自然也就不是好的选择。如果我们将AJAX 请求放置在生命周期的其他函数中我们并不能保证请求仅在组件挂载完毕后才会要求响应。如果我们的数据请求在组件挂载之前就完成并且调用了setState
函数将数据添加到组件状态中对于未挂载的组件则会报错。而在 componentDidMount
函数中进行 AJAX
请求则能有效避免这个问题
5、shouldComponentUpdate 的作用
shouldComponentUpdate
允许我们手动地判断是否要进行组件更新根据组件的应用场景设置函数的合理返回值能够帮我们避免不必要的更新
6、如何告诉 React 它应该编译生产环境版
通常情况下我们会使用 Webpack
的 DefinePlugin
方法来将 NODE_ENV
变量值设置为 production
。编译版本中 React
会忽略 propType
验证以及其他的告警信息同时还会降低代码库的大小React 使用了 Uglify
插件来移除生产环境下不必要的注释等信息
7、概述下 React 中的事件处理逻辑
为了解决跨浏览器兼容性问题React
会将浏览器原生事件Browser Native Event
封装为合成事件SyntheticEvent
传入设置的事件处理器中。这里的合成事件提供了与原生事件相同的接口不过它们屏蔽了底层浏览器的细节差异保证了行为的一致性。另外有意思的是React 并没有直接将事件附着到子元素上而是以单一事件监听器的方式将所有的事件发送到顶层进行处理。这样 React
在更新 DOM
的时候就不需要考虑如何去处理附着在 DOM
上的事件监听器最终达到优化性能的目的
8、createElement 与 cloneElement 的区别是什么
createElement
函数是 JSX 编译之后使用的创建 React Element
的函数而 cloneElement 则是用于复制某个元素并传入新的 Props
9、redux中间件
中间件提供第三方插件的模式自定义拦截 action -> reducer
的过程。变为 action
-> middlewares
-> reducer
。这种机制可以让我们改变数据流实现如异步action action
过滤日志输出异常报告等功能
redux-logger
提供日志输出redux-thunk
处理异步操作redux-promise
处理异步操作actionCreator
的返回值是promise
10、redux有什么缺点
一个组件所需要的数据必须由父组件传过来而不能像flux
中直接从store
取。当一个组件相关数据更新时即使父组件不需要用到这个组件父组件还是会重新render
可能会有效率影响或者需要写复杂的shouldComponentUpdate
进行判断。
11、react组件的划分业务组件技术组件
根据组件的职责通常把组件分为UI
组件和容器组件。UI
组件负责 UI
的呈现容器组件负责管理数据和逻辑。两者通过React-Redux
提供connect
方法联系起来
12、react旧版生命周期函数
初始化阶段
getDefaultProps
:获取实例的默认属性getInitialState
:获取每个实例的初始化状态componentWillMount
组件即将被装载、渲染到页面上render
:组件在这里生成虚拟的DOM节点componentDidMount
:组件真正在被装载之后
运行中状态
componentWillReceiveProps
:组件将要接收到属性的时候调用shouldComponentUpdate
:组件接受到新属性或者新状态的时候可以返回false
接收数据后不更新阻止render
调用后面的函数不会被继续执行了componentWillUpdate
:组件即将更新不能修改属性和状态render
:组件重新描绘componentDidUpdate
:组件已经更新
销毁阶段
componentWillUnmount:组件即将销毁新版生命周期
在新版本中React 官方对生命周期有了新的 变动建议:
使用getDerivedStateFromProps替换componentWillMount 使用getSnapshotBeforeUpdate替换componentWillUpdate 避免使用componentWillReceiveProps
其实该变动的原因正是由于上述提到的 Fiber
。首先从上面我们知道 React
可以分成 reconciliation
与 commit
两个阶段对应的生命周期如下:
reconciliation
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
commit
componentDidMoun
componentDidUpdate
componentWillUnmount
在 Fiber
中reconciliation
阶段进行了任务分割涉及到 暂停 和 重启因此可能会导致 reconciliation
中的生命周期函数在一次更新渲染循环中被 多次调用 的情况产生一些意外错误
新版的建议生命周期如下:
class Component extends React.Component {
// 替换 `componentWillReceiveProps`
// 初始化和 update 时被调用
// 静态函数无法使用 this
static getDerivedStateFromProps(nextProps, prevState) {}
// 判断是否需要更新组件
// 可以用于组件性能优化
shouldComponentUpdate(nextProps, nextState) {}
// 组件被挂载后触发
componentDidMount() {}
// 替换 componentWillUpdate
// 可以在更新之前获取最新 dom 数据
getSnapshotBeforeUpdate() {}
// 组件更新后调用
componentDidUpdate() {}
// 组件即将销毁
componentWillUnmount() {}
// 组件已销毁
componentDidUnMount() {}
}
使用建议:
在 constructor
初始化state
在 componentDidMount
中进行事件监听并在componentWillUnmount
中解绑事件在 componentDidMount
中进行数据的请求而不是在componentWillMount
需要根据 props
更新state
时使用getDerivedStateFromProps(nextProps, prevState)
旧 props
需要自己存储以便比较
public static getDerivedStateFromProps(nextProps, prevState) {
// 当新 props 中的 data 发生变化时同步更新到 state 上
if (nextProps.data !== prevState.data) {
return {
data: nextProps.data
}
} else {
return null1
}
}
可以在componentDidUpdate监听 props 或者 state 的变化例如:
componentDidUpdate(prevProps) {
// 当 id 发生变化时重新获取数据
if (this.props.id !== prevProps.id) {
this.fetchData(this.props.id);
}
}
在componentDidUpdate使用
setState`时必须加条件否则将进入死循环
shouldComponentUpdate
: 默认每次调用setState
一定会最终走到 diff
阶段但可以通过shouldComponentUpdate
的生命钩子返回false来直接阻止后面的逻辑执行通常是用于做条件渲染优化渲染的性能。
13、react性能优化是哪个周期函数
shouldComponentUpdate
这个方法用来判断是否需要调用render
方法重新描绘dom
。因为dom的描绘非常消耗性能如果我们能在
shouldComponentUpdate
方法中能够写出更优化的dom diff
算法可以极大的提高性能
14、为什么虚拟dom会提高性能
虚拟dom相当于在js和真实dom中间加了一个缓存利用dom diff算法避免了没有必要的dom操作从而提高性能
具体实现步骤如下
用 JavaScript
对象结构表示 DOM 树的结构然后用这个树构建一个真正的DOM
树插到文档当中当状态变更的时候重新构造一棵新的对象树。然后用新的树和旧的树进行比较记录两棵树差异 把2所记录的差异应用到步骤1所构建的真正的 DOM
树上视图就更新
15、diff算法?
把树形结构按照层级分解只比较同级元素。 给列表结构的每个单元添加唯一的key属性方便比较。 React
只会匹配相同class
的component
这里面的class指的是组件的名字合并操作调用 component
的setState
方法的时候, React 将其标记为 -dirty
.到每一个事件循环结束,React
检查所有标记dirty
的component
重新绘制.选择性子树渲染。开发人员可以重写 shouldComponentUpdate
提高diff
的性能
16、react性能优化方案
重写 shouldComponentUpdate
来避免不必要的dom
操作使用 production
版本的react.js
使用 key
来帮助React
识别列表中所有子组件的最小变化
17、简述flux 思想
Flux 的最大特点就是数据的"单向流动"。
用户访问
View
View发出用户的
Action
Dispatcher
收到Action要求Store
进行相应的更新Store
更新后发出一个"change
"事件View
收到"change
"事件后更新页面
18、说说你用react有什么坑点
JSX做表达式判断时候需要强转为boolean类型
如果不使用 !!b 进行强转数据类型会在页面里面输出 0。
render() {
const b = 0;
return <div>
{
!!b && <div>这是一段文本div>
}
div>
}
尽量不要在
componentWillReviceProps
里使用setState
如果一定要使用那么需要判断结束条件不然会出现无限重渲染导致页面崩溃给组件添加ref时候尽量不要使用匿名函数因为当组件更新的时候匿名函数会被当做新的
prop
处理让ref
属性接受到新函数的时候react
内部会先清空ref
也就是会以null
为回调参数先执行一次ref
这个props
然后在以该组件的实例执行一次ref
所以用匿名函数做ref的时候有的时候去ref
赋值后的属性会取到null
遍历子节点的时候不要用 index 作为组件的 key 进行传入
19、我现在有一个button要用react在上面绑定点击事件要怎么做
class Demo {
render() {
return <button onClick={(e) => {
alert('我点击了按钮')
}}>
按钮
button>
}
}
你觉得你这样设置点击事件会有什么问题吗
由于onClick
使用的是匿名函数所有每次重渲染的时候会把该onClick
当做一个新的prop来处理会将内部缓存的onClick
事件进行重新赋值所以相对直接使用函数来说可能有一点的性能下降
修改
class Demo {
onClick = (e) => {
alert('我点击了按钮')
}
render() {
return <button onClick={this.onClick}>
按钮
button>
}
20、react 的虚拟dom是怎么实现的
首先说说为什么要使用Virturl DOM
因为操作真实DOM
的耗费的性能代价太高所以react
内部使用js
实现了一套dom
结构在每次操作在和真实dom
之前使用实现好的diff
算法对虚拟dom
进行比较递归找出有变化的dom节点然后对其进行更新操作。为了实现虚拟DOM
我们需要把每一种节点类型抽象成对象每一种节点类型有自己的属性也就是prop
每次进行diff
的时候react
会先比较该节点类型假如节点类型不一样那么react
会直接删除该节点然后直接创建新的节点插入到其中假如节点类型一样那么会比较prop
是否有更新假如有prop
不一样那么react
会判定该节点有更新那么重渲染该节点然后在对其子节点进行比较一层一层往下直到没有子节点
21、react 的渲染过程中兄弟节点之间是怎么处理的也就是key值不一样的时候
通常我们输出节点的时候都是map
一个数组然后返回一个ReactNode
为了方便react
内部进行优化我们必须给每一个reactNode
添加key
这个key prop
在设计值处不是给开发者用的而是给react用的大概的作用就是给每一个reactNode添加一个身份标识方便react进行识别在重渲染过程中如果key一样若组件属性有所变化则react
只更新组件对应的属性没有变化则不更新如果key
不一样则react
先销毁该组件然后重新创建该组件
23、react-router里的标签和
标签有什么区别
24、connect原理
首先connect
之所以会成功是因为Provider
组件在原应用组件上包裹一层使原来整个应用成为Provider
的子组件接收Redux
的store
作为props
通过context
对象传递给子孙组件上的connect
connect做了些什么。它真正连接 Redux
和 React
它包在我们的容器组件的外一层它接收上面 Provider
提供的 store
里面的state
和 dispatch
传给一个构造函数返回一个对象以属性形式传给我们的容器组件
connect
是一个高阶函数首先传入mapStateToProps、mapDispatchToProps然后返回一个生产Component的函数(wrapWithConnect)然后再将真正的Component作为参数传入wrapWithConnect这样就生产出一个经过包裹的Connect组件该组件具有如下特点
通过props.store
获取祖先Component
的store props
包括stateProps
、dispatchProps
、parentProps
,合并在一起得到nextState
作为props
传给真正的Component componentDidMount
时添加事件this.store.subscribe(this.handleChange)
实现页面交互shouldComponentUpdate
时判断是否有避免进行渲染提升页面性能并得到nextState componentWillUnmount时移除注册的事件this.handleChange
由于connect的源码过长我们只看主要逻辑
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
return function wrapWithConnect(WrappedComponent) {
class Connect extends Component {
constructor(props, context) {
// 从祖先Component处获得store
this.store = props.store || context.store
this.stateProps = computeStateProps(this.store, props)
this.dispatchProps = computeDispatchProps(this.store, props)
this.state = { storeState: null }
// 对stateProps、dispatchProps、parentProps进行合并
this.updateState()
}
shouldComponentUpdate(nextProps, nextState) {
// 进行判断当数据发生改变时Component重新渲染
if (propsChanged
|| mapStateProducedChange
|| dispatchPropsChanged) {
this.updateState(nextProps)
return true
}
}
componentDidMount() {
// 改变Component的state
this.store.subscribe(() = {
this.setState({
storeState: this.store.getState()
})
})
}
render() {
// 生成包裹组件Connect
return (
<WrappedComponent {...this.nextState} />
)
}
}
Connect.contextTypes = {
store: storeShape
}
return Connect;
}
}
25、Redux实现原理解析
为什么要用redux
在React
中数据在组件中是单向流动的数据从一个方向父组件流向子组件通过props
,所以两个非父子组件之间通信就相对麻烦redux
的出现就是为了解决state
里面的数据问题
Redux设计理念
Redux
是将整个应用状态存储到一个地方上称为store
,里面保存着一个状态树store
tree
,组件可以派发(dispatch
)行为(action
)给store
,而不是直接通知其他组件组件内部通过订阅store
中的状态state
来刷新自己的视图
image
Redux三大原则
1.唯一数据源
整个应用的state
都被存储到一个状态树里面并且这个状态树只存在于唯一的store
中
2.保持只读状态
state
是只读的唯一改变state
的方法就是触发actionaction
是一个用于描述以发生时间的普通对象
3.数据改变只能通过纯函数来执行
使用纯函数来执行修改为了描述action
如何改变state
的你需要编写reducers
Redux源码
let createStore = (reducer) => {
let state;
//获取状态对象
//存放所有的监听函数
let listeners = [];
let getState = () => state;
//提供一个方法供外部调用派发action
let dispath = (action) => {
//调用管理员reducer得到新的state
state = reducer(state, action);
//执行所有的监听函数
listeners.forEach((l) => l())
}
//订阅状态变化事件当状态改变发生之后执行监听函数
let subscribe = (listener) => {
listeners.push(listener);
}
dispath();
return {
getState,
dispath,
subscribe
}
}
let combineReducers=(renducers)=>{
//传入一个renducers管理组返回的是一个renducer
return function(state={},action={}){
let newState={};
for(var attr in renducers){
newState[attr]=renducers[attr](state[attr],action)
}
return newState;
}
}
export {createStore,combineReducers};
26、pureComponent和FunctionComponent区别
PureComponent
和Component
完全相同但是在shouldComponentUpdate
实现中PureComponent
使用了props
和state
的浅比较。主要作用是用来提高某些特定场景的性能
27 react hooks它带来了那些便利
代码逻辑聚合逻辑复用 HOC
嵌套地狱代替 class
React
中通常使用 类定义 或者 函数定义 创建组件:
在类定义中我们可以使用到许多 React
特性例如 state
、 各种组件生命周期钩子等但是在函数定义中我们却无能为力因此 React 16.8
版本推出了一个新功能 (React Hooks
)通过它可以更好的在函数定义组件中使用 React
特性。
好处:
跨组件复用: 其实 render
props
/HOC
也是为了复用相比于它们Hooks 作为官方的底层API
最为轻量而且改造成本小不会影响原来的* 组件层次结构和传说中的嵌套地狱类定义更为复杂 不同的生命周期会使逻辑变得分散且混乱不易维护和管理 时刻需要关注 this
的指向问题代码复用代价高高阶组件的使用经常会使整个组件树变得臃肿 状态与UI隔离: 正是由于 Hooks
的特性状态逻辑会变成更小的粒度并且极容易被抽象成一个自定义Hooks
组件中的状态和UI
变得更为清晰和隔离。
注意:
避免在 循环/条件判断/嵌套函数 中调用 hooks
保证调用顺序的稳定只有 函数定义组件 和 hooks
可以调用 hooks
避免在 类组件 或者 普通函数 中调用不能在useEffect
中使用useStateReact
会报错提示类组件不会被替换或废弃不需要强制改造类组件两种方式能并存重要钩子
状态钩子 (useState
): 用于定义组件的State
其到类定义中this.state
的功能
// useState 只接受一个参数: 初始状态
// 返回的是组件名和更改该组件对应的函数
const [flag, setFlag] = useState(true);
// 修改状态
setFlag(false)
// 上面的代码映射到类定义中:
this.state = {
flag: true
}
const flag = this.state.flag
const setFlag = (bool) => {
this.setState({
flag: bool,
})
}
生命周期钩子 (useEffect):
类定义中有许多生命周期函数而在 React Hooks
中也提供了一个相应的函数 (useEffect
)这里可以看做componentDidMount
、componentDidUpdate
和componentWillUnmount
的结合。
useEffect(callback, [source])
接受两个参数callback
: 钩子回调函数source
: 设置触发条件仅当source
发生改变时才会触发useEffect
钩子在没有传入[source]参数时默认在每次render
时都会优先调用上次保存的回调中返回的函数后再重新调用回调
useEffect(() => {
// 组件挂载后执行事件绑定
console.log('on')
addEventListener()
// 组件 update 时会执行事件解绑
return () => {
console.log('off')
removeEventListener()
}
}, [source]);
// 每次 source 发生改变时执行结果(以类定义的生命周期便于大家理解):
// --- DidMount ---
// 'on'
// --- DidUpdate ---
// 'off'
// 'on'
// --- DidUpdate ---
// 'off'
// 'on'
// --- WillUnmount ---
// 'off'
通过第二个参数我们便可模拟出几个常用的生命周期:
componentDidMount
: 传入[]时就只会在初始化时调用一次const useMount = (fn) => useEffect(fn, [])
componentWillUnmount
: 传入[]回调中的返回的函数也只会被最终执行一次const useUnmount = (fn) => useEffect(() => fn, [])
mounted
: 可以使用 useState 封装成一个高度可复用的mounted
状态
const useMounted = () => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
!mounted && setMounted(true);
return () => setMounted(false);
}, []);
return mounted;
}
componentDidUpdate: useEffect每次均会执行其实就是排除了 DidMount 后即可
const mounted = useMounted()
useEffect(() => {
mounted && fn()
})
其它内置钩子:
useContext
: 获取context
对象useReducer
: 类似于Redux
思想的实现但其并不足以替代 Redux可以理解成一个组件内部的redux
:并不是持久化存储会随着组件被销毁而销毁属于组件内部各个组件是相互隔离的单纯用它并无法共享数据 配合 useContext
的全局性可以完成一个轻量级的Redux
(easy-peasy
)useCallback
: 缓存回调函数避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染具有性能优化的效果useMemo
: 用于缓存传入的props
避免依赖的组件每次都重新渲染useRef
: 获取组件的真实节点useLayoutEffect
DOM更新同步钩子。用法与useEffect类似只是区别于执行时间点的不同 useEffect属于异步执行并不会等待 DOM 真正渲染后执行而 useLayoutEffect
则会真正渲染后才触发可以获取更新后的 state
自定义钩子( useXxxxx
): 基于Hooks
可以引用其它Hooks
这个特性我们可以编写自定义钩子如上面的useMounted
。又例如我们需要每个页面自定义标题:
function useTitle(title) {
useEffect(
() => {
document.title = title;
});
}
// 使用:
function Home() {
const title = '我是首页'
useTitle(title)
return (
<div>{title}div>
)
}
28、React Portal 有哪些使用场景
在以前 react
中所有的组件都会位于 #app
下而使用 Portals
提供了一种脱离 #app
的组件因此 Portals
适合脱离文档流(out of flow
) 的组件特别是 position: absolute
与 position: fixed
的组件。比如模态框通知警告goTop
等。
以下是官方一个模态框的示例可以在以下地址中测试效果
<html>
<body>
<div id="app">div>
<div id="modal">div>
<div id="gotop">div>
<div id="alert">div>
body>
html>
const modalRoot = document.getElementById('modal');
class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement('div');
}
componentDidMount() {
modalRoot.appendChild(this.el);
}
componentWillUnmount() {
modalRoot.removeChild(this.el);
}
render() {
return ReactDOM.createPortal(
this.props.children,
this.el,
);
}
}
React Hooks
当中的useEffect
是如何区分生命周期钩子的
useEffect
可以看成是componentDidMountcomponentDidUpdate
和componentWillUnmount
三者的结合。useEffect(callback, [source])
接收两个参数调用方式如下
useEffect(() => {
console.log('mounted');
return () => {
console.log('willUnmount');
}
}, [source]);
生命周期函数的调用主要是通过第二个参数[source]来进行控制有如下几种情况
[source]参数不传时则每次都会优先调用上次保存的函数中返回的那个函数然后再调用外部那个函数 [source]参数传[]时则外部的函数只会在初始化时调用一次返回的那个函数也只会最终在组件卸载时调用一次 [source]参数有值时则只会监听到数组中的值发生变化后才优先调用返回的那个函数再调用外部的函数。
29、react和vue的区别
相同点
数据驱动页面提供响应式的试图组件 都有 virtual DOM
,组件化的开发通过props
参数进行父子之间组件传递数据都实现了webComponents
规范数据流动单向都支持服务器的渲染SSR 都有支持 native
的方法react
有React native vue
有wexx
不同点
数据绑定 Vue
实现了双向的数据绑定react
数据流动是单向的数据渲染大规模的数据渲染 react
更快使用场景 React
配合Redux
架构适合大规模多人协作复杂项目Vue适合小快的项目开发风格 react
推荐做法jsx
+inline style
把html
和css
都写在js
了vue
是采用webpack
+vue-loader
单文件组件格式html
,js
,css
同一个文件
30、什么是高阶组件(HOC)
高阶组件(Higher Order Componennt)本身其实不是组件而是一个函数这个函数接收一个元组件作为参数然后返回一个新的增强组件高阶组件的出现本身也是为了逻辑复用举个例子
function withLoginAuth(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
isLogin: false
};
}
async componentDidMount() {
const isLogin = await getLoginStatus();
this.setState({ isLogin });
}
render() {
if (this.state.isLogin) {
return <WrappedComponent {...this.props} />;
}
return (<div>您还未登录...div>);
}
}
}
31、React实现的移动应用中如果出现卡顿有哪些可以考虑的优化方案
增加
shouldComponentUpdate
钩子对新旧props
进行比较如果值相同则阻止更新避免不必要的渲染或者使用PureReactComponent
替代Component
其内部已经封装了shouldComponentUpdate
的浅比较逻辑对于列表或其他结构相同的节点为其中的每一项增加唯一
key
属性以方便React
的diff
算法中对该节点的复用减少节点的创建和删除操作
render函数中减少类似
onClick={() => {
doSomething()
}}
的写法每次调用render
函数时均会创建一个新的函数即使内容没有发生任何变化也会导致节点没必要的重渲染建议将函数保存在组件的成员对象中这样只会创建一次
组件的props如果需要经过一系列运算后才能拿到最终结果则可以考虑使用
reselect
库对结果进行缓存如果props值未发生变化则结果直接从缓存中拿避免高昂的运算代价webpack-bundle-analyzer分析当前页面的依赖包是否存在不合理性如果存在找到优化点并进行优化
32、setState
在了解setState
之前我们先来简单了解下 React
一个包装结构: Transaction
:
事务 (Transaction)
是 React
中的一个调用结构用于包装一个方法结构为: initialize
- perform(method)
- close
。通过事务可以统一管理一个方法的开始与结束处于事务流中表示进程正在执行一些操作
setState: React 中用于修改状态更新视图。它具有以下特点:
异步与同步: setState并不是单纯的异步或同步这其实与调用时的环境相关:
在合成事件 和 生命周期钩子(除 componentDidUpdate) 中setState是"异步"的
原因: 因为在setState
的实现中有一个判断: 当更新策略正在事务流的执行中时该组件更新会被推入dirtyComponents
队列中等待执行否则开始执行batchedUpdates
队列更新
在生命周期钩子调用中更新策略都处于更新之前组件仍处于事务流中而componentDidUpdate
是在更新之后此时组件已经不在事务流中了因此则会同步执行
在合成事件中React 是基于 事务流完成的事件委托机制 实现也是处于事务流中
问题: 无法在setState后马上从this.state上获取更新后的值。
解决: 如果需要马上同步去获取新值setState
其实是可以传入第二个参数的。setState(updater, callback)
在回调中即可获取最新值
在 原生事件 和 setTimeout
中setState
是同步的可以马上获取更新后的值
原因: 原生事件是浏览器本身的实现与事务流无关自然是同步而setTimeout
是放置于定时器线程中延后执行此时事务流已结束因此也是同步
批量更新: 在 合成事件 和 生命周期钩子 中setState
更新队列时存储的是 合并状态(Object.assign)
。因此前面设置的 key
值会被后面所覆盖最终只会执行一次更新
函数式: 由于 Fiber 及 合并 的问题官方推荐可以传入 函数 的形式。setState(fn)
在fn中返回新的state对象即可例如
this.setState((state, props) => newState)
使用函数式可以用于避免setState的批量更新的逻辑传入的函数将会被 顺序调用
注意事项:
setState
合并在 合成事件 和 生命周期钩子 中多次连续调用会被优化为一次当组件已被销毁如果再次调用 setStateReact
会报错警告通常有两种解决办法将数据挂载到外部通过 props 传入如放到 Redux 或 父级中 在组件内部维护一个状态量 ( isUnmounted
)componentWillUnmount
中标记为true
在setState
前进行判断
34、HOC(高阶组件)
HOC(Higher Order Componennt)
是在 React
机制下社区形成的一种组件模式在很多第三方开源库中表现强大。
简述:
高阶组件不是组件是 增强函数可以输入一个元组件返回出一个新的增强组件 高阶组件的主要作用是 代码复用操作 状态和参数用法:
属性代理 (Props Proxy): 返回出一个组件它基于被包裹组件进行 功能增强默认参数: 可以为组件包裹一层默认参数
function proxyHoc(Comp) {
return class extends React.Component {
render() {
const newProps = {
name: 'tayde',
age: 1,
}
return <Comp {...this.props} {...newProps} />
}
}
}
提取状态: 可以通过 props
将被包裹组件中的 state
依赖外层例如用于转换受控组件:
function withOnChange(Comp) {
return class extends React.Component {
constructor(props) {
super(props)
this.state = {
name: '',
}
}
onChangeName = () => {
this.setState({
name: 'dongdong',
})
}
render() {
const newProps = {
value: this.state.name,
onChange: this.onChangeName,
}
return <Comp {...this.props} {...newProps} />
}
}
}
使用姿势如下这样就能非常快速的将一个 Input
组件转化成受控组件。
const NameInput = props => (<input name="name" {...props} />)
export default withOnChange(NameInput)
包裹组件: 可以为被包裹元素进行一层包装
function withMask(Comp) {
return class extends React.Component {
render() {
return (
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, .6)',
}}
)
}
}
}
反向继承 (Inheritance Inversion
): 返回出一个组件继承于被包裹组件常用于以下操作
function IIHoc(Comp) {
return class extends Comp {
render() {
return super.render();
}
};
}
渲染劫持 (Render Highjacking)
条件渲染: 根据条件渲染不同的组件
function withLoading(Comp) {
return class extends Comp {
render() {
if(this.props.isLoading) {
return <Loading />
} else {
return super.render()
}
}
};
}
可以直接修改被包裹组件渲染出的 React 元素树
操作状态 (Operate State
): 可以直接通过 this.state
获取到被包裹组件的状态并进行操作。但这样的操作容易使 state
变得难以追踪不易维护谨慎使用。
应用场景:
权限控制通过抽象逻辑统一对页面进行权限判断按不同的条件进行页面渲染:
function withAdminAuth(WrappedComponent) {
return class extends React.Component {
constructor(props){
super(props)
this.state = {
isAdmin: false,
}
}
async componentWillMount() {
const currentRole = await getCurrentUserRole();
this.setState({
isAdmin: currentRole === 'Admin',
});
}
render() {
if (this.state.isAdmin) {
return <Comp {...this.props} />;
} else {
return (<div>您没有权限查看该页面请联系管理员div>);
}
}
};
}
性能监控包裹组件的生命周期进行统一埋点:
function withTiming(Comp) {
return class extends Comp {
constructor(props) {
super(props);
this.start = Date.now();
this.end = 0;
}
componentDidMount() {
super.componentDidMount && super.componentDidMount();
this.end = Date.now();
console.log(`${WrappedComponent.name} 组件渲染时间为 ${this.end - this.start} ms`);
}
render() {
return super.render();
}
};
}
代码复用可以将重复的逻辑进行抽象。
使用注意:
纯函数: 增强函数应为纯函数避免侵入修改元组件 避免用法污染: 理想状态下应透传元组件的无关参数与事件尽量保证用法不变 命名空间: 为HOC
增加特异性的组件名称这样能便于开发调试和查找问题 引用传递: 如果需要传递元组件的 refs
引用可以使用React.forwardRef
静态方法: 元组件上的静态方法并无法被自动传出会导致业务层无法调用解决: 函数导出 静态方法赋值 重新渲染: 由于增强函数每次调用是返回一个新组件因此如果在 Render
中使用增强函数就会导致每次都重新渲染整个HOC
而且之前的状态会丢失
35、React如何进行组件/逻辑复用?
抛开已经被官方弃用的Mixin,组件抽象的技术目前有三种比较主流:
高阶组件:
属性代理 反向继承 渲染属性 react-hooks
36、你对 Time Slice的理解?
时间分片
React 在渲染render的时候不会阻塞现在的线程 如果你的设备足够快你会感觉渲染是同步的 如果你设备非常慢你会感觉还算是灵敏的 虽然是异步渲染但是你将会看到完整的渲染而不是一个组件一行行的渲染出来 同样书写组件的方式 也就是说这是React背后在做的事情对于我们开发者来说是透明的具体是什么样的效果呢
37、setState到底是异步还是同步?
先给出答案: 有时表现出异步,有时表现出同步
setState
只在合成事件和钩子函数中是“异步”的在原生事件和setTimeout
中都是同步的
setState
的“异步”并不是说内部由异步代码实现其实本身执行的过程和代码都是同步的只是合成事件和钩子函数的调用顺序在更新之前导致在合成事件和钩子函数中没法立马拿到更新后的值形成了所谓的“异步”当然可以通过第二个参数setState(partialState, callback)
中的callback
拿到更新后的结果
setState
的批量更新优化也是建立在“异步”合成事件、钩子函数之上的在原生事件和setTimeout
中不会批量更新在“异步”中如果对同一个值进行多次setState
的批量更新策略会对其进行覆盖取最后一次的执行如果是同时setState
多个不同的值在更新时会对其进行合并批量更新
1. JavaScript 重温系列(22篇全)
2. ECMAScript 重温系列(10篇全)
3. JavaScript设计模式 重温系列(9篇全) 4. 正则 / 框架 / 算法等 重温系列(16篇全) 5. Webpack4 入门(上)|| Webpack4 入门(下) 6. MobX 入门(上) || MobX 入门(下) 7. 70+篇原创系列汇总 回复“加群”与大佬们一起交流学习~
点击“阅读原文”查看70+篇原创文章
点这,与大家一起分享本文吧~