前言
这次想开一个新系列,能不能从比较浅的角度来说明白react到底在干嘛,而不是动不动就深入到里面说源码,当然研究源码本身没什么问题,但是对于新手来说确实有点困难,希望这个系列的文章能从浅的角度帮助入门react的工程师更好的了解react。
但是浅的角度并不是一无所知,有以下几个前提
- 必须写过react项目
- 熟悉react的常用api
- 有一定的js基础知识
前置知识
什么是jsx
JSX 是 JavaScript 语法的扩展,它用来声明 React 组件的 UI 结构。
- JSX 看起来类似 HTML ,但实际上完全是合法的 JavaScript 代码。
- JSX 会被编译器(如 Babel)编译为 React.createElement() 函数调用。
- JSX 支持嵌入 JavaScript 表达式,可以在 JSX 内直接使用 JavaScript 变量和函数。
JSX就是javascript用来描述React Virtual DOM的一种形式
jsx
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
上面的代码会被babel转换成下面的形式
javascript
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
createElement也就是所谓创建虚拟DOM
什么是虚拟DOM
虚拟dom,Virtual DOM 或者 Virtual Node
- 用JavaScript对象模拟真实DOM和DOM结构。
- React组件的render()方法或者函数组件执行,回一个虚拟DOM树,描述页面应该如何渲染。
- 在状态变更时,重新render()方法或者函数组件执行可以得到新的虚拟DOM树。
- React runtime会通过diff算法比较原始虚拟DOM和新的虚拟DOM树的差异。
- 最后只将变更的内容更新到真实的DOM中,避免不必要的操作, 这样通过虚拟DOM作为中间层,提高了对真实DOM的操作效率。
- 虚拟DOM让编写UI更加方便直观,不用直接操作 DOM。
正文
从两个方面了解生命周期
每次说到react生命周期,网上的文章都是列出一大堆生命周期api,看的我头大。 当然这个也没什么,为了让面试更顺利这api确实需要再面试之前记一下(本人是不喜欢这种方式),所以我会在后面才总结生命周期。 不过如果大家已经写过一到两年的前端,或者说本身就有丰富的开发经验(后端,移动端) 那么往小了看,也就是从组件的角度来看 组件生命周期无非就是三个
- 挂载
- 更新
- 卸载
从大了来看 整个react运行机制上会有三个阶段
- render
- 调用组件的render方法,生成一个虚拟DOM。
- 只在内存里生成虚拟DOM,不改变真实DOM。
- pre-commit
- 在这一步,先把更新前的虚拟DOM记下来,也就是记住更新之前的状态。
- commit
- 这一步才是把虚拟DOM画在网页上,也就是转换为真实DOM。
- 还会先比较新旧虚拟DOM的不同,只更新需要改变的地方,不全部重新画。
react运行时的三个阶段,我们可以看做创建虚拟DOM,对虚拟DOM进行操作,将许虚拟DOM的更新到真实DOM上去。 而从组件的生命周期来看,就是一个组件的出生,成长,死亡。
但是这里要小心在表达的时候,render指的是里面的创建虚拟dom,还是说渲染实际内容到页面(也就是mount到页面上,mount到页面上的流程就包含了,render,pre-commit,commit)

hooks处于生命周期的什么位置
这里我并不想把任何hooks比作任何生命周期的钩子函数,而是直接讲解它是在什么时候执行。
这里也不会讲解hooks的具体用法,只是探讨一下hooks处于生命周期的什么阶段。
在react hooks大行其道的年代,react推崇函数组件,所以老版本利用生命周期钩子函数来写业务的年代也缓慢退出舞台。
不过即使是使用hooks和函数组件,我们难免会和当初的生命周期对应起来。
useEffect和useEffectLayout
比如经典中的经典。
什么时候请求我们的数据
什么时候来取消某个消息(请求,需要退出)的订阅
首先老生常谈的是
我们请求数据会放在useEffect
里面,取消某个功能的订阅会放在useEffect
返回函数之中。
useEffect
返回函数不必多说,一定是组件卸载的时候才会执行,非常容易理解。
那么useEffect
函数本身呢?也就是我们发送请求获取数据的时候。
useEffect的目的是为了执行有副作用的函数,比如异步获取数据,本身就会对ui造成影响,因为本来我们的页面就要根据服务器提供的数据进行渲染。
而react是纯函数为主的UI库,既UI = F(props(state))
我输入一样的props(state)要带来的结果是一样的。
但是我们又必须要异步获取数据,导致函数不是纯函数。
所以我们需要useEffect
这个hooks来处理副作用的操作。
那么useEffect
在哪个位置执行,既能保证纯函数也能保证副作用呢?
这里就比较巧妙了。
首先我们第一次加载页面的时候的确是纯函数,但是页面是没有数据渲染的只有结构的页面,没有服务器数据。
这个时候页面正在发送数据到服务器请求数据,数据来到浏览器。
useEffect
里面重新设置state(其他组件的props),这个时候就相当于其他地方输入了新的state(props),页面重新渲染,获得想要的页面,当然这一个过程非常快,用户根本就没法感知到。
也就是useEffect
是在react的组件渲染到真实页面上之后发生的。
就是为了保证纯函数这一个特点。
在react官网对于useEffect就有明确的执行时机useEffect -- React

明显是在页面渲染在浏览器之后。
在回到前面的说明,useEffect执行是在commit阶段,也就是挂载,更新阶段commit的时候都会执行useEffect。
那和useEffect 类似的还有useLayoutEffect,也是在commit阶段执行。
但是useLayoutEffect 和useEffect本身就有区别,这里我们不做探究,useLayoutEffect页面真实渲染之前执行,useEffect在页面渲染之后执行。
useEffect的clean函数也就是返回的函数什么时候执行。

如果发生了更新,那么就会运行一次clean函数,如果组件被卸载了,那么会运行最后一次。(useLayoutEffect类似)
自此我们学会了最经典的useEffect在生命周期的位置。
图像来自React class and hooks lifecycle explained (hello-js.com)

其他hooks
其他hooks,比如useState(useReducer),useMemo,useContext,useCallback这些常用的hooks又在什么位置呢?
我们从我们写代码的层面来分析。
比较明确的是这些明显都是和state状态有关
状态在react之中可以被更新的,这些hooks在更新阶段根据场景重新执行
比如useState会重新在setState更新之后获取最新的state,useMemo在监听的到依赖变化之后,返回最新的缓存值;useContext在某个state更新之后,引用context的组件获取最新值。
他们都是在内容被渲染到页面执行,当然我们的更新阶段,包含了render(重新创建虚拟dom),pre-commit(获取更新之前的状态)。
图来自 React hooks lifecycle diagram (wavez.github.io)

生命周期函数钩子
终于来到了钩子函数阶段。
关于钩子函数我们需要注意的其实就只有这两个,他们也是react 16新增的函数。
- getDerivedStatefromProps
- getsnapshotbeforeupdate
关于它们的位置非常好记,他们分别在shouldComponentUpdate之前(getDerivedStatefromProps),之后(getsnapshotbeforeupdate还在componentDidmount(componentdidupdate)之前)
这个两个玩意是干嘛的,不过我们先回顾一下其他的钩子,16和16之前依然存在的(被去除的后面有)
- shouldComponentUpdate
- componentDidmount
- componentdidupdate
- componentWillUnmount
通过名字我们就知道他们在哪个周期。
比如
- componentDidmount,接收prevProps, prevState, 在页面内容出现在页面之后,我们一般用来请求数据。
- shouldComponentUpdate,接收nextProps,nextState,返回boolean值,决定组件是否重新渲染。
新的两个周期
-
getSnapshotBeforeUpdate
接收prevProps,prevState, 返回snapshot的值, 组件的虚拟DOM已经重新渲染完成(在shouldComponentUpdate之后),但实际DOM还未开始更新,这里可以获取未更新的dom的值来生成一个snapshot,返回出去的snapshot作为componentDidmount的参数
-
getDerivedStateFromProps:
- 会在调用 render 方法之前被调用,在挂载和更新时都会被调用。也就是在shouldComponentUpdate之前
- 它接收到最新的 props 和当前的 state 作为参数。
- 需要返回一个对象来更新 state,或者返回 null 来表示 state 不需要更新。
- 通过将 props 映射到 state,可以在 props 改变时更新 state。

旧的,不推荐使用三个周期
-
UNSAFE_componentWillReceiveProps(nextProps, nextContext)
- 被 getDerivedStateFromProps 替代
- 它会在已挂载的组件接收到新的 props 之后被调用。
- 可以通过对比 nextProps 和 prevProps 来执行状态更新。
- 典型用法是根据 props 改变来更新 state。
-
UNSAFE_componentWillMount() componentWillMount非常简单,就是在组件挂载之前钩子函数
-
UNSAFE_componentWillUpdate(nextProps, nextState) componentWillUpdate会在组件接收到新的props或state后立即被调用。
为什么这三个周期会被取消,就和最新的fiber架构有关。
这里简单来解释就是fiber可以看成一个一个的节点,每一个渲染的任务就是一个fiber节点,react运行时和根据浏览器空闲的时间以及任务的优先级来执行任务。
但是这个任务是可以被中断的,比如一个任务执行到一半,就退出了。下次回来的要重新开始执行这个任务,这三个周期会被重复执行。
至于为什么这个几个生命周期会被重复执行,其他的不影响,后面新文章再说。