你好,我是伯约,在开始第 3 讲之前,我先来公布下第 2 讲小作业的答案!

小作业答案

你可以先新建一个目录,就叫 babelTest 吧。

然后新建一个 .babelrc 文件,这个文件是 babel 的配置文件。因为需要自己实现 plugins,所以先写上。

{"plugins": []
}

然后实现一个解析 JSX 文件的插件,取名叫 ./jsx-parser。

module.exports = function () {return {manipulateOptions: function manipulateOptions(opts, parserOpts) {parserOpts.plugins.push("jsx");}};
};

把它加入 plugins 里,就像下面这样:

{"plugins": ["./jsx-parser"]
}

这是第一步,可以识别 jsx 文件,那么第二步来放一下上一讲讲到的源码,新建一个 jsx-plugin 文件,然后把上一讲的源码复制进去。

{"plugins": ["jsx-plugin", "./jsx-parser"]
}

接下来就需要一个 jsx 文件进行试验,新建 hello.jsx,写入下面的内容:

function Test() {return <div><a>Hello~~~</a></div>
}

完工,最后是用 babel 执行它。

babel
bash: command not found: babel

等等?为什么不能执行呢?因为没安装 babel 呀。

npm install babel-cli -g
babel hello.jsx

最后,看看输出,是不是有种彻底征服 JSX 的感觉呀?

接下我们进入正题,一起来聊聊在面试中“如何避免生命周期中的坑?”该如何回答。

破题

关于如何避免生命周期中的坑,你得先去理解问题中的潜台词。“如何避免坑?”更深层的意思是“你蹚过多少坑?”当然,你不可能把每次遇到的 Bug 都一一讲给面试官听,这会显得非常没有重点。

正如前两讲所说,答题的技巧更为重要。不仅需要对知识概念有体系化的认知——“讲概念,说用途,理思路,优缺点,来一遍”,还需要对你长期开发过程中的思考,有经验层面的方法总结。

“如何避免坑?”换种思维思考也就是“为什么会有坑?”在代码编写中,遇到的坑往往会有两种:

  • 在不恰当的时机调用了不合适的代码;

  • 在需要调用时,却忘记了调用。

就好比养花浇水,你需要在恰当的时机做恰当的事。比如在春季和秋季,浇水的时候最好选择在上午和下午;在夏季,适合在早上和晚上浇水;冬季的话,中午最适合浇水。

回到本题,在生命周期中出现的坑,那就一定跟生命周期有关。所以,通过梳理生命周期,明确周期函数职责,确认什么时候该做什么事儿,以此来避免坑。

承题

根据破题的思路,我们需要确立讨论的范围:

  • 基于周期的梳理,确认生命周期函数的使用方式;

  • 基于职责的梳理,确认生命周期函数的适用范围。

以此建立时机与操作的对应关系。

入手

概念

当我们在讨论 React 组件生命周期的时候,一定是在讨论类组件(Class Component)。函数式组件并没有生命周期的概念,因为它本身就是一个函数,只会从头执行到尾。

生命周期是一个抽象的概念,能让开发者产生联想记忆的往往是那些函数,比如 componentDidMount、componentWilMount 等等。然而这些函数并不是它的生命周期,只是在生命周期中按顺序被调用的函数。挂载 -> 更新 -> 卸载这一 React 组件完整的流程,才是生命周期。

流程梳理

接下来我将按照生命周期中函数的调用顺序,逐一列举被调用的函数。

挂载阶段

挂载阶段是指组件从初始化到完成加载的过程。

constructor

constructor 是类通用的构造函数,常用于初始化。所以在过去,constructor 通常用于初始化 state 与绑定函数。常见的写法如下:

import React from 'react'
class Counter extends React.Component {constructor(props) {super(props)this.state = {count: 0,}this.handleClick = this.handleClick.bind(this)}handleClick() {// do some stuff}render() {return null}
}

那为什么会强调过去呢,因为当类属性开始流行之后,React 社区的写法发生了变化,即去除了 constructor。

import React from 'react'
class Counter extends React.Component {state = {count: 0,}

// 类属性第三阶段提案
handleClick = () => {
// do some stuff
}
render() {
return null
}

社区中去除 constructor 的原因非常明确:

  • constructor 中并不推荐去处理初始化以外的逻辑;

  • 本身 constructor 并不属于 React 的生命周期,它只是 Class 的初始化函数;

  • 通过移除 constructor,代码也会变得更为简洁。

getDerivedStateFromProps

本函数的作用是使组件在 props 变化时更新 state,那它什么时候才会起效呢?它的触发时机是:

  • 当 props 被传入时;

  • state 发生变化时;

  • forceUpdate 被调用时。

最常见的一个错误是认为只有 props 发生变化时,getDerivedStateFromProps 才会被调用,而实际上只要父级组件重新渲染时,getDerivedStateFromProps 就会被调用。所以是外部参数,也就是 props 传入时就会发生变化。以下是官方文档中的例子:

class ExampleComponent extends React.Component {// 在构造函数中初始化 state,// 或者使用属性初始化器。state = {isScrollingDown: false,lastRow: null,};

static getDerivedStateFromProps(props, state) {
    if (props.currentRow !== state.lastRow) {
      return {
        isScrollingDown: props.currentRow > state.lastRow,
        lastRow: props.currentRow,
      };
    }

// 返回 null 表示无需更新 state。
    return null;
  }
}

依据官方的说法,它的使用场景是很有限的。由于太多错误使用的案例,React 官方团队甚至写了一篇你可能不需要使用派生 state。文中主要列举了两种反模式的使用方式:

  1. 直接复制 prop 到 state;

  2. 在 props 变化后修改 state。

这两种写法除了增加代码的维护成本外,没有带来任何好处。

UNSAFE_componentWillMount

也就是 componentWillMount,本来用于组件即将加载前做某些操作,但目前被标记为弃用。因为在 React 的异步渲染机制下,该方法可能会被多次调用。

一个常见的错误是 componentWillMount 跟服务器端同构渲染的时候,如果在该函数里面发起网络请求,拉取数据,那么会在服务器端与客户端分别执行一次。所以更推荐在componentDidMount中完成数据拉取操作。

一个良好的设计应该是不让用户有较高的理解成本,而该函数却与之背道而驰**。**所以已被标记弃用,还在用的同学们可以跟着迁出了。

render

render 函数返回的 JSX 结构,用于描述具体的渲染内容。但切记,render 函数并没有真正的去渲染组件,渲染是依靠 React 操作 JSX 描述结构来完成的。还有一点需要注意,render 函数应该是一个纯函数,不应该在里面产生副作用,比如调用 setState 或者绑定事件。

那为什么不能 setState 呢?因为 render 函数在每次渲染时都会被调用,而 setState 会触发渲染,就会造成死循环。

那又为什么不能绑定事件呢?因为容易被频繁调用注册。

componentDidMount

componentDidMount 主要用于组件加载完成时做某些操作,比如发起网络请求或者绑定事件,该函数是接着 render 之后调用的。但 componentDidMount 一定是在真实 DOM 绘制完成之后调用吗?在浏览器端,我们可以这么认为。

但在其他场景下,尤其是 React Native 场景下,componentDidMount 并不意味着真实的界面已绘制完毕。由于机器的性能所限,视图可能还在绘制中。

更新阶段

更新阶段是指外部 props 传入,或者 state 发生变化时的阶段。该阶段我们着重介绍下以下 6 个函数:

UNSAFE_componentWillReceiveProps

该函数已被标记弃用,因为其功能可被函数 getDerivedStateFromProps 所替代。

另外,当 getDerivedStateFromProps 存在时,UNSAFE_componentWillReceiveProps 不会被调用。

getDerivedStateFromProps

同挂载阶段的表现一致。

shouldComponentUpdate

该方法通过返回 true 或者 false 来确定是否需要触发新的渲染。因为渲染触发最后一道关卡,所以也是性能优化的必争之地。通过添加判断条件来阻止不必要的渲染。

React 官方提供了一个通用的优化方案,也就是 PureComponent。PureComponent 的核心原理就是默认实现了shouldComponentUpdate函数,在这个函数中对 props 和 state 进行浅比较,用来判断是否触发更新。

shouldComponentUpdate(nextProps, nextState) {// 浅比较仅比较值与引用,并不会对 Object 中的每一项值进行比较if (shadowEqual(nextProps, this.props) || shadowEqual(nextState, this.state) ) {return true}return false
}

UNSAFE_componentWillUpdate

同样已废弃,因为后续的 React 异步渲染设计中,可能会出现组件暂停更新渲染的情况。

render

同挂载阶段一致

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate 方法是配合 React 新的异步渲染的机制,在 DOM 更新发生前被调用,返回值将作为 componentDidUpdate 的第三个参数。

官方用例如下:

class ScrollingList extends React.Component {constructor(props) {super(props);this.listRef = React.createRef();}getSnapshotBeforeUpdate(prevProps, prevState) {// Are we adding new items to the list?// Capture the scroll position so we can adjust scroll later.if (prevProps.list.length < this.props.list.length) {const list = this.listRef.current;return list.scrollHeight - list.scrollTop;}return null;}componentDidUpdate(prevProps, prevState, snapshot) {// If we have a snapshot value, we've just added new items.// Adjust scroll so these new items don't push the old ones out of view.// (snapshot here is the value returned from getSnapshotBeforeUpdate)if (snapshot !== null) {const list = this.listRef.current;list.scrollTop = list.scrollHeight - snapshot;}}render() {return (<div ref={this.listRef}>{/* ...contents... */}</div>);}

componentDidUpdate

正如上面的案例,getSnapshotBeforeUpdate 的返回值会作为componentDidUpdate的第三个参数使用。

componentDidUpdate****中可以使用 setState,会触发重渲染,但一定要小心使用,避免死循环。

卸载阶段

卸载阶段就容易很多了,只有一个回调函数。

componentWillUnmount

该函数主要用于执行清理工作。一个比较常见的 Bug 就是忘记在 componentWillUnmount 中取消定时器,导致定时操作依然在组件销毁后不停地执行。所以一定要在该阶段解除事件绑定,取消定时器。

小结

依照上面的梳理,你可以建立一个自己的知识导图了。

现在,你有没有发现,React 生命周期下的东西虽然很多,但是很清晰了。基本上看着前面的函数名字,就可以将整个生命周期的内容理下来了。

职责梳理

在梳理整个生命周期之后,需要再强调两个事情。

  • 什么情况下会触发重新渲染。

  • 渲染中发生报错后会怎样?又该如何处理?

如果我们的 React 应用足够复杂、渲染层级足够深时,一次重新渲染,将会消耗非常高的性能,导致卡顿等问题。下面 3 种情况都会触发重新渲染。

函数组件

函数组件任何情况下都会重新渲染。它并没有生命周期,但官方提供了一种方式优化手段,那就是 React.memo。

const MyComponent = React.memo(function MyComponent(props) {/* 使用 props 渲染 */
});

React.memo 并不是阻断渲染,而是跳过渲染组件的操作并直接复用最近一次渲染的结果,这与 shouldComponentUpdate 是完全不同的。

React.Component

如果不实现 shouldComponentUpdate 函数,那么有两种情况触发重新渲染。

  1. 当 state 发生变化时。这个很好理解,是常见的情况。

  2. 当父级组件的 Props 传入时。无论 Props 有没有变化,只要传入就会引发重新渲染。

React.PureComponent

PureComponent 默认实现了 shouldComponentUpdate 函数。所以仅在 props 与 state 进行浅比较后,确认有变更时才会触发重新渲染。

错误边界

错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,如下 React 官方所给的示例:

class ErrorBoundary extends React.Component {constructor(props) {super(props);this.state = { hasError: false };}static getDerivedStateFromError(error) {// 更新 state 使下一次渲染能够显示降级后的 UIreturn { hasError: true };}componentDidCatch(error, errorInfo) {// 你同样可以将错误日志上报给服务器logErrorToMyService(error, errorInfo);}

render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children; 
  }
}

无论是 React,还是 React Native,如果没有错误边界,在用户侧看到的现象会是这样的:在执行某个操作时,触发了 Bug,引发了崩溃,页面突然白屏。

但渲染时的报错,只能通过 componentDidCatch 捕获。这是在做线上页面报错监控时,极其容易忽略的点儿。

答题

经过以上的梳理,我们可以尝试答题了。

避免生命周期中的坑需要做好两件事:
不在恰当的时候调用了不该调用的代码;
在需要调用时,不要忘了调用。

那么主要有这么 7 种情况容易造成生命周期的坑。

getDerivedStateFromProps 容易编写反模式代码,使受控组件与非受控组件区分模糊。

componentWillMount 在 React 中已被标记弃用,不推荐使用,主要原因是新的异步渲染架构会导致它被多次调用。所以网络请求及事件绑定代码应移至 componentDidMount 中。

componentWillReceiveProps 同样被标记弃用,被 getDerivedStateFromProps 所取代,主要原因是性能问题。

shouldComponentUpdate 通过返回 true 或者 false 来确定是否需要触发新的渲染。主要用于性能优化。

componentWillUpdate 同样是由于新的异步渲染机制,而被标记废弃,不推荐使用,原先的逻辑可结合 getSnapshotBeforeUpdate 与 componentDidUpdate 改造使用。

如果在 componentWillUnmount 函数中忘记解除事件绑定,取消定时器等清理操作,容易引发 bug。

如果没有添加错误边界处理,当渲染发生异常时,用户将会看到一个无法操作的白屏,所以一定要添加。

进阶提问

“React 的请求应该放在哪里,为什么?” 这也是经常会被追问的问题。你可以这样回答。

对于异步请求,应该放在 componentDidMount 中去操作。从时间顺序来看,除了 componentDidMount 还可以有以下选择:

  • constructor:可以放,但从设计上而言不推荐。constructor 主要用于初始化 state 与函数绑定,并不承载业务逻辑。而且随着类属性的流行,constructor 已经很少使用了。

  • componentWillMount:已被标记废弃,在新的异步渲染架构下会触发多次渲染,容易引发 Bug,不利于未来 React 升级后的代码维护。

所以React 的请求放在 componentDidMount 里是最好的选择。

总结

本讲主要梳理了 React 的生命周期,也梳理了“坑”在哪里,即不恰当的时机调用了不合适的代码;在需要调用时,却忘记了调用。通过本讲你不仅能掌握 React 的坑在哪里,还可以将这套理论用于同类型问题中去。

本讲中提到了函数组件,在下一讲中将着重讲述它与类组件的区别。

《大前端高薪训练营》


精选评论

ezra.xu:

.babelrc配置文件里的那个jsx-plugin的引用,在windows环境下好像要和jsx-parser一样用相对路径才能正常工作。

**东:

componentWillReceiveProps 能解释以下为什么会有所谓的性能问题。getDerivedStateFromProps也是会在每次组件进行渲染的时候进行调用的,这样根本没区别啊。是因为原来的能调用setState么?

讲师回复:

"componentWillReceiveProps 更新状态是同步进行的,而 getDerivedStateFromProps 的设计更适用于异步场景。但如果只是写写中后台页面,那完全是没有问题的。性能问题常常会出现在深层级的巨型前端应用中,只有在那种场景下讨论才会有意义。即便当下使用不到这些知识,只是记下,未来也会很有帮助的
"

**5467:

不是都在render函数里通过onClick绑定事件的吗?为啥说不能在render函数内绑定事件?

讲师回复:

这里的“绑定事件”,是指 addEventListener 一类的绑定事件。但 bind和箭头函数在 onClick 中绑定事件也会造成一些问题,具体是什么问题呢?可以等到后续的章节,我再来揭晓一下。

*盼:

老师讲课很棒,思路好清晰

编辑回复:

是的,坚持学完奥,你一定会收益颇多。

**鑫:

带我复习了一遍生命周期,很棒

前端面试宝典React篇03 如何避免生命周期中的坑?相关推荐

  1. 第 6 节:前端面试指南 — React 篇(附面试题答案)

    更新完「Vue篇」之后,上周的出差去了一趟南京,很多读者朋友都在微信催更了. 前 5 期没看的同学,建议先看完再来看这一期的,传送门: 第 1 期:「简历篇」含简历模板文件 第 2 期:「HTML篇」 ...

  2. 【前端学习】React学习笔记-事件、生命周期、虚拟DOMdiffing

    跟着尚硅谷的天禹老师学习React 看视频可以直接点击 b站视频地址 React中的事件处理 补充ref 上面的ref在React官网中提到不要被过度使用,在一些情况下可以使用其他方法来获取数据,比如 ...

  3. React 前端面试宝典

    什么是React(谈一谈React) 从概念.用途.思路.优缺点入手: React 是一个网页 UI 框架,通过组件化的方式解决视图层开发复用的问题,本质是一个组件化框架. 它的核心设计思路有三点,分 ...

  4. 前端面试宝典之react篇

    React 中 keys 的作用是什么? Keys 是 React 用于追踪哪些列表中元素被修改.被添加或者被移除的辅助标识 在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性.在 ...

  5. 携手共筑前端面试宝典之JQUERY篇-王大师

    写在前面 本人详解 作者:王文峰,参加过 CSDN 2020年度博客之星,<Java王大师王天师>作者 公众号:山峯草堂,非技术多篇文章,专注于天道酬勤的 Java 开发问题.中国国学.传 ...

  6. 【前端面试宝典】计算机网络篇(1)

    写在前面 CSDN话题挑战赛第1期 活动详情地址:https://marketing.csdn.net/p/bb5081d88a77db8d6ef45bb7b6ef3d7f 参赛话题:前端面试宝典 话 ...

  7. 中高级前端面试宝典之浏览器篇

    中高级前端面试宝典 作为一名前端开发工程师,要掌握的知识点是多而杂的,在面试刷题阶段,经常没头没脑的,我将面试题系统化,分了好几个系列,祝愿大家(包括我)在这个疫情刚过去的互联网寒冬找到事儿少钱多

  8. 前端面试宝典。向未来开启计划

    写在前面 CSDN话题挑战赛第1期 活动详情地址:CSDN 参赛话题:前端面试宝典 话题描述:欢迎各位加入话题创作得小伙伴,如果我没有猜错得话,我觉得你是应该同我一样是一位前端人.如今前端在IT事业中 ...

  9. 【前端面试宝典】超基础的vue知识

    写在前面 CSDN话题挑战赛第1期 活动详情地址:https://marketing.csdn.net/p/bb5081d88a77db8d6ef45bb7b6ef3d7f 参赛话题:前端面试宝典 话 ...

  10. < CSDN话题挑战赛第1期 - 前端面试宝典 >

    CSDN话题挑战赛第1期 活动详情地址:https://marketing.csdn.net/p/bb5081d88a77db8d6ef45bb7b6ef3d7f 参赛话题:前端面试宝典 话题描述:欢 ...

最新文章

  1. java 判断数的位数_Java判断数字位数的两种方法
  2. 团队冲刺第二阶段-2
  3. 设计模式之Prototype(原型)(转)
  4. NIO原理及案例使用
  5. SVN的使用(服务端与客户端)
  6. 济南泉水与城市生态主题 第四届泉水文化论坛第二次会议
  7. 龙图 VP 李翀:数据化运营及云计算下的运维
  8. jQuery中的视图样式和动画效果
  9. SQL SERVER2008 存储过程、表、视图、函数的权限
  10. 【Elasticsearch】明明存在,怎么搜索不出来呢?
  11. 风控必须了解的报表权限与角色控制
  12. 表达式求值(NOIP2013 普及组第二题)
  13. Android的SharedPreferences存取String和List<String>类型(在Activity和Fragment内使用)
  14. setTimeout(), nextTick(),setImmediate()区别 ZT~
  15. IT人的职业生涯规划
  16. ant design table样式修改合集
  17. cs服务器搭建(cobaltstrike)
  18. react中使用阿里Viser图表
  19. python签到脚本_基于Python实现签到脚本过程解析
  20. Drools LHS与RHS常见语法

热门文章

  1. 判断入射满射c语言编码,数学上可以分三类函数包括() 答案:单射双射满射...
  2. 搞Java的年薪 40W 是什么水平? 1
  3. 《Say As You Wish: Fine-grained Control of Image Caption Generation with Abstract Scene Graphs》阅读笔记
  4. php orm中关联查询,【整理】Laravel中Eloquent ORM 关联关系的操作
  5. 找1到n中缺失的数字(长度为n-1的整形数组,数字的范围在1到n,找其中一个缺失的数字)
  6. 关于将数据写入文件的两个函数fwrite()与fprintf()的大不同 —————— 开开开山怪
  7. springboot2 集成支付宝支付
  8. app installation failed 的问题的解决过程
  9. python中seaborn报错These `style` levels are missing dashes解决办法
  10. 郸城二高2021年高考成绩查询,郸城几所高中高考成绩汇总!有你们村的没?