一天梳理完react面试高频知识点
描述事件在 React中的处理方式。
为了解决跨浏览器兼容性问题, React中的事件处理程序将传递 SyntheticEvent的实例,它是跨浏览器事件的包装器。这些 SyntheticEvent与你习惯的原生事件具有相同的接口,它们在所有浏览器中都兼容。
React实际上并没有将事件附加到子节点本身。而是通过事件委托模式,使用单个事件监听器监听顶层的所有事件。这对于性能是有好处的。这也意味着在更新DOM时, React不需要担心跟踪事件监听器。
React 中的key是什么?为什么它们很重要?
key可以帮助 React跟踪循环创建列表中的虚拟DOM元素,了解哪些元素已更改、添加或删除。
每个绑定key的虚拟DOM元素,在兄弟元素之间都是独一无二的。在 React的和解过程中,比较新的虛拟DOM树与上一个虛拟DOM树之间的差异,并映射到页面中。key使 React处理列表中虛拟DOM时更加高效,因为 React可以使用虛拟DOM上的key属性,快速了解元素是新的、需要删除的,还是修改过的。如果没有key,Rεat就不知道列表中虚拟DOM元素与页面中的哪个元素相对应。所以在创建列表的时候,不要忽略key。
react旧版生命周期函数
初始化阶段
getDefaultProps
:获取实例的默认属性getInitialState
:获取每个实例的初始化状态componentWillMount
:组件即将被装载、渲染到页面上render
:组件在这里生成虚拟的DOM
节点componentDidMount
:组件真正在被装载之后
运行中状态
componentWillReceiveProps
:组件将要接收到属性的时候调用shouldComponentUpdate
:组件接受到新属性或者新状态的时候(可以返回false,接收数据后不更新,阻止render
调用,后面的函数不会被继续执行了)componentWillUpdate
:组件即将更新不能修改属性和状态render
:组件重新描绘componentDidUpdate
:组件已经更新
销毁阶段
componentWillUnmount
:组件即将销毁
React中Diff算法的原理是什么?
原理如下。
(1)节点之间的比较。
节点包括两种类型:一种是 React组件,另一种是HTML的DOM。
如果节点类型不同,按以下方式比较。
如果 HTML DOM不同,直接使用新的替换旧的。如果组件类型不同,也直接使用新的替换旧的。
如果 HTML DOM类型相同,按以下方式比较。
在 React里样式并不是一个纯粹的字符串,而是一个对象,这样在样式发生改变时,只需要改变替换变化以后的样式。修改完当前节点之后,递归处理该节点的子节点。
如果组件类型相同,按以下方式比较。
如果组件类型相同,使用 React机制处理。一般使用新的 props替换旧的 props,并在之后调用组件的 componentWillReceiveProps方法,之前组件的 render方法会被调用。
节点的比较机制开始递归作用于它的子节点。
(2)两个列表之间的比较。
一个节点列表中的一个节点发生改变, React无法很妤地处理这个问题。循环新旧两个列表,并找出不同,这是 React唯一的处理方法。
但是,有一个办法可以把这个算法的复杂度降低。那就是在生成一个节点列表时给每个节点上添加一个key。这个key只需要在这一个节点列表中唯一,不需要全局唯一。
(3)取舍
需要注意的是,上面的启发式算法基于两点假设。
类型相近的节点总是生成同样的树,而类型不同的节点也总是生成不同的树
可以为多次 render都表现稳定的节点设置key。
上面的节点之间的比较算法基本上就是基于这两个假设而实现的。要提高 React应用的效率,需要按照这两点假设来开发。
请说岀 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版本中,作用域是可以改变的。
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可以看成是
componentDidMount
,componentDidUpdate
和componentWillUnmount
三者的结合。useEffect(callback, [source])接收两个参数,调用方式如下
useEffect(() => {
console.log('mounted');
return () => {
console.log('willUnmount');
}
}, [source]);
生命周期函数的调用主要是通过第二个参数[source]来进行控制,有如下几种情况:
[source]
参数不传时,则每次都会优先调用上次保存的函数中返回的那个函数,然后再调用外部那个函数;[source]
参数传[]时,则外部的函数只会在初始化时调用一次,返回的那个函数也只会最终在组件卸载时调用一次;[source]
参数有值时,则只会监听到数组中的值发生变化后才优先调用返回的那个函数,再调用外部的函数。
react-router里的<Link>
标签和<a>
标签有什么区别
对比
<a>
,Link
组件避免了不必要的重渲染
react 的渲染过程中,兄弟节点之间是怎么处理的?也就是key值不一样的时候
通常我们输出节点的时候都是map一个数组然后返回一个
ReactNode
,为了方便react
内部进行优化,我们必须给每一个reactNode
添加key
,这个key prop
在设计值处不是给开发者用的,而是给react用的,大概的作用就是给每一个reactNode
添加一个身份标识,方便react进行识别,在重渲染过程中,如果key一样,若组件属性有所变化,则react
只更新组件对应的属性;没有变化则不更新,如果key不一样,则react先销毁该组件,然后重新创建该组件
什么是 React的refs?为什么它们很重要
refs允许你直接访问DOM元素或组件实例。为了使用它们,可以向组件添加个ref属性。
如果该属性的值是一个回调函数,它将接受底层的DOM元素或组件的已挂载实例作为其第一个参数。可以在组件中存储它。
export class App extends Component {
showResult() {
console.log(this.input.value);
}
render() {
return (
<div>
<input type="text" ref={(input) => (this.input = input)} />
<button onClick={this.showResult.bind(this)}>展示结果</button>
</div>
);
}
}
如果该属性值是一个字符串, React将会在组件实例化对象的refs属性中,存储一个同名属性,该属性是对这个DOM元素的引用。可以通过原生的 DOM API操作它。
export class App extends Component {
showResult() {
console.log(this.refs.username.value);
}
render() {
return (
<div>
<input type="text" ref="username" />
<button onClick={this.showResu1t.bind(this)}>展示结果</button>
</div>
);
}
}
useEffect(fn, []) 和 componentDidMount 有什么差异
useEffect
会捕获props
和 state。所以即便在回调函数里,你拿到的还是初始的 props 和 state。如果想得到“最新”的值,可以使用 ref。
在 ReactNative中,如何解决 adb devices找不到连接设备的问题?
在使用 Genymotion时,首先需要在SDK的 platform-tools中加入环境变量,然后在 Genymotion中单击 Setting,选择ADB选项卡,单击 Use custom Android SDK tools,浏览本地SDK的位置,单击OK按钮就可以了。启动虛拟机后,在cmd中输入 adb devices可以查看设备。
我现在有一个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>
}
这段代码有什么问题?
class App extends Component {
constructor(props) {
super(props);
this.state = {
username: "有课前端网",
msg: " ",
};
}
render() {
return <div> {this.state.msg}</div>;
}
componentDidMount() {
this.setState((oldState, props) => {
return {
msg: oldState.username + " - " + props.intro,
};
});
}
}
render ( < App intro=" 前端技术专业学习平台">,ickt )
在页面中正常输出“有课前端网-前端技术专业学习平台”。但是这种写法很少使用,并不是常用的写法。React允许对 setState方法传递一个函数,它接收到先前的状态和属性数据并返回一个需要修改的状态对象,正如我们在上面所做的那样。它不但没有问题,而且如果根据以前的状态( state)以及属性来修改当前状态,推荐使用这种写法。
什么是纯函数?
纯函数是不依赖并且不会在其作用域之外修改变量状态的函数。本质上,纯函数始终在给定相同参数的情况下返回相同结果。
key的作用
是给每一个 vnode 的唯一 id,可以依靠 key,更准确,更快的拿到 oldVnode 中对应的 vnode 节点
<!-- 更新前 -->
<div>
<p key="ka">ka</p>
<h3 key="song">song</he>
</div>
<!-- 更新后 -->
<div>
<h3 key="song">song</h3>
<p key="ka">ka</p>
</div>
如果没有 key,React 会认为 div 的第一个子节点由 p 变成 h3,第二个子节点由 h3 变成 p,则会销毁这两个节点并重新构造。
但是当我们用 key 指明了节点前后对应关系后,React 知道 key === "ka"
的 p 更新后还在,所以可以复用该节点,只需要交换顺序。
key 是 React 用来追踪哪些列表元素被修改、被添加或者被移除的辅助标志。
在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。在 React diff 算法中,React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重新渲染。同时,React 还需要借助 key 来判断元素与本地状态的关联关系。
setState方法的第二个参数有什么用?使用它的目的是什么?
它是一个回调函数,当 setState方法执行结束并重新渲染该组件时调用它。在工作中,更好的方式是使用 React组件生命周期之——“存在期”的生命周期方法,而不是依赖这个回调函数。
export class App extends Component {
constructor(props) {
super(props);
this.state = {
username: "雨夜清荷",
};
}
render() {
return <div> {this.state.username}</div>;
}
componentDidMount() {
this.setstate(
{
username: "有课前端网",
},
() => console.log("re-rendered success. ")
);
}
}
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
在React中页面重新加载时怎样保留数据?
这个问题就设计到了数据持久化, 主要的实现方式有以下几种:
- Redux: 将页面的数据存储在redux中,在重新加载页面时,获取Redux中的数据;
- data.js: 使用webpack构建的项目,可以建一个文件,data.js,将数据保存data.js中,跳转页面后获取;
- sessionStorge: 在进入选择地址页面之前,componentWillUnMount的时候,将数据存储到sessionStorage中,每次进入页面判断sessionStorage中有没有存储的那个值,有,则读取渲染数据;没有,则说明数据是初始化的状态。返回或进入除了选择地址以外的页面,清掉存储的sessionStorage,保证下次进入是初始化的数据
- history API: History API 的
pushState
函数可以给历史记录关联一个任意的可序列化state
,所以可以在路由push
的时候将当前页面的一些信息存到state
中,下次返回到这个页面的时候就能从state
里面取出离开前的数据重新渲染。react-router 直接可以支持。这个方法适合一些需要临时存储的场景。
diff 算法?
- 把树形结构按照层级分解,只比较同级元素
- 给列表结构的每个单元添加唯一的 key 属性,方便比较
- React 只会匹配相同 class 的 component(这里面的 class 指的是组件的名字)
- 合并操作,调用 component 的 setState 方法的时候, React 将其标记为 dirty.到每一个 事件循环结束, React 检查所有标记 dirty 的 component 重新绘制.
- 选择性子树渲染。开发人员可以重写 shouldComponentUpdate 提高 diff 的性能。
新版生命周期
在新版本中,React 官方对生命周期有了新的 变动建议:
- 使用
getDerivedStateFromProps
替换componentWillMount;
- 使用
getSnapshotBeforeUpdate
替换componentWillUpdate;
- 避免使用
componentWillReceiveProps
;
其实该变动的原因,正是由于上述提到的
Fiber
。首先,从上面我们知道 React 可以分成reconciliation
与commit
两个阶段,对应的生命周期如下:
reconciliation
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
commit
componentDidMount
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时,必须加条件,否则将进入死循环;
- getSnapshotBeforeUpdate(prevProps, prevState)可以在更新之前获取最新的渲染数据,它的调用是在 render 之后, update 之前;
- shouldComponentUpdate: 默认每次调用setState,一定会最终走到 diff 阶段,但可以通过shouldComponentUpdate的生命钩子返回false来直接阻止后面的逻辑执行,通常是用于做条件渲染,优化渲染的性能。