- ⭐️ 本文首发自 前端修罗场(点击加入社区,参与学习打卡,获取奖励),是一个由资深开发者独立运行的专业技术社区,我专注 Web 技术、答疑解惑、面试辅导以及职业发展。。
- 目前就职于全球前100强知名外企,曾就职于头部互联网企业 | 清华大学出版社签约作者 | CSDN 银牌讲师 | 蓝桥云课2021年度人气作者Top2 | CSDN 博客专家 | 阿里云专家博主 | 华为云享专家
出品著作:《ElementUI 详解与实战》| 《ThreeJS 在网页中创建动画》|《PWA 渐进式Web应用开发》- 本文已收录至前端面试题库专栏: 《前端面试宝典》(点击订阅)
- 此专栏文章针对准备找工作的应届生、初中高级前端工程师设计,以及想要巩固前端基础知识的开发者,文章中包含 90% 的面试考点和实际开发中需要掌握的知识,内容按点分类,环环相扣,重要的是,形成了前端知识技能树,多数同学已经通过面试拿到 offer,并提升了自己的前端知识面。
作者对重点考题做了详细解析和重点标注(建议在 PC 上阅读
),并通过图解、思维导图的方式帮你降低了学习成本,节省备考时间,尽可能快地提升。可以说目前市面上没有像这样完善的面试备考指南!- ❤️ 现在订阅专栏,私聊博主,即可享受一次免费的模拟面试、简历修改、答疑服务。拉你进前端答疑互助交流群,享受博主答疑服务和备考服务,优质文章分享。【私聊备注:前端修罗场】。
- 加入前端修罗场,从此快人一步,和一群人一起更进一步!
- 目前优惠中,即将恢复至原价 ~
<div onClick={this.handleClick.bind(this)}>点我div>
React 并不是将 click 事件绑定到了 div 的真实 DOM 上,而是在 document 处监听了所有的事件,当事件发生并且冒泡到 document 处的时候,React 将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。
除此之外,冒泡到 document 上的事件也不是原生的浏览器事件,而是由react自己实现的合成事件(SyntheticEvent
)。因此如果不想要是事件冒泡的话应该调用 event.preventDefault()
方法,而不是调用 event.stopProppagation()
方法。
JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了 document
上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。
另外冒泡到 document
上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation
是无效的,而应该调用 event.preventDefault
。
实现合成事件的目的如下:
区别:
preventDefault()
来阻止默认行为。合成事件是 react 模拟原生 DOM 事件所有能力的一个事件对象,其优点如下:
事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到document 上合成事件才会执行。
React基于 Virtual DOM 实现了一个 SyntheticEvent
层(合成事件层),定义的事件处理器会接收到一个合成事件对象的实例,它符合 W3C 标准,且与原生的浏览器事件拥有同样的接口,支持冒泡机制,所有的事件都自动绑定在最外层上。
在React底层,主要对合成事件做了两件事:
这三者是目前 react 解决代码复用的主要方式:
render props
是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术,更具体的说,render prop 是一个用于告知组件需要渲染什么内容的函数 prop。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的优缺点∶
(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 的优缺点也很明显∶
(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的优点如下∶
需要注意的是:hook 只能在组件顶层使用,不可在分支 \ 循环语句中使用。
总结∶
Hoc、render props 和 hook 都是为了解决代码复用的问题,但是 hoc 和 render props 都有特定的使用场景和明显的缺点。hook 是 react16.8 更新的新的 API,让组件逻辑复用更简洁明了,同时也解决了 hoc 和 render props 的一些缺点。
总结:
React-Fiber 的思想是基于协程
的概念。首先协程式一种让出机制
,它可以将当前正在执行的任务让出,让 CPU 处理其他任务。因此,React-Fiber 基于这个想法,为了在执行渲染时可以合理分配 CPU 资源,将对 DOM 的操作进行了分批延时处理。浏览器如果有高优先级的任务,可以优先处理,处理完再回来处理渲染任务。即可中断的概念。
React 16 开始,采用了 Fiber 机制替代了原有的同步渲染 VDOM 的方案,提高了页面渲染性能和用户体验。
在早期的单任务系统上,用户一次只能提交一个任务,当前运行的任务拥有全部硬件和软件资源,如果任务不主动释放 CPU 控制权,那么将一直占用所有资源,可能影响其他任务。
在没有中断的情况下,当 CPU 在执行一段代码时,如果程序不主动退出(如:一段无限循环代码),那么 CPU 将被一直占用,影响其他任务运行。
中断机制会强制中断当前 CPU 所执行的代码,转而去执行先前注册好的中断服务程序。比较常见的如:时钟中断,它每隔一定时间将中断当前正在执行的任务,并立刻执行预先设置的中断服务程序,从而实现不同任务之间的交替执行,这也是在多任务系统的重要的基础机制。
浏览器中每一帧耗时大概在 16ms
左右,它会经过下面这几个过程:
步骤 4 的 RIC,算是一种防止多余计算资源被浪费的机制,RIC 非常像前面提到的 “中断服务”,而浏览器的每一帧类似 “中断机制”。
React Fiber 是基于自定义一套机制来模拟实现,如:setTimeout、setImmediate、MessageChannel
。
中断服务后,不同任务就能实现间断执行的可能,如何实现多任务的合理调度,就需要一个调度任务来进行处理。在中断后,需要考虑现场保护和现场还原。
早期 React 是同步渲染机制,实际上是一个递归过程,递归可能会带来长的调用栈,这其实会给现场保护和还原变得复杂。
React Fiber 的做法将递归过程拆分成一系列小任务(Fiber),转换成线性的链表结构,此时现场保护只需要保存下一个任务结构信息即可,所以拆分的任务上需要扩展额外信息,该结构记录着任务执行时所需要的必备信息:
{
stateNode,//实例对象
child, //子节点,指向自身下面的第一个fiber
return,//父节点,指向上一个fiber
sibling,//兄弟组件, 指向一个兄弟节点
expirationTime
...
}
跟结构有关系的就三个属性:
当 React 进行渲染时,会生成如下任务链,此时如果在执行任务 B 后时发现时间不足,主动释放后,只需要记录下一次任务 C 的信息,等再次调度时取得上次记录的信息即可。
React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程期间, React 会占据浏览器资源,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿。
为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。
所以从 React16开始, 通过 Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:
核心思想
:Fiber 也称协程(类型 generator
) 或者纤程。它和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的让出机制。让出 CPU 的执行权,让 CPU 能在这段时间执行其他的操作。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。
PureComponent
表示一个纯组件,可以用来优化 React 程序,减少 render 函数执行的次数,从而提高组件的性能。
在React中,当 prop 或者 state 发生变化时,可以通过在 shouldComponentUpdate
生命周期函数中执行return false
来阻止页面的更新,从而减少不必要的 rende r执行。React.PureComponent 会自动执行 shouldComponentUpdate
。
不过,pureComponent 中 的 shouldComponentUpdate() 进行的是浅比较,也就是说如果是引用数据类型的数据,只会比较不是同一个地址,而不会比较这个地址里面的数据是否一致。浅比较会忽略属性和或状态突变情况,其实也就是数据引用指针没有变化,而数据发生改变的时候 render 是不会执行的。 如果需要重新渲染那么就需要重新开辟空间引用数据。PureComponent一般会用在一些纯展示组件上。
使用 pureComponent 的好处:当组件更新时,如果组件的 props 或者 state 都没有改变,render函数就不会触发。省去虚拟 DOM 的生成和对比过程,达到提升性能的目的。这是因为 react 自动做了一层浅比较。
element
是一个普通对象(plain object),描述了对于一个 DOM 节点或者其他组件component
,你想让它在屏幕上呈现成什么样子。元素 element
可以在它的属性 props
中包含其他元素(译注:用于形成元素树)。创建一个 React 元素 element
成本很低。元素 element
创建之后是不可变的。component
可以通过多种方式声明。**可以是带有一个 render()
方法的类,简单点也可以定义为一个函数。**这两种情况下,它都把属性 props
作为输入,把返回的一棵元素树作为输出。instance
是你在所写的组件类 component class
中使用关键字 this
所指向的东西(组件实例)。它用来存储本地状态和响应生命周期事件很有用。函数式组件(Functional component
)根本没有实例instance
。类组件(Class component
)有实例instance
,但是永远也不需要直接创建一个组件的实例,因为React帮我们做了这些。
React.createClass 和 extends Component 的区别主要在于:
(1)语法区别
(2)propType 和 getDefaultProps
(3)状态的区别
(4)this区别
(5)Mixins
React mixins
的特性将不能被使用了。官方解释∶
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
高阶组件(HOC)就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件,它只是一种组件的设计模式,这种设计模式是由 react 自身的组合性质必然产生的。我们将它们称为纯组件,因为它们可以接受任何动态提供的子组件,但它们不会修改或复制其输入组件中的任何行为。
// 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));
1)HOC的优缺点
2)适用场景
3)具体应用例子
// HOC.js
function withAdminAuth(WrappedComponent) {
return class extends React.Component {
state = {
isAdmin: false,
}
async UNSAFE_componentWillMount() {
const currentRole = await getCurrentUserRole();
this.setState({
isAdmin: currentRole === 'Admin',
});
}
render() {
if (this.state.isAdmin) {
return <WrappedComponent {...this.props} />;
} else {
return (<div>您没有权限查看该页面,请联系管理员!</div>);
}
}
};
}
// pages/page-a.js
class PageA extends React.Component {
constructor(props) {
super(props);
// something here...
}
UNSAFE_componentWillMount() {
// fetching data
}
render() {
// render page with data
}
}
export default withAdminAuth(PageA);
// pages/page-b.js
class PageB extends React.Component {
constructor(props) {
super(props);
// something here...
}
UNSAFE_componentWillMount() {
// fetching data
}
render() {
// render page with data
}
}
export default withAdminAuth(PageB);
class Home extends React.Component {
render() {
return (<h1>Hello World.</h1>);
}
}
function withTiming(WrappedComponent) {
return class extends WrappedComponent {
constructor(props) {
super(props);
this.start = 0;
this.end = 0;
}
UNSAFE_componentWillMount() {
super.componentWillMount && super.componentWillMount();
this.start = Date.now();
}
componentDidMount() {
super.componentDidMount && super.componentDidMount();
this.end = Date.now();
console.log(`${WrappedComponent.name} 组件渲染时间为 ${this.end - this.start} ms`);
}
render() {
return super.render();
}
};
}
export default withTiming(Home);
注意:withTiming
是利用 反向继承 实现的一个高阶组件,功能是计算被包裹组件(这里是 Home 组件)的渲染时间。
const withFetching = fetching => WrappedComponent => {
return class extends React.Component {
state = {
data: [],
}
async UNSAFE_componentWillMount() {
const data = await fetching();
this.setState({
data,
});
}
render() {
return <WrappedComponent data={this.state.data} {...this.props} />;
}
}
}
// pages/page-a.js
export default withFetching(fetching('science-fiction'))(MovieList);
// pages/page-b.js
export default withFetching(fetching('action'))(MovieList);
// pages/page-other.js
export default withFetching(fetching('some-other-type'))(MovieList);
该方法当 props
发生变化时执行,初始化 render
时不执行,在这个回调函数里面,你可以根据属性的变化,通过调用 this.setState()
来更新你的组件状态,旧的属性还是可以通过 this.props
来获取,这里调用更新状态是安全的,并不会触发额外的 render
调用。
使用好处:在这个生命周期中,可以在子组件的 render 函数执行前获取新的 props,从而更新子组件自己的 state。 可以将数据请求放在这里进行执行,需要传的参数则从componentWillReceiveProps(nextProps)
中获取。而不必将所有的请求都放在父组件中。于是该请求只会在该组件渲染时才会发出,从而减轻请求负担。componentWillReceiveProps 在初始化 render 的时候不会执行,它会在 Component 接受到新的状态(Props)时被触发,一般用于父组件状态更新时子组件的重新渲染。
(1)哪些方法会触发 react 重新渲染?
setState 是 React 中最常用的命令,通常情况下,执行 setState 会触发 render。但是这里有个点值得关注,执行 setState 的时候不一定会重新渲染。当 setState 传入 null 时,并不会触发 render。
class App extends React.Component {
state = {
a: 1
};
render() {
console.log("render");
return (
<React.Fragement>
<p>{this.state.a}</p>
<button
onClick={() => {
this.setState({ a: 1 }); // 这里并没有改变 a 的值
}}
>
Click me
</button>
<button onClick={() => this.setState(null)}>setState null</button>
<Child />
</React.Fragement>
);
}
}
只要父组件重新渲染了,即使传入子组件的 props 未发生变化,那么子组件也会重新渲染,进而触发 render
(2)重新渲染 render 会做些什么?
React 的处理 render 的基本思维模式是每次一有变动就会去重新渲染整个应用。在 Virtual DOM 没有出现之前,最简单的方法就是直接调用 innerHTML。Virtual DOM 厉害的地方并不是说它比直接操作 DOM 快,而是说不管数据怎么变,都会尽量以最小的代价去更新 DOM。React 将 render 函数返回的虚拟 DOM 树与老的进行比较,从而确定 DOM 要不要更新、怎么更新。当 DOM 树很大时,遍历两棵树进行各种比对还是相当耗性能的,特别是在顶层 setState 一个微小的修改,默认会去遍历整棵树。 尽管 React 使用高度优化的 Diff 算法,但是这个过程仍然会损耗性能。
组件状态的改变可以因为 props
的改变,或者直接通过 setState
方法改变。组件获得新的状态,然后React决定是否应该重新渲染组件。只要组件的state发生变化,React 就会对组件进行重新渲染。这是因为React中的shouldComponentUpdate
方法**默认返回 true
,这就是导致每次更新都重新渲染的原因。
当React将要渲染组件时会执行 shouldComponentUpdate
方法来看它是否返回 true
(组件应该更新,也就是重新渲染)。所以需要重写 shouldComponentUpdate
方法让它根据情况返回 true
或者 false
来告诉React什么时候重新渲染什么时候跳过重新渲染。
React 声明组件的三种方式:
无状态组件
React.createClass
定义的组件extends React.Component
定义的组件(1)无状态函数式组件
它是为了创建纯展示组件,这种组件只负责根据传入的 props 来展示,不涉及到 state 状态的操作
组件不会被实例化,整体渲染性能得到提升,不能访问 this 对象,不能访问生命周期的方法
(2)ES5 原生方式 React.createClass // RFC
React.createClass 会自绑定函数方法,导致不必要的性能开销,增加代码过时的可能性。
(3)E6继承形式 React.Component // RCC
目前极为推荐的创建有状态组件的方式,最终会取代 React.createClass 形式;相对于 React.createClass 可以更好实现代码复用。
无状态组件相对于于后者的区别:
与无状态组件相比,React.createClass 和 React.Component 都是创建有状态的组件,这些组件是要被实例化的,并且可以访问组件的生命周期方法。
React.createClass 与 React.Component 区别:
① 函数 this 自绑定
② 组件属性类型 propTypes 及其默认 props 属性 defaultProps 配置不同
③ 组件初始状态 state 的配置不同