react 面试如何回答才能让面试官满意
React setState 调用的原理
具体的执行过程如下(源码级解析):
首先调用了
setState
入口函数,入口函数在这里就是充当一个分发器的角色,根据入参的不同,将其分发到不同的功能函数中去;
enqueueSetState
方法将新的state
放进组件的状态队列里,并调用enqueueUpdate
来处理将要更新的实例对象;
在
enqueueUpdate
方法中引出了一个关键的对象——batchingStrategy
,该对象所具备的isBatchingUpdates
属性直接决定了当下是要走更新流程,还是应该排队等待;如果轮到执行,就调用batchedUpdates
方法来直接发起更新流程。由此可以推测,batchingStrategy
或许正是 React 内部专门用于管控批量更新的对象。
注意:batchingStrategy
对象可以理解为“锁管理器”。这里的“锁”,是指 React 全局唯一的 isBatchingUpdates
变量,isBatchingUpdates
的初始值是 false
,意味着“当前并未进行任何批量更新操作”。每当 React 调用 batchedUpdate
去执行更新动作时,会先把这个锁给“锁上”(置为 true
),表明“现在正处于批量更新过程中”。当锁被“锁上”的时候,任何需要更新的组件都只能暂时进入 dirtyComponents
里排队等候下一次的批量更新,而不能随意“插队”。此处体现的“任务锁”的思想,是 React 面对大量状态仍然能够实现有序分批处理的基石。
state 和 props 共同点和区别
共同点
state 和 props 的改变都会触发 render 函数(界面会发生改变)
不同点
props 是 readonly(只读),但是 state 是可读可写
props 来自父组件,state 是组件内部的数据对象
React 16 中新生命周期有哪些
关于 React16 开始应用的新生命周期: 可以看出,React16 自上而下地对生命周期做了另一种维度的解读:
Render 阶段:用于计算一些必要的状态信息。这个阶段可能会被 React 暂停,这一点和 React16 引入的 Fiber 架构(我们后面会重点讲解)是有关的;
Pre-commit 阶段:所谓“commit”,这里指的是“更新真正的 DOM 节点”这个动作。所谓 Pre-commit,就是说我在这个阶段其实还并没有去更新真实的 DOM,不过 DOM 信息已经是可以读取的了;
Commit 阶段:在这一步,React 会完成真实 DOM 的更新工作。Commit 阶段,我们可以拿到真实 DOM(包括 refs)。
与此同时,新的生命周期在流程方面,仍然遵循“挂载”、“更新”、“卸载”这三个广义的划分方式。它们分别对应到:
挂载过程:
constructor
getDerivedStateFromProps
render
componentDidMount
更新过程:
getDerivedStateFromProps
shouldComponentUpdate
render
getSnapshotBeforeUpdate
componentDidUpdate
卸载过程:
componentWillUnmount
区分状态和 props
React 事件机制
React 并不是将 click 事件绑定到了 div 的真实 DOM 上,而是在 document 处监听了所有的事件,当事件发生并且冒泡到 document 处的时候,React 将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。
除此之外,冒泡到 document 上的事件也不是原生的浏览器事件,而是由 react 自己实现的合成事件(SyntheticEvent)。因此如果不想要是事件冒泡的话应该调用 event.preventDefault()方法,而不是调用 event.stopProppagation()方法。 JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了 document
上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。
另外冒泡到 document
上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation
是无效的,而应该调用 event.preventDefault
。
实现合成事件的目的如下:
合成事件首先抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力;
对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。
React 高阶组件是什么,和普通组件有什么区别,适用什么场景
官方解释∶
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
高阶组件(HOC)就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件,它只是一种组件的设计模式,这种设计模式是由 react 自身的组合性质必然产生的。我们将它们称为纯组件,因为它们可以接受任何动态提供的子组件,但它们不会修改或复制其输入组件中的任何行为。
1)HOC 的优缺点
优点∶ 逻辑服用、不影响被包裹组件的内部逻辑。
缺点∶hoc 传递给被包裹组件的 props 容易和被包裹后的组件重名,进而被覆盖
2)适用场景
代码复用,逻辑抽象
渲染劫持
State 抽象和更改
Props 更改
3)具体应用例子
权限控制: 利用高阶组件的 条件渲染 特性可以对页面进行权限控制,权限控制一般分为两个维度:页面级别和 页面元素级别
组件渲染性能追踪: 借助父组件子组件生命周期规则捕获子组件的生命周期,可以方便的对某个组件的渲染时间进行记录∶
注意:withTiming 是利用 反向继承 实现的一个高阶组件,功能是计算被包裹组件(这里是 Home 组件)的渲染时间。
页面复用
跨级组件的通信方式?
父组件向子组件的子组件通信,向更深层子组件通信:
使用 props,利用中间组件层层传递,但是如果父组件结构较深,那么中间每一层组件都要去传递 props,增加了复杂度,并且这些 props 并不是中间组件自己需要的。
使用 context,context 相当于一个大容器,可以把要通信的内容放在这个容器中,这样不管嵌套多深,都可以随意取用,对于跨越多层的全局数据可以使用 context 实现。
React 中的 props 为什么是只读的?
this.props
是组件之间沟通的一个接口,原则上来讲,它只能从父组件流向子组件。React 具有浓重的函数式编程的思想。
提到函数式编程就要提一个概念:纯函数。它有几个特点:
给定相同的输入,总是返回相同的输出。
过程没有副作用。
不依赖外部状态。
this.props
就是汲取了纯函数的思想。props 的不可以变性就保证的相同的输入,页面显示的内容是一样的,并且不会产生副作用
Redux 和 Vuex 有什么区别,它们的共同思想
(1)Redux 和 Vuex 区别
Vuex 改进了 Redux 中的 Action 和 Reducer 函数,以 mutations 变化函数取代 Reducer,无需 switch,只需在对应的 mutation 函数里改变 state 值即可
Vuex 由于 Vue 自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的 State 即可
Vuex 数据流的顺序是∶View 调用 store.commit 提交对应的请求到 Store 中对应的 mutation 函数->store 改变(vue 检测到数据变化自动渲染)
通俗点理解就是,vuex 弱化 dispatch,通过 commit 进行 store 状态的一次更变;取消了 action 概念,不必传入特定的 action 形式进行指定变更;弱化 reducer,基于 commit 参数直接对数据进行转变,使得框架更加简易;
(2)共同思想
单—的数据源
变化可以预测
本质上∶ redux 与 vuex 都是对 mvvm 思想的服务,将数据从视图中抽离的一种方案。
在 React 中组件的 props 改变时更新组件的有哪些方法?
在一个组件传入的 props 更新时重新渲染该组件常用的方法是在componentWillReceiveProps
中将新的 props 更新到组件的 state 中(这种 state 被成为派生状态(Derived State)),从而实现重新渲染。React 16.3 中还引入了一个新的钩子函数getDerivedStateFromProps
来专门实现这一需求。
(1)componentWillReceiveProps(已废弃)
在 react 的 componentWillReceiveProps(nextProps)生命周期中,可以在子组件的 render 函数执行前,通过 this.props 获取旧的属性,通过 nextProps 获取新的 props,对比两次 props 是否相同,从而更新子组件自己的 state。
这样的好处是,可以将数据请求放在这里进行执行,需要传的参数则从 componentWillReceiveProps(nextProps)中获取。而不必将所有的请求都放在父组件中。于是该请求只会在该组件渲染时才会发出,从而减轻请求负担。
(2)getDerivedStateFromProps(16.3 引入)
这个生命周期函数是为了替代componentWillReceiveProps
存在的,所以在需要使用componentWillReceiveProps
时,就可以考虑使用getDerivedStateFromProps
来进行替代。
两者的参数是不相同的,而getDerivedStateFromProps
是一个静态函数,也就是这个函数不能通过 this 访问到 class 的属性,也并不推荐直接访问属性。而是应该通过参数提供的 nextProps 以及 prevState 来进行判断,根据新传入的 props 来映射到 state。
需要注意的是,如果 props 传入的内容不需要影响到你的 state,那么就需要返回一个 null,这个返回值是必须的,所以尽量将其写到函数的末尾:
React-Router 的实现原理是什么?
客户端路由实现的思想:
基于 hash 的路由:通过监听
hashchange
事件,感知 hash 的变化改变 hash 可以直接通过 location.hash=xxx
基于 H5 history 路由:
改变 url 可以通过 history.pushState 和 resplaceState 等,会将 URL 压入堆栈,同时能够应用
history.go()
等 API监听 url 的变化可以通过自定义事件触发实现
react-router 实现的思想:
基于
history
库来实现上述不同的客户端路由实现思想,并且能够保存历史记录等,磨平浏览器差异,上层无感知通过维护的列表,在每次 URL 发生变化的回收,通过配置的 路由路径,匹配到对应的 Component,并且 render
Redux 原理及工作流程
(1)原理 Redux 源码主要分为以下几个模块文件
compose.js 提供从右到左进行函数式编程
createStore.js 提供作为生成唯一 store 的函数
combineReducers.js 提供合并多个 reducer 的函数,保证 store 的唯一性
bindActionCreators.js 可以让开发者在不直接接触 dispacth 的前提下进行更改 state 的操作
applyMiddleware.js 这个方法通过中间件来增强 dispatch 的功能
(2)工作流程
const store= createStore(fn)生成数据;
action: {type: Symble('action01), payload:'payload' }定义行为;
dispatch 发起 action:store.dispatch(doSomething('action001'));
reducer:处理 action,返回新的 state;
通俗点解释:
首先,用户(通过 View)发出 Action,发出方式就用到了 dispatch 方法
然后,Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action,Reducer 会返回新的 State
State—旦有变化,Store 就会调用监听函数,来更新 View
以 store 为核心,可以把它看成数据存储中心,但是他要更改数据的时候不能直接修改,数据修改更新的角色由 Reducers 来担任,store 只做存储,中间人,当 Reducers 的更新完成以后会通过 store 的订阅来通知 react component,组件把新的状态重新获取渲染,组件中也能主动发送 action,创建 action 后这个动作是不会执行的,所以要 dispatch 这个 action,让 store 通过 reducers 去做更新 React Component 就是 react 的每个组件。
如何解决 props 层级过深的问题
使用 Context API:提供一种组件之间的状态共享,而不必通过显式组件树逐层传递 props;
使用 Redux 等状态库。
React.createClass 和 extends Component 的区别有哪些?
React.createClass 和 extends Component 的 bai 区别主要在于:
(1)语法区别
createClass 本质上是一个工厂函数,extends 的方式更加接近最新的 ES6 规范的 class 写法。两种方式在语法上的差别主要体现在方法的定义和静态属性的声明上。
createClass 方式的方法定义使用逗号,隔开,因为 creatClass 本质上是一个函数,传递给它的是一个 Object;而 class 的方式定义方法时务必谨记不要使用逗号隔开,这是 ES6 class 的语法规范。
(2)propType 和 getDefaultProps
React.createClass:通过 proTypes 对象和 getDefaultProps()方法来设置和获取 props.
React.Component:通过设置两个属性 propTypes 和 defaultProps
(3)状态的区别
React.createClass:通过 getInitialState()方法返回一个包含初始值的对象
React.Component:通过 constructor 设置初始状态
(4)this 区别
React.createClass:会正确绑定 this
React.Component:由于使用了 ES6,这里会有些微不同,属性并不会自动绑定到 React 类的实例上。
(5)Mixins
React.createClass:使用 React.createClass 的话,可以在创建组件时添加一个叫做 mixins 的属性,并将可供混合的类的集合以数组的形式赋给 mixins。
如果使用 ES6 的方式来创建组件,那么
React mixins
的特性将不能被使用了。
在 React 中组件的 this.state 和 setState 有什么区别?
this.state 通常是用来初始化 state 的,this.setState 是用来修改 state 值的。如果初始化了 state 之后再使用 this.state,之前的 state 会被覆盖掉,如果使用 this.setState,只会替换掉相应的 state 值。所以,如果想要修改 state 的值,就需要使用 setState,而不能直接修改 state,直接修改 state 之后页面是不会更新的。
React setState 调用之后发生了什么?是同步还是异步?
(1)React 中 setState 后发生了什么
在代码中调用 setState 函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。
在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。
如果在短时间内频繁 setState。React 会将 state 的改变压入栈中,在合适的时机,批量更新 state 和视图,达到提高性能的效果。
(2)setState 是同步还是异步的
假如所有 setState 是同步的,意味着每执行一次 setState 时(有可能一个同步代码中,多次 setState),都重新 vnode diff + dom 修改,这对性能来说是极为不好的。如果是异步,则可以把一个同步代码中的多个 setState 合并成一次组件更新。所以默认是异步的,但是在一些情况下是同步的。
setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而不同。在源码中,通过 isBatchingUpdates 来判断 setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。
异步: 在 React 可以控制的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。
同步: 在 React 无法控制的地方,比如原生事件,具体就是在 addEventListener 、setTimeout、setInterval 等事件中,就只能同步更新。
一般认为,做异步设计是为了性能优化、减少渲染次数:
setState
设计为异步,可以显著的提升性能。如果每次调用setState
都进行一次更新,那么意味着render
函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新;如果同步更新了
state
,但是还没有执行render
函数,那么state
和props
不能保持同步。state
和props
不能保持一致性,会在开发中产生很多的问题;
Redux 中间件是怎么拿到 store 和 action? 然后怎么处理?
redux 中间件本质就是一个函数柯里化。redux applyMiddleware Api 源码中每个 middleware 接受 2 个参数, Store 的 getState 函数和 dispatch 函数,分别获得 store 和 action,最终返回一个函数。该函数会被传入 next 的下一个 middleware 的 dispatch 方法,并返回一个接收 action 的新函数,这个函数可以直接调用 next(action),或者在其他需要的时刻调用,甚至根本不去调用它。调用链中最后一个 middleware 会接受真实的 store 的 dispatch 方法作为 next 参数,并借此结束调用链。所以,middleware 的函数签名是({ getState,dispatch })=> next => action。
类组件和函数组件有何不同?
解答
在 React 16.8 版本(引入钩子)之前,使用基于类的组件来创建需要维护内部状态或利用生命周期方法的组件(即componentDidMount
和shouldComponentUpdate
)。基于类的组件是 ES6 类,它扩展了 React 的 Component 类,并且至少实现了render()
方法。
类组件:
函数组件是无状态的(同样,小于 React 16.8 版本),并返回要呈现的输出。它们渲染 UI 的首选只依赖于属性,因为它们比基于类的组件更简单、更具性能。
函数组件:
注意:在 React 16.8 版本中引入钩子意味着这些区别不再适用(请参阅 14 和 15 题)。
Redux 中间件是什么?接受几个参数?柯里化函数两端的参数具体是什么?
Redux 的中间件提供的是位于 action 被发起之后,到达 reducer 之前的扩展点,换而言之,原本 view -→> action -> reducer -> store 的数据流加上中间件后变成了 view -> action -> middleware -> reducer -> store ,在这一环节可以做一些"副作用"的操作,如异步请求、打印日志等。
applyMiddleware 源码:
从 applyMiddleware 中可以看出∶
redux 中间件接受一个对象作为参数,对象的参数上有两个字段 dispatch 和 getState,分别代表着 Redux Store 上的两个同名函数。
柯里化函数两端一个是 middewares,一个是 store.dispatch
什么是上下文 Context
Context 通过组件树提供了一个传递数据的方法,从而避免了在每一个层级手动的传递 props 属性。
用法:在父组件上定义 getChildContext 方法,返回一个对象,然后它的子组件就可以通过 this.context 属性来获取
评论