生命周期
https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
组件的生命周期可分成三个状态:
- Mounting:已插入真实 DOM
- Updating:正在被重新渲染
- Unmounting:已移出真实 DOM
生命周期的方法有
- **componentWillMount **在渲染前调用,在客户端也在服务端。
- **componentDidMount **: 在第一次渲染后调用,只在客户端。之后组件已经生成了对应的DOM结构,可以通过this.getDOMNode()来进行访问。 如果你想和其他JavaScript框架一起使用,可以在这个方法中调用setTimeout, setInterval或者发送AJAX请求等操作(防止异步操作阻塞UI)。
- **componentWillReceiveProps **在组件接收到一个新的 prop (更新后)时被调用。这个方法在初始化render时不会被调用。
- **shouldComponentUpdate **返回一个布尔值。在组件接收到新的props或者state时被调用。在初始化时或者使用forceUpdate时不被调用。可以在你确认不需要更新组件时使用。
- componentWillUpdate在组件接收到新的props或者state但还没有render时被调用。在初始化时不会被调用。
- **componentDidUpdate **在组件完成更新后立即调用。在初始化时不会被调用。
- componentWillUnmount在组件从 DOM 中移除之前立刻被调用。
hooks
userStateuseEffectuseContext
useContext可以帮助我们跨越组件层级直接传递变量,实现共享。 需要注意的是useContext和redux的作用是不同的!!!
useContext:解决的是组件之间值传递的问题
redux:是应用中统一管理状态的问题
但通过和useReducer的配合使用,可以实现类似Redux的作用。
useReducer
useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。
useCallback
返回一个memoized回调函数,我的理解即返回一个函数的句柄,等同于函数的变量,因此你可以使用memoizedCallback()进行执行该函数或者传递给事件和子组件,这里可以推荐绝大多数事件或者子组件的方法使用useCallback,避免组件更新重复渲染。 因此useCallback中的doSomething并不会在定义时就执行,而是需要手动调用返回的memoizedCallback才是真的执行。简单理解为useCallback定义了一个函数,仅在deps发生变化时重新定义该函数,否则该函数的变量不会变化,事件和子组件内容也就不用重新绑定或者渲染。
useMemo
作为性能优化的手段(缓存数据) 原理与 class 组件的 PureComponent 优化原理相同,对 props 进行了浅比较
useRef
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。常见访问子组件
useImperativeMethodsuseMutationEffectuseLayoutEffect
其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
useEffect 和 useLayoutEffect 的区别?
useEffect 在渲染时是异步执行,并且要等到浏览器将所有变化渲染到屏幕后才会被执行。 useLayoutEffect 在渲染时是同步执行,其执行时机与 componentDidMount,componentDidUpdate 一致
对于 useEffect 和 useLayoutEffect 哪一个与 componentDidMount,componentDidUpdate 的是等价的?
useLayoutEffect,因为从源码中调用的位置来看,useLayoutEffect的 create 函数的调用位置、时机都和 componentDidMount,componentDidUpdate 一致,且都是被 React 同步调用,都会阻塞浏览器渲染。
useEffect 和 useLayoutEffect 哪一个与 componentWillUnmount 的是等价的?
同上,useLayoutEffect 的 detroy 函数的调用位置、时机与 componentWillUnmount 一致,且都是同步调用。useEffect 的 detroy 函数从调用时机上来看,更像是 componentDidUnmount (注意React 中并没有这个生命周期函数)。
为什么建议将修改 DOM 的操作里放到 useLayoutEffect 里,而不是 useEffect?
可以看到在流程9/10期间,DOM 已经被修改,但但浏览器渲染线程依旧处于被阻塞阶段,所以还没有发生回流、重绘过程。由于内存中的 DOM 已经被修改,通过 useLayoutEffect 可以拿到最新的 DOM 节点,并且在此时对 DOM 进行样式上的修改,假设修改了元素的 height,这些修改会在步骤 11 和 react 做出的更改一起被一次性渲染到屏幕上,依旧只有一次回流、重绘的代价。 如果放在 useEffect 里,useEffect 的函数会在组件渲染到屏幕之后执行,此时对 DOM 进行修改,会触发浏览器再次进行回流、重绘,增加了性能上的损耗。
函数式编程
编程范式 纯函数 不可变值
JSX 本质 和 vdom
JSX 即 createElement 函数 执行生成 vnode patch(elem, vnode) patch(vnode, newVnode)
合成事件
所有事件挂载到 document 上 event 不是原生的,是 SyntheticEvent 合成事件对象,模拟出来 DOM 事件所有能力 所有的事件,都被挂载到 document 上 和 Vue 事件不同,和 Dom 事件也不同
import React from 'react'
class EventDemo extends React.Component {
constructor(props) {
super(props)
this.state = {
name: 'zhangsan',
list: [
{
id: 'id-1',
title: '标题1',
},
{
id: 'id-2',
title: '标题2',
},
{
id: 'id-3',
title: '标题3',
},
],
}
// 修改方法的 this 指向
this.clickHandler1 = this.clickHandler1.bind(this)
}
render() {
// this - 使用 bind
// return <p onClick={this.clickHandler1}>{this.state.name}</p>;
// this - 使用静态方法
// return <p onClick={this.clickHandler2}>clickHandler2 {this.state.name}</p>;
// event
// return ( ,,,
// <a href="https://imooc.com/" onClick={this.clickHandler3}>
// click me
// </a>
// );
// 传递参数 - 用 bind(this, a, b)
return (
<ul>
{this.state.list.map((item, index) => {
return (
<li
key={item.id}
onClick={this.clickHandler4.bind(this, item.id, item.title)}
>
index {index}; title {item.title}
</li>
)
})}
</ul>
)
}
clickHandler1() {
// console.log('this....', this) // this 默认是 undefined
this.setState({
name: 'lisi',
})
}
// 静态方法,this 指向当前实例
clickHandler2 = () => {
this.setState({
name: 'lisi',
})
}
// 获取 event
clickHandler3 = (event) => {
event.preventDefault() // 阻止默认行为
event.stopPropagation() // 阻止冒泡
console.log('target', event.target) // 指向当前元素,即当前元素触发
console.log('current target', event.currentTarget) // 指向当前元素,假象!!!
// 注意,event 其实是 React 封装的。可以看 __proto__.constructor 是 SyntheticEvent 组合事件
console.log('event', event) // 不是原生的 Event ,原生的 MouseEvent
console.log('event.__proto__.constructor', event.__proto__.constructor)
// 原生 event 如下。其 __proto__.constructor 是 MouseEvent
console.log('nativeEvent', event.nativeEvent)
console.log('nativeEvent target', event.nativeEvent.target) // 指向当前元素,即当前元素触发
console.log('nativeEvent current target', event.nativeEvent.currentTarget) // 指向 document !!!
// 1. event 是 SyntheticEvent ,模拟出来 DOM 事件所有能力
// 2. event.nativeEvent 是原生事件对象
// 3. 所有的事件,都被挂载到 document 上
// 4. 和 DOM 事件不一样,和 Vue 事件也不一样
}
// 传递参数
clickHandler4(id, title, event) {
console.log(id, title)
console.log('event', event) // 最后追加一个参数,即可接收 event
}
}
export default EventDemo
为何要合成事件机制
更好的兼容性和跨平台 载到 document ,减少内存消化,避免频繁解绑 方便事件的统一管理(如事务机制)
setState 和 batchUpdate
有时异步(普通使用),有时同步(setTimeout、DOM事件) 有时合并(对象形式),有时不合并(函数形式) 后者比较好理解(像Object.assign),主要讲解前者
import React from 'react'
// 函数组件(后面会讲),默认没有 state
class StateDemo extends React.Component {
constructor(props) {
super(props)
// 第一,state 要在构造函数中定义
this.state = {
count: 0,
}
}
render() {
return (
<div>
<p>{this.state.count}</p>
<button onClick={this.increase}>累加</button>
</div>
)
}
increase = () => {
// 第二,不要直接修改 state ,使用不可变值 ----------------------------
// this.state.count++ // 错误
// this.setState({
// count: this.state.count + 1, // SCU
// });
// 操作数组、对象的的常用形式
// 第三,setState 可能是异步更新(有可能是同步更新) ----------------------------
// this.setState(
// {
// count: this.state.count + 1,
// },
// () => {
// // 联想 Vue $nextTick - DOM
// console.log("count by callback", this.state.count); // 回调函数中可以拿到最新的 state
// }
// );
// console.log("count", this.state.count); // 异步的,拿不到最新值
// // setTimeout 中 setState 是同步的
// setTimeout(() => {
// this.setState({
// count: this.state.count + 1,
// });
// console.log("count in setTimeout", this.state.count);
// }, 0);
// 自己定义的 DOM 事件,setState 是同步的。再 componentDidMount 中
// 第四,state 异步更新的话,更新前会被合并 ----------------------------
// 传入对象,会被合并(类似 Object.assign )。执行结果只一次 +1
// this.setState({
// count: this.state.count + 1,
// });
// this.setState({
// count: this.state.count + 1,
// });
// this.setState({
// count: this.state.count + 1,
// });
// 传入函数,不会被合并。执行结果是 +3
this.setState((prevState, props) => {
return {
count: prevState.count + 1,
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1,
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1,
}
})
}
// bodyClickHandler = () => {
// this.setState({
// count: this.state.count + 1,
// });
// console.log("count in body event", this.state.count);
// };
// componentDidMount() {
// // 自己定义的 DOM 事件,setState 是同步的
// document.body.addEventListener("click", this.bodyClickHandler);
// }
// componentWillUnmount() {
// // 及时销毁自定义 DOM 事件
// document.body.removeEventListener("click", this.bodyClickHandler);
// // clearTimeout
// }
}
export default StateDemo
// -------------------------- 我是分割线 -----------------------------
// // 不可变值(函数式编程,纯函数) - 数组
// const list5Copy = this.state.list5.slice();
// list5Copy.splice(2, 0, "a"); // 中间插入/删除
// this.setState({
// list1: this.state.list1.concat(100), // 追加
// list2: [...this.state.list2, 100], // 追加
// list3: this.state.list3.slice(0, 3), // 截取
// list4: this.state.list4.filter((item) => item > 100), // 筛选
// list5: list5Copy, // 其他操作
// });
// // 注意,不能直接对 this.state.list 进行 push pop splice 等,这样违反不可变值
// // 不可变值 - 对象
// this.setState({
// obj1: Object.assign({}, this.state.obj1, { a: 100 }),
// obj2: { ...this.state.obj2, a: 100 },
// });
// // 注意,不能直接对 this.state.obj 进行属性设置,这样违反不可变值
this.setState异步还是同步?
setState 无所谓异步还是同步 看是否能命中 batchedUpdates 机制, Legacy模式命中异步,反之同步;Concurrent模式统一异步 判断 isBatchingUpdates
例:
- 合成事件中是异步
- 钩子函数中的是异步
- 原生事件中是同步
- setTimeout中是同步
哪些能命中 batchUpdate 机制
生命周期(和它调用的函数) React 中注册的事件(和它调用的函数) React 可以 “管理”的入口
哪些不能命中 batchUpdate 机制
setTimeout setInterval等(和它调用的函数) 自定义的 DOM 事件(和它调用的函数) React “管不到”的入口
transaction 事务机制
组件渲染和更新过程
JSX 如何渲染成页面 setState 之后如何更新页面 全流程 更新的两个阶段:reconciliation commit React fiber
表单校验
受控组件(非受控组件) input textarea select 用 value checkbox radio 用 checked
import React, { memo } from 'react'
import { Form, Input } from 'antd'
const Test = memo((props) => {
const [form] = Form.useForm()
const passwordValidator = (rule, value, callback) => {
const { getFieldValue } = form
if (value && value !== getFieldValue('password')) {
callback('两次输入不一致!')
}
callback()
}
return (
<Form from={form}>
<Form.Item label='非空限制'>
{getFieldDecorator('name', {
rules: [
{
required: true,
message: '不能为空',
},
],
})(<Input placeholder='请输入名称' />)}
</Form.Item>
<Form.Item label='字符串限制-范围限制'>
{getFieldDecorator('password', {
rules: [
{
required: true,
message: '密码不能为空',
},
{
min: 4,
message: '密码不能少于4个字符',
},
{
max: 6,
message: '密码不能大于6个字符',
},
],
})(<Input placeholder='请输入密码' type='password' />)}
</Form.Item>
<Form.Item label='字符串限制-长度限制'>
{getFieldDecorator('nickname', {
rules: [
{
required: true,
message: '昵称不能为空',
},
{
len: 4,
message: '长度需4个字符',
},
],
})(<Input placeholder='请输入昵称' />)}
</Form.Item>
<Form.Item label='自定义校验'>
{getFieldDecorator('passwordcomfire', {
rules: [
{
required: true,
message: '请再次输入密码',
},
{
validator: passwordValidator,
},
],
})(<Input placeholder='请输入密码' type='password' />)}
</Form.Item>
<Form.Item label='空格校验'>
{getFieldDecorator('hobody', {
rules: [
{
whitespace: true,
message: '不能输入空格',
},
],
})(<Input placeholder='请输入昵称' />)}
</Form.Item>
<Form.Item label='正则校验'>
{getFieldDecorator('qbc', {
rules: [
{
message: '只能输入数字',
pattern: /^[0-9]+$/,
},
],
})(<Input placeholder='请输入ABC' />)}
</Form.Item>
</Form>
)
})
export default Test
组件使用
props 传递数据 props 传递函数 props 类型检查
异步加载
import ReactDOM from 'react-dom'
import React, { Component, lazy, Suspense } from 'react'
const Sub = lazy(() => import('./sub'))
class App extends Component {
render() {
return (
<div>
<Suspense fallback={<div>loading</div>}>
<Sub />
</Suspense>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'))
组件优化-经典面试题
import React, { Component, PureComponent, memo } from 'react'
import ReactDOM from 'react-dom'
// 组件的优化
class Sub extends PureComponent {
// shouldComponentUpdate(nextProps, nextState) {
// if (nextProps.name === this.props.name) {
// return false;
// }
// return true;
// }
render() {
console.log('sub render')
return <div>sub</div>
}
}
const SubFun = memo((props) => {
console.log('SubFun render')
return <div>SubFun</div>
})
class App extends Component {
state = {
count: 0,
}
handleClick = () => {
this.setState({
count: this.state.count + 1,
})
}
callback = () => {}
render() {
console.log('render')
const { count } = this.state
return (
<div>
{/* <Sub name="wcd" /> */}
{/* <Sub cb={() => this.callback()} /> */}
<Sub cb={this.callback} />
<SubFun cb={this.callback} />
<p>{count}</p>
<button onClick={this.handleClick}>button</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'))
React.FC
经常出现React.Fc这个函数, 比如我不使用React.Fc来处理组件的函数, 则在组件里面使用props.children会报错, 我们一起进入源码分析一下。
type FC<P = {}> = FunctionComponent<P>
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null
propTypes?: WeakValidationMap<P> | undefined
contextTypes?: ValidationMap<any> | undefined
defaultProps?: Partial<P> | undefined
displayName?: string | undefined
}
FC
这个type
接收一个参数P
, 默认值为空对象, 而这个P
。FunctionComponent
就是个过度的名称而已, 可以认为FC
就是FunctionComponent
。- 第一句意义为第一个参数为
PropsWithChildren<P>
类型, 第二个参数可有可无, 有则为任意类型, 返回React
的dom
或者返回null。 - 后面四个参数不是必填, 我们主要研究第一句。
我们追查一下 PropsWithChildren
type PropsWithChildren<P> = P & { children?: ReactNode | undefined }
只是将传入的P的类型与{ children?: ReactNode | undefined }
合并而已, 看到这里我们就明白了, 其实用 React.FC
包裹一下是可以帮助 ts
推导出 props
身上可能有 children
属性。
HOC
高阶组件就是一个函数,接受一个组件作为参数,返回一个新的组件