useEffect 与 useLayoutEffect 的区别
(1)共同点
- 运用效果: useEffect 与 useLayoutEffect 两者都是用于处理副作用,这些副作用包括改变 DOM、设置订阅、操作定时器等。在函数组件内部操作副作用是不被允许的,所以需要使用这两个函数去处理。
- 使用方式: useEffect 与 useLayoutEffect 两者底层的函数签名是完全一致的,都是调用的 mountEffectImpl方法,在使用上也没什么差异,基本可以直接替换。
(2)不同点
- 使用场景: useEffect 在 React 的渲染过程中是被异步调用的,用于绝大多数场景;而 useLayoutEffect 会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在 useLayoutEffect 做计算量较大的耗时任务从而造成阻塞。
- 使用效果: useEffect是按照顺序执行代码的,改变屏幕像素之后执行(先渲染,后改变DOM),当改变屏幕内容时可能会产生闪烁;useLayoutEffect是改变屏幕像素之前就执行了(会推迟页面显示的事件,先改变DOM后渲染),不会产生闪烁。useLayoutEffect总是比useEffect先执行。
在未来的趋势上,两个 API 是会长期共存的,暂时没有删减合并的计划,需要开发者根据场景去自行选择。React 团队的建议非常实用,如果实在分不清,先用 useEffect,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect 即可。
react 版本差异
react16.8 hooks
React 16之后有三个生命周期被废弃(但并未删除)
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
官方计划在17版本完全删除这三个函数,只保留UNSAVE_前缀的三个函数,目的是为了向下兼容,
react 16.4 新增
getSnapshotBeforeUpdate
getDerivedStateFromProps
对于废弃的生命周期函数,官方会采用逐步迁移的方式来实现版本的迁移:
16.3:为不安全的生命周期引入别名,UNSAFE_componentWillMount、UNSAFE_componentWillReceiveProps 和 UNSAFE_componentWillUpdate。(旧的生命周期名称和新的别名都可以在此版本中使用。)
未来 16.x 版本:为 componentWillMount、componentWillReceiveProps 和 componentWillUpdate 启用废弃告警。(旧的生命周期名称和新的别名都将在这个版本中工作,但是旧的名称在开发模式下会产生一个警告。)
17.0:删除 componentWillMount、componentWillReceiveProps 和 componentWillUpdate。(在此版本之后,只有新的 “UNSAFE_” 生命周期名称可以使用。)。
虚拟 DOM 的引入与直接操作原生 DOM 相比,哪一个效率更高,为什么
虚拟DOM相对原生的DOM不一定是效率更高,如果只修改一个按钮的文案,那么虚拟 DOM 的操作无论如何都不可能比真实的 DOM 操作更快。在首次渲染大量DOM时,由于多了一层虚拟DOM的计算,虚拟DOM也会比innerHTML插入慢。它能保证性能下限,在真实DOM操作的时候进行针对性的优化时,还是更快的。所以要根据具体的场景进行探讨。
在整个 DOM 操作的演化过程中,其实主要矛盾并不在于性能,而在于开发者写得爽不爽,在于研发体验/研发效率。虚拟 DOM 不是别的,正是前端开发们为了追求更好的研发体验和研发效率而创造出来的高阶产物。虚拟 DOM 并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。**虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式(也就是函数式的 UI 编程方式)的同时,仍然保持一个还不错的性能。
React Hooks在平时开发中需要注意的问题和原因
(1)不要在循环,条件或嵌套函数中调用Hook,必须始终在 React函数的顶层使用Hook
这是因为React需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。
(2)使用useState时候,使用push,pop,splice等直接更改数组对象的坑
使用push直接更改数组无法获取到新值,应该采用析构方式,但是在class里面不会有这个问题。代码示例:
function Indicatorfilter() {
let [num,setNums] = useState([0,1,2,3])
const test = () => {
// 这里坑是直接采用push去更新num
// setNums(num)是无法更新num的
// 必须使用num = [...num ,1]
num.push(1)
// num = [...num ,1]
setNums(num)
}
return (
<div className='filter'>
<div onClick={test}>测试</div>
<div>
{num.map((item,index) => ( <div key={index}>{item}</div>
))} </div>
</div>
)
}
class Indicatorfilter extends React.Component<any,any>{
constructor(props:any){
super(props)
this.state = {
nums:[1,2,3]
}
this.test = this.test.bind(this)
}
test(){
// class采用同样的方式是没有问题的
this.state.nums.push(1)
this.setState({
nums: this.state.nums
})
}
render(){
let {nums} = this.state
return(
<div>
<div onClick={this.test}>测试</div>
<div>
{nums.map((item:any,index:number) => ( <div key={index}>{item}</div>
))} </div>
</div>
)
}
}
(3)useState设置状态的时候,只有第一次生效,后期需要更新状态,必须通过useEffect
TableDeail是一个公共组件,在调用它的父组件里面,我们通过set改变columns的值,以为传递给TableDeail 的 columns是最新的值,所以tabColumn每次也是最新的值,但是实际tabColumn是最开始的值,不会随着columns的更新而更新:
const TableDeail = ({ columns,}:TableData) => {
const [tabColumn, setTabColumn] = useState(columns)
}
// 正确的做法是通过useEffect改变这个值
const TableDeail = ({ columns,}:TableData) => {
const [tabColumn, setTabColumn] = useState(columns)
useEffect(() =>{setTabColumn(columns)},[columns])
}
(4)善用useCallback
父组件传递给子组件事件句柄时,如果我们没有任何参数变动可能会选用useMemo。但是每一次父组件渲染子组件即使没变化也会跟着渲染一次。
(5)不要滥用useContext
可以使用基于 useContext 封装的状态管理工具。
setState之后 发生了什么?
- (1)代码中调用 setState 函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程(Reconciliation)。
- (2)经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面;
- (3)在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染;
- (4)在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。
setState的调用会引起React的更新生命周期的4个函数执行。
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate
说说你用react有什么坑点?
1. JSX做表达式判断时候,需要强转为boolean类型
如果不使用
!!b
进行强转数据类型,会在页面里面输出0
。
render() {
const b = 0;
return <div>
{
!!b && <div>这是一段文本</div>
}
</div>
}
2. 尽量不要在 componentWillReviceProps
里使用 setState,如果一定要使用,那么需要判断结束条件,不然会出现无限重渲染,导致页面崩溃
3. 给组件添加ref时候,尽量不要使用匿名函数,因为当组件更新的时候,匿名函数会被当做新的prop处理,让ref属性接受到新函数的时候,react内部会先清空ref,也就是会以null为回调参数先执行一次ref这个props,然后在以该组件的实例执行一次ref,所以用匿名函数做ref的时候,有的时候去ref赋值后的属性会取到null
4. 遍历子节点的时候,不要用 index 作为组件的 key 进行传入
参考 前端进阶面试题详细解答
React 高阶组件、Render props、hooks 有什么区别,为什么要不断迭代
这三者是目前react解决代码复用的主要方式:
- 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。
- render props是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术,更具体的说,render prop 是一个用于告知组件需要渲染什么内容的函数 prop。
- 通常,render props 和高阶组件只渲染一个子节点。让 Hook 来服务这个使用场景更加简单。这两种模式仍有用武之地,(例如,一个虚拟滚动条组件或许会有一个 renderltem 属性,或是一个可见的容器组件或许会有它自己的 DOM 结构)。但在大部分场景下,Hook 足够了,并且能够帮助减少嵌套。
(1)HOC 官方解释∶
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
简言之,HOC是一种组件的设计模式,HOC接受一个组件和额外的参数(如果需要),返回一个新的组件。HOC 是纯函数,没有副作用。
// hoc的定义
function withSubscription(WrappedComponent, selectData) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
data: selectData(DataSource, props)
};
}
// 一些通用的逻辑处理
render() {
// ... 并使用新数据渲染被包装的组件!
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
// 使用
const BlogPostWithSubscription = withSubscription(BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id));
HOC的优缺点∶
- 优点∶ 逻辑服用、不影响被包裹组件的内部逻辑。
- 缺点∶ hoc传递给被包裹组件的props容易和被包裹后的组件重名,进而被覆盖
(2)Render props 官方解释∶
"render prop"是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术
具有render prop 的组件接受一个返回React元素的函数,将render的渲染逻辑注入到组件内部。在这里,"render"的命名可以是任何其他有效的标识符。
// DataProvider组件内部的渲染逻辑如下
class DataProvider extends React.Components {
state = {
name: 'Tom'
}
render() {
return (
<div>
<p>共享数据组件自己内部的渲染逻辑</p>
{ this.props.render(this.state) } </div>
);
}
}
// 调用方式
<DataProvider render={data => (
<h1>Hello {data.name}</h1>
)}/>
由此可以看到,render props的优缺点也很明显∶
- 优点:数据共享、代码复用,将组件内的state作为props传递给调用者,将渲染逻辑交给调用者。
- 缺点:无法在 return 语句外访问数据、嵌套写法不够优雅
(3)Hooks 官方解释∶
Hook是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。通过自定义hook,可以复用代码逻辑。
// 自定义一个获取订阅数据的hook
function useSubscription() {
const data = DataSource.getComments();
return [data];
}
//
function CommentList(props) {
const {data} = props;
const [subData] = useSubscription();
...
}
// 使用
<CommentList data='hello' />
以上可以看出,hook解决了hoc的prop覆盖的问题,同时使用的方式解决了render props的嵌套地狱的问题。hook的优点如下∶
- 使用直观;
- 解决hoc的prop 重名问题;
- 解决render props 因共享数据 而出现嵌套地狱的问题;
- 能在return之外使用数据的问题。
需要注意的是:hook只能在组件顶层使用,不可在分支语句中使用。、
React.Component 和 React.PureComponent 的区别
PureComponent表示一个纯组件,可以用来优化React程序,减少render函数执行的次数,从而提高组件的性能。
在React中,当prop或者state发生变化时,可以通过在shouldComponentUpdate生命周期函数中执行return false来阻止页面的更新,从而减少不必要的render执行。React.PureComponent会自动执行 shouldComponentUpdate。
不过,pureComponent中的 shouldComponentUpdate() 进行的是浅比较,也就是说如果是引用数据类型的数据,只会比较不是同一个地址,而不会比较这个地址里面的数据是否一致。浅比较会忽略属性和或状态突变情况,其实也就是数据引用指针没有变化,而数据发生改变的时候render是不会执行的。如果需要重新渲染那么就需要重新开辟空间引用数据。PureComponent一般会用在一些纯展示组件上。
使用pureComponent的好处:当组件更新时,如果组件的props或者state都没有改变,render函数就不会触发。省去虚拟DOM的生成和对比过程,达到提升性能的目的。这是因为react自动做了一层浅比较。
展示组件(Presentational component)和容器组件(Container component)之间有何不同
展示组件关心组件看起来是什么。展示专门通过 props 接受数据和回调,并且几乎不会有自身的状态,但当展示组件拥有自身的状态时,通常也只关心 UI 状态而不是数据的状态。
容器组件则更关心组件是如何运作的。容器组件会为展示组件或者其它容器组件提供数据和行为(behavior),它们会调用 Flux actions
,并将其作为回调提供给展示组件。容器组件经常是有状态的,因为它们是(其它组件的)数据源。
概述一下 React中的事件处理逻辑。
为了解决跨浏览器兼容性问题, React会将浏览器原生事件( Browser Native Event)封装为合成事件( Synthetic Event)并传入设置的事件处理程序中。
这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。另外, React并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理(基于事件委托原理)。
这样 React在更新DOM时就不需要考虑如何处理附着在DOM上的事件监听器,最终达到优化性能的目的。
请说岀 React从 EMAScript5编程规范到 EMAScript6编程规范过程中的几点改变。
主要改变如下。
(1)创建组件的方法不同。
EMAScript5版本中,定义组件用 React.createClass。EMAScript6版本中,定义组件要定义组件类,并继承 Component类。
(2)定义默认属性的方法不同。
EMAScript5版本中,用 getDefaultProps定义默认属性。EMAScript6版本中,为组件定义 defaultProps静态属性,来定义默认属性。
(3)定义初始化状态的方法不同。EMAScript5版本中,用 getInitialState定义初始化状态。EMAScript6版本中,在构造函数中,通过this. state定义初始化状态。
注意:构造函数的第一个参数是属性数据,一定要用 super继承。
(4)定义属性约束的方法不同。
EMAScript5版本中,用 propTypes定义属性的约束。
EMAScript6版本中,为组件定义 propsTypes静态属性,来对属性进行约束。
(5)使用混合对象、混合类的方法不同。
EMAScript5版本中,通过mixins继承混合对象的方法。
EMAScript6版本中,定义混合类,让混合类继承 Component类,然后让组件类继承混合类,实现对混合类方法的继承。
(6)绑定事件的方法不同。
EMAScript5版本中,绑定的事件回调函数作用域是组件实例化对象。
EMAScript6版本中,绑定的事件回调函数作用域是null。
(7)父组件传递方法的作用域不同。
EMAScript5版本中,作用域是父组件。 EMAScript6版本中,变成了null。
(8)组件方法作用域的修改方法不同。
EMAScript5版本中,无法改变作用域。
EMAScript6版本中,作用域是可以改变的。
当调用 setState的时候,发生了什么操作?**
当调用 setState时, React做的第一件事是将传递给setState的对象合并到组件的当前状态,这将启动一个称为和解( reconciliation)的过程。
和解的最终目标是,根据这个新的状态以最有效的方式更新DOM。
为此, React将构建一个新的 React虚拟DOM树(可以将其视为页面DOM元素的对象表示方式)。
一旦有了这个DOM树,为了弄清DOM是如何响应新的状态而改变的, React会将这个新树与上一个虚拟DOM树比较。
这样做, React会知道发生的确切变化,并且通过了解发生的变化后,在绝对必要的情况下进行更新DOM,即可将因操作DOM而占用的空间最小化。
React中的setState和replaceState的区别是什么?
(1)setState() setState()用于设置状态对象,其语法如下:
setState(object nextState[, function callback])
- nextState,将要设置的新状态,该状态会和当前的state合并
- callback,可选参数,回调函数。该函数会在setState设置成功,且组件重新渲染后调用。
合并nextState和当前state,并重新渲染组件。setState是React事件处理函数中和请求回调函数中触发UI更新的主要方法。
(2)replaceState() replaceState()方法与setState()类似,但是方法只会保留nextState中状态,原state不在nextState中的状态都会被删除。其语法如下:
replaceState(object nextState[, function callback])
- nextState,将要设置的新状态,该状态会替换当前的state。
- callback,可选参数,回调函数。该函数会在replaceState设置成功,且组件重新渲染后调用。
总结: setState 是修改其中的部分状态,相当于 Object.assign,只是覆盖,不会减少原来的状态。而replaceState 是完全替换原来的状态,相当于赋值,将原来的 state 替换为另一个对象,如果新状态属性减少,那么 state 中就没有这个状态了。
受控组件和非受控组件区别是啥?
-
受控组件是 React 控制中的组件,并且是表单数据真实的唯一来源。
-
非受控组件是由 DOM 处理表单数据的地方,而不是在 React 组件中。
尽管非受控组件通常更易于实现,因为只需使用refs
即可从 DOM 中获取值,但通常建议优先选择受控制的组件,而不是非受控制的组件。
这样做的主要原因是受控组件支持即时字段验证,允许有条件地禁用/启用按钮,强制输入格式。
React 中 refs 的作用是什么
Refs
是React
提供给我们的安全访问DOM
元素或者某个组件实例的句柄- 可以为元素添加
ref
属性然后在回调函数中接受该元素在DOM
树中的句柄,该值会作为回调函数的第一个参数返回
useEffect(fn, []) 和 componentDidMount 有什么差异
useEffect
会捕获props
和 state。所以即便在回调函数里,你拿到的还是初始的 props 和 state。如果想得到“最新”的值,可以使用 ref。
Redux 中间件是什么?接受几个参数?柯里化函数两端的参数具体是什么?
Redux 的中间件提供的是位于 action 被发起之后,到达 reducer 之前的扩展点,换而言之,原本 view -→> action -> reducer -> store 的数据流加上中间件后变成了 view -> action -> middleware -> reducer -> store ,在这一环节可以做一些"副作用"的操作,如异步请求、打印日志等。
applyMiddleware源码:
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
// 利用传入的createStore和reducer和创建一个store
const store = createStore(...args)
let dispatch = () => {
throw new Error()
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 接着 compose 将 chain 中的所有匿名函数,组装成一个新的函数,即新的 dispatch
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
从applyMiddleware中可以看出∶
- redux中间件接受一个对象作为参数,对象的参数上有两个字段 dispatch 和 getState,分别代表着 Redux Store 上的两个同名函数。
- 柯里化函数两端一个是 middewares,一个是store.dispatch
react-redux 的实现原理?
通过 redux 和 react context 配合使用,并借助高阶函数,实现了 react-redux
diff 虚拟DOM 比较的规则
-
【旧虚拟DOM】 与 【新虚拟DOM】中相同key
若虚拟DOM中的内容没有发生改变,直接使用旧的虚拟DOM
若虚拟DOM中的内容发生改变了,则生成新真实的DOM,随后替换页面中之前的真实DOM
-
【旧虚拟DOM】 中未找到 与 【新虚拟DOM】相同的key
根据数据创建真实DOM,随后渲染到页面
类组件和函数组件之间的区别是啥?
-
类组件可以使用其他特性,如状态
state
和生命周期钩子。 -
当组件只是接收
props
渲染到页面时,就是无状态组件,就属于函数组件,也被称为哑组件或展示组件。
函数组件和类组件当然是有区别的,而且函数组件的性能比类组件的性能要高,因为类组件使用的时候要实例化,而函数组件直接执行函数取返回结果即可。为了提高性能,尽量使用函数组件。区别 函数组件 类组件 是否有 this
没有 有 是否有生命周期 没有 有 是否有状态 state
没有 有