说在前面的事
React 16.8 版本正式发布了 Hook 机制,所以在设计到React的一些知识点的时候会有 Class Component 和 Function Component 的区别。
对 React 的理解
- 用于构建界面的JavaScript库
- 使用虚拟DOM来有效操作DOM
- 一切皆为组件
- 遵循自顶向下的单项数据流
生命周期
Class Component 生命周期
React 生命周期指的是从创建到卸载的整个过程,每个过程都有对应的钩子函数会被调用。主要有以下几个阶段:
- 挂载阶段 - 组件实例被创建和插入 DOM 树的过程
- 更新阶段 - 组件被重新渲染的过程
- 卸载阶段 - 组件从 DOM 树被销毁的过程
说在前面的事
React 16.3 版本开始,对生命周期钩子函数进行渐进式的调整。分别废弃和新增了一些钩子函数。
新旧生命周期对比
Old lifecycle
挂载
- constructor
- componentWillMount
- render
- componentDidMount
更新
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
卸载
- componentWillUnmount
New lifecycle
挂载
- constructor
- getDerivedStateFromProps
- render
- componentDidMount
更新
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
卸载
- componentWillUnmount
从以上生命周期的对比,可以看出 React 16.3 开始,
废弃了 - componentWillMount、componentWillReceiveProps、componentDidUpdate 三个钩子函数。
新增了 - getDerivedStateFromProps、getSnapshotBeforeUpdate 两个钩子函数
废弃原因
在filber机制下,生命周期分为render和commit阶段。render时是可以被打断的、重新渲染并终止的。
当一个任务执行到一半被打断后,下一次渲染线程抢回主动权时,这个任务被重启的形式是 重复执行一遍整个任务 而非 接着上次执行到的地方继续执行。
你会发现废弃的三个钩子函数都是在render之前,所以可能因为render重启而导致这三个钩子函数多次执行。
constructor()
constructor()早组件挂载之前被调用
作用:初始化props,初始化state,绑定this。
- 若类组件只需要初始化props,则 constructor 可以不写。
- 若还做了别的事,则 constructor 不能省略。
super的作用:将父类的this对象继承给子类
class Welcome extends React.Component {
/*
如果没有额外的代码,这三行可省略
不写也会默认初始化 props 的数据
*/
constructor(props) {
super(props); // 初始化props
}
}
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = { // 初始化state
count: 0;
}
}
}
static getDerivedStateFromProps(nextProps, state)
getDerivedStateFromProps() 在调用 render方法之前调用,在初始化和后续更新都会被调用。
/*
参数:
第一个参数为即将更新的 `props`, 第二个参数为上一个状态的 `state`。
可以比较 `props` 和 `state`来加一些限制条件,防止无用的state更新
返回值:
返回一个对象来更新 `state`, 如果返回 `null` 则不更新任何内容
*/
static getDerivedStateFromPorps (nextProps,prevState){
if(nextProps.tab!=prevState.tab){
return {
tab:nextProps.tab
};
}
return null;
}
render()
render() 方法是class组件中唯一必须实现的方法,用于渲染dom,返回一个虚拟DOM对象。
这个虚拟DOM对象只能有一个根元素。如果需要返回两个根元素,需要使用<React.Fragment>包起来。(<React.Fragment>可以简写成<></>。这个包裹元素的标签只是占位符,并不会被渲染到页面上。)
// 一个根元素
render() {
return {
<div></div>
}
}
// 两个根元素
render() {
return {
<>
<div></div>
<div></div>
</>
}
}
componentDidMount()
componentDidMount() 在组件挂载后 (插入DOM树后) 立即调用,此时组件已经出现在页面(操作依赖的DOM最好在此处进行)
componentDidMount() 是发送网络请求、启用事件监听方法的好时机,并且可以在 此钩子函数里直接调用 setState()。
【问题】为什么数据获取要在componentDidMount中进行?
【参考】react.js - 为什么废弃react生命周期函数?
shouldComponentUpdate(nextProps, nextState)
shouldComponentUpdate() 在组件更新之前调用,可以控制组件是否进行更新, 返回true时组件更新, 返回false则不更新。
注意
- 不建议在
shouldComponentUpdate()中进行深层比较或使用JSON.stringify()。这样非常影响效率,且会损害性能。 - 不建议在
shouldComponentUpdate()中进行深层比较或使用JSON.stringify()。这样非常影响效率,且会损害性能
getSnapshotBeforeUpdate(prevProps, prevState)
在最近一次的渲染输出被提交之前调用。也就是说,在 render 之后,即将对组件进行挂载时调用。
componentDidUpdate(prevProps, prevState, snapshot)
参考:深入详解React生命周期 - 掘金 (juejin.cn)
componentWillUnmount()
组件销毁时调用
注意
- 如果你在 componentDidMount 里面监听了 window scroll , 那么你就要在 componentWillUnmount里面取消监听
- 如果你在 componentDidMount 里面创建了 Timer, 那么你就要在 componentWillUnmount 里面取消 Timer
- 如果你在 componentDidMount里面创建了AJAX请求, 那么你就要在 componentWillUnmount里面取消请求
Function Component 生命周期
在Function Component中摒弃了生命周期函数这个概念,改称为Hooks。
React Hooks 是 React 16.8 引入的新特性。是一组使函数组件能够拥有转态和其他React特性的API,可以在不编写类组件的情况下使用React的功能。
React Hooks 包括以下API:
- useState:添加状态
- useEffect:添加副作用
- useContext:访问context
- useReducer:管理复杂的状态
- useCallback:缓存回调函数,避免不必要的重新渲染
- useMemo:缓存值,避免不必要的重新计算
- useRef:存储可变的值
useState
useState 用于在函数组件中调用给组件添加一些内部状态 state。
import React, { useState } from 'react';
const [state, setState] = useState(initialState);
useEffect
useEffect 接收一个包含命令式、且可能有副作用代码的函数。
useEffect函数会在浏览器完成布局和绘制之后,下一次重新渲染之前执行,保证不会阻塞浏览器对屏幕的更新。
import React, { useEffect } from 'react';
useEffect(() => {});
清除effect
通常情况下,组件卸载时需要清除 effect 创建的副作用操作,useEffect Hook 函数可以返回一个清除函数,清除函数会在组件卸载前执行。组件在多次渲染中都会在执行下一个 effect 之前,执行该函数进行清除上一个 effect
import React, { useState, useEffect } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1)
}, 1000);
// 返回一个清除函数,在组件卸载和洗衣歌effect执行前执行
return () => {
clearInterval(timer);
}
}, [])
}
export default Counter
useEffect可以接收第二个参数。它是 effect 所依赖的值数组,这样就只有当数组值发生变化才会重新创建订阅。但需要注意的是:
- 确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量
- 传递一个空数组作为第二个参数可以使 effect 只会在初始渲染完成后执行一次
useContext
Context 提供了一个无需为每层组件手动添加 props ,就能在组件树间进行数据传递的方法,useContext 用于函数组件中订阅上层 context 的变更,可以获取上层 context 传递的 value prop 值
useContext 接收一个 context 对象(React.createContext的返回值)并返回 context 的当前值,当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
import React, { useState, useContext } from 'react'
const themes = {
dark: {
background: '#000000'
},
light: {
background: '#ffffff'
}
}
// 为当前的theme创建一个context
const themeContext = React.useContext();
export default function parentComp() {
const [theme, setTheme] = useState(themes.dark);
const changeTheme = () => {
setTheme(currentTheme => {
currentTheme === theme.dark ? theme.light : theme.dark
})
}
return (
// 使用 Provider 将当前 props.value 传递给内部组件
<ThemeContext.Provider value={{theme, changeTheme}} >
<ChildComp />
</themeContext.Provider>
)
}
function ChildComp() {
// 通过 useContext 获取当前 context 值
const { theme, changeTheme } = useContext(themeContext);
return (
<button style={{background: theme.background}} onClick={changeTheme}>
change theme
</button>
)
}
useReducer
useReducer 作为 useState 的代替方案。在 state 逻辑较复杂且包含多个子值,或下一个 state 依赖 之前的 state 等。
const [state, dispatch] = useReducer(reducer, initialArg, init);
import React, { useReducer } from 'react'
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
useRef
- 绑定DOM元素
- 绑定可变值
useCallback
useCallback 结合 React.memo() 是性能优化常见的方式。可以避免由于父组件状态变更,导致不必要的子组件进行重新渲染。
import React, { useState, useCallback } from 'react';
import Button from './Button';
export default function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const handleClickButton1 = () => {
setCount1(count1 + 1);
};
const handleClickButton2 = useCallback(() => {
setCount2(count2 + 1);
}, [count2]);
return (
<div>
<Button onClickButton={handleClickButton1}>Button1</Button>
<Button onClickButton={handleClickButton2}>Button2</Button>
</div>
);
}
// Button.jsx
import React from 'react';
const Button = ({ onClickButton, children }) => {
return (
<>
<button onClick={onClickButton}>{children}</button>
<span>{Math.random()}</span>
</>
);
};
export default React.memo(Button);
- 当点击Button1时,只会更新Button1组件的内容
- 当点击Button2时,两个按钮组件都会被更新
这里或许会注意到 Button 组件的 React.memo 这个方法,此方法内会对 props 做一个浅层比较,如果如果 props 没有发生改变,则不会重新渲染此组件。
对于 Button 组件都需要一个onClickButton的属性,这个属性直接定义为一个方法,每当父组件重新渲染时,这里就会声明出一个新的方法。所以,尽管子组件使用了 React.memo() 还是会重新渲染
对于 Button2 而言,使用 useCallback 进行包装,并且传入了 count2 变量。这就意味着,会根据 count2 是否改变,来决定这里的函数是否被重新声明。
useMemo
与 useCallback 有异曲同工之妙,针对传入子组件的值进行缓存优化
// ...
const [count, setCount] = useState(0);
const userInfo = useMemo(() => {
return {
// ...
name: "Jace",
age: count
};
}, [count]);
return <UserCard userInfo={userInfo}>
上面的代码,只有当 count发生改变之后,才会返回新的对象。
但是 useMemo 的应用不止于此,应用范围要比 useCallback 要广泛得多。可以把一些耗时的计算逻辑放到 useMemo 中,只有当依赖值发生改变时,才会重新去执行这个计算逻辑。
当把返回值改成返回函数时,变通的实现了 useCallback。
useCallback 与 useMemo 的对比
- 缓存
useCallback缓存的是函数useMemo缓存的是函数的返回值
- 组件优化
useCallback是优化子组件的useMemo既可以优化当前组件又可以优化子组件。- 优化当前组件主要依靠通过缓存一些复杂的计算逻辑
useLayoutEffect - todo
哪个生命周期发送异步请求
- componentDidMount。
参考这个 react.js - 为什么废弃react生命周期函数?
JSX 与 JS
JSX 全称 Javascript XML, 是react定义的一种JS的扩展语法。
浏览器不能识别JSX语法,需要通过babel转义成JS。
基本规则:
- 遇到 < 开头的代码, 以标签的语法解析: html同名标签转换为html同名元素, 其它标签需要特别解析
- 遇到以 { 开头的代码,以JS语法解析: 标签中的js表达式必须用{ }包含
- 样式的类名指定不要用class,要用className,内联样式,要用style={{key:value}}的形式去写
- 标签首字母,若是小写字母开头,则将标签转为html中同名元素,若html中无该标签对应的同名元素,则报错;若大写字母开头,react就去渲染对应的组件,若组件没有定义,则报错。
在React中,元素和组件有什么区别
- 元素是页面中DOM元素的对象表示方式
- 组件是一个函数或者一个类
state和props的区别
- state 是组件自己管理的数据,控制自己的状态,可变
- 没有 state 的叫无状态组件,有 state 的叫有状态组件
- props 是外部传入的数据,不可变。
props变动,是否会引起 state hook 中数据的变动
React组件的 props 更新,会重新渲染组件,但是不会引起 state 数据的变动。state 的值只能由 setState 控制。
若想在 props 更新时触发对应的 state 更新,则需要在 useEffect 中监听 props,并在函数中对 state 进行操作
const App = props => {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(0)
}, [props])
}
React组件的通信方式
这个问题也可以换一种方式提问:数据如何在React组件中流动?
参考:构建一个react项目 - react的通信方式 - 掘金 (juejin.cn)
React18 的新变化
- 并发渲染机制
- 根据用户的设备性能和网速对渲染过程进行适当的调整, 保证 React 应用在长时间的渲染过程中依旧保持可交互性,避免页面出现卡顿或无响应的情况,从而提升用户体验。
- 新的创建方式
- 先通过
ReactDOM.createRoot()创建一个 root 节点,然后该 root 节点来调用render()方法 - 以前的是直接通过
ReactDOM.render()方法创建
// 新的创建方式 const root = ReactDOM.createRoot(rootElement); root.render(<App />); // 旧的创建方式 ReactDOM.render(rootElement, <App />); - 先通过
- 自动批处理优化
- 批处理:React 将多个状态更新分组到一个重新渲染中以获得更好的性能。
- 在 V18 以前,只能在事件处理函数中实现批处理。在 v18 中,所有更新都将自动批处理。
并发模式如何执行的
React 中的并发,并不是指同一时刻同时在做多件事情。因为 js 本身就是单线程的(同一时间只能执行一件事情),而且还要跟 UI 渲染竞争主线程。
若一个很耗时的任务占据了线程,那么后续的执行内容都会被阻塞。
为了避免这种情况,React 就利用 fiber 结构和时间切片的机制,将一个大任务分解成多个小任务,然后按照任务的优先级和线程的占用情况,对任务进行调度。
- 对于每个更新,为其分配一个优先级 lane,用于区分其紧急程度。
- 通过 Fiber 结构将不紧急的更新拆分成多段更新,并通过宏任务的方式 将其合理分配到浏览器的帧当中。这样就能使得紧急任务能够插入进来。
- 高优先级的更新会打断低优先级的更新,等高优先级更新完成后,再开始 低优先级更新。
受控组件和非受控组件
- 受控组件:受 React 控制的组件。当组件状态发生变化时,都会通知 React 进行处理,比如可以使用
useState存储 - 非受控组件:如 input/textarea/select/checkbox等组件,本身就能控制数据和状态的变更,数据存储在 DOM 中而不是组件内。可以使用
ref从DOM中获取元素数据
const App = () => {
const [inputVal, setInputVal] = useState(0);
const eleRef = useRef(null);
const handleValue = () => {
console.log(eleRef.current?.value)
}
return (
<>
// 受控组件
<input value={inputVal} onChange={(e) => {
setInputVal(e.target.value)
}}/>
// 非受控组件
<input defaultValue='0' ref={eleRef}/>
<button onClick={handleValue}>点击提交</button>
</>
)
}
React事件机制
- React 所有事件都挂载在 document 对象上 (减少内存开销就是因为所有的事件都绑定在 document 上,其他节点没有绑定事件)
- 当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件
- 会先执行原生事件,然后处理 React 事件
- 最后真正执行 document 上挂载的事件
React中的Key
而元素key属性的作用是用于判断元素是新创建的还是被移动的元素,从而减少不必要的元素渲染
虚拟DOM和Diff算法的理解
参考:blog.csdn.net/m0_52409770… vue3js.cn/interview/R…
其中有一点要特别提醒的
1. 数据更改 -> 虚拟DOM -> 更新界面
2. 数据更改 -> 更新界面
/*
为什么多一步转化为 虚拟DOM 这个操作,看起来多了一步的消耗。
其实在转化为 虚拟DOM 时用了 Diff算法 【虚拟DOM算法 = 虚拟DOM + Diff算法】
*/
使用虚拟DOM算法的耗损计算:总耗损 = 虚拟DOM增删改 + 真实DOM差异增删改(与Diff算法效率有关) + 回流与重绘(较少节点)
直接操作真实DOM的耗损计算:总耗损 = 真实DOM完全增删改 + 回流与重绘(可能较多节点)
Diff三个层级的比较:
- Tree层级
- Component层级
- Element层级
- Tree diff
1. 进行树结构的层级比较,对同一个父节点下的所有子节点进行比较
2. 看节点是什么类型
2.1. 组件类型 -> 进行Component diff
2.2. 元素类型 -> 进行Element diff
- Component diff
1. 若组件类型相同,则继续进行 Tree diff
2. 若组件类型不同,则替换整个组件的内容
- Element diff
1. 节点是原生标签,则看标签名名称是都相同
1.1. 相同,递归进行 Tree diff
1.2. 不同,进行替换