2022-02-14
React
0
请注意,本文编写于 1228 天前,最后修改于 517 天前,其中某些信息可能已经过时。

目录

React 开发者工具
React组件
函数式组件
效果
代码
类式组件
JavaScript类
效果
代码
简单组件与复杂组件的区别
组件实例的三大核心属性
1.state
效果
代码
state的简写方式
理解
2.props
批量传递props
对props进行限制
props的简写方式
构造器与props
函数式组件使用props
理解
3.refs
效果
字符串形式的ref - 不太推荐
回调函数形式的ref
createRef API
事件处理
收集表单数据
非受控组件
受控组件
高阶函数与函数柯里化
高阶函数
函数的柯里化
不用柯里化的写法
组件的生命周期
理解生命周期
旧版生命周期
流程图
setState()与forceUpdate()流程
父组件render流程
总结旧版生命周期的三个阶段
新版生命周期
流程图
getDerivedStateFromProps()
getSnapshotBeforeUpdate()
总结新版生命周期的三个阶段
重要的钩子
即将废弃的钩子
DOM的Diffing算法
验证Diffing算法
虚拟DOM中key的作用
用index作为key可能会引发的问题
开发中如何选择key

React 开发者工具

推荐使用Chrome或Edge浏览器,安装React Developer Tools(Facebook出品)。

image-20220213160959897

安装完成后,访问使用React编写的页面时,图标会高亮(开发环境为红色有debug标识,生产环境为蓝色),同时F12开发者工具中会多出ComponentsProfiler两个选项卡。

React组件

函数式组件

效果

image-20220213181322649

简单组件:无状态state

代码

jsx
// 1.创建函数式组件 function Demo() { // 创建组件,函数名首字母须大写 console.log(this) // 此处的this是undefined,因为babel编译后开启了严格模式 return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2> } // 2.渲染组件到页面 ReactDOM.render(<Demo/>, document.getElementById('test'))

执行ReactDOM.render(<Demo/>, document.getElementById('test'))后发生了什么?

  1. React解析组件标签,找到了Demo组件

  2. 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中

类式组件

JavaScript类

构造
javascript
// 创建一个Person类 class Person { // 构造器方法 constructor(name, age) { // 构造器中的this:类的实例对象 this.name = name this.age = age } // 一般方法 speak() { // speak()方法放在类的原型对象上,供实例使用 // 通过Person实例调用speak时,speak中的this就是Person实例 // 谁调用它,它指向谁 console.log(`我叫${this.name},我的年龄是${this.age}`) } } // 创建一个Person的实例对象 const p1 = new Person('tom', 20) const p2 = new Person('jerry', 22) console.log(p1) console.log(p2) p1.speak() p2.speak()

image-20220213192510026

继承
javascript
// 创建一个Student类,继承于Person类 class Student extends Person { // 无新增属性可不写构造器方法 constructor(name, age, grade) { super(name, age) this.grade = grade this.school = 'xxxx' } // 重写从父类继承的方法 speak() { console.log(`我叫${this.name},我的年龄是${this.age},我今年读${this.grade}`) } study() { // study()方法放在类的原型对象上,供实例使用 // 通过Student实例调用study时,study中的this就是Student实例 console.log('我每天努力学习') } } // 创建一个Student的实例对象 const s1 = new Student('小王', 15) const s2 = new Student('小张', 16, '高一') console.log(s1) console.log(s2) s2.speak()

image-20220213192709261

  1. 类中的构造器不是必须写的,要对实例进行一些初始化的操作(如添加指定属性)时才写;
  2. 如果A类继承了B类,且A类中写了构造器,那么A类构造器中的super是必须要调用的;
  3. 类中所定义的方法,都是放在了类的原型对象上,供实例去使用

类中可以直接写赋值语句。

类中this的指向
js
class Person { constructor(name, age) { this.name = name this.age = age } speak() { console.log(this) } } const p1 = new Person('tom', 18) p1.speak() const x = p1.speak x()

image-20220218142424764

效果

屏幕截图 2022-02-13 194815

复杂组件:有状态state

代码

jsx
// 1.创建类式组件 class Demo extends React.Component { render() { // render方法放在Demo的原型对象上,供实例使用 // render中的this:Demo的实例对象 console.log('render中的this: ', this) return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2> } } // 2.渲染组件到页面 ReactDOM.render(<Demo/>, document.getElementById('test'))

执行ReactDOM.render(<Demo/>, document.getElementById('test'))后发生了什么?

  1. React解析组件标签,找到了Demo组件
  2. 发现组件是使用类定义的,随后new出来该类的实例,并通过该实例调用到原型上的render方法
  3. 将render返回的虚拟DOM转为真实DOM,随后呈现在页面中

简单组件与复杂组件的区别

简单组件:无状态 state

复杂组件:有状态 state

组件实例的三大核心属性

1.state

原生js中的事件绑定

html
<body> <button id="btn1">按钮1</button> <button id="btn2">按钮2</button> <button onclick="demo()">按钮3</button> <script> const btn1 = document.getElementById('btn1') btn1.addEventListener('click', ()=>{ alert('点击按钮1') }) // 兼容性最好 const btn2 = document.getElementById('btn2') btn2.onclick = ()=>{ alert('点击按钮2') } function demo() { alert('点击按钮3') } </script> </body>

React中的事件绑定

jsx
class Weather extends React.Component { constructor(props) { super(props) // 初始化状态 this.state = {isHot: true} } render() { console.log(this) // 读取状态 const {isHot} = this.state return <h1 onClick={demo}>今天天气很{isHot ? '炎热' : '凉爽'}</h1> } } function demo() { console.log('标题被点击') }

注意return <h1 onClick={demo}>今天天气很{isHot ? '炎热' : '凉爽'}</h1>中html用法要改为驼峰,demo函数不需要加括号

类内的方法作为回调时,不通过实例调用

jsx
class Weather extends React.Component { constructor(props) { super(props) // 初始化状态 this.state = {isHot: true} } render() { // console.log(this) // 读取状态 const {isHot} = this.state return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}</h1> } changeWeather() { // changeWeather放在Weather的原型对象上,供实例使用 // 由于changeWeather是作为onClick的回调,所以不是通过实例调用的,是直接调用 // 类中的方法默认开启了局部的严格模式,所以changeWeather中的this是undefined console.log(this) } }

image-20220218143656468

解决方法:在构造器中绑定

jsx
this.changeWeather = this.changeWeather.bind(this)

image-20220218143845258

image-20220218144055873

JavaScript中的bind:

js
function demo() { console.log(this) } demo() const x = demo.bind({a:1, b:'b'}) x()

image-20220218144700014

效果

GIF 2022-2-19 12-55-13

代码

jsx
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>三大属性 - state</title> </head> <body> <div id="test"></div> <!-- 引入 React 核心库 --> <script type="text/javascript" src="../../js/react.development.js"></script> <!-- 引入 react-dom --> <script type="text/javascript" src="../../js/react-dom.development.js"></script> <!-- 引入babel 将jsx转为js --> <script type="text/javascript" src="../../js/babel.min.js"></script> <script type="text/babel"> /* 此处必须为babel */ // 1.创建组件 class Weather extends React.Component { // 构造器调用——1次 constructor(props) { super(props) // 初始化状态 this.state = {isHot: true} // 绑定changeWeather中的this指向 this.changeWeather = this.changeWeather.bind(this) } // render调用——1+n次(初始化+状态更新次数) render() { // console.log(this) // 读取状态 const {isHot} = this.state return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}</h1> } // changeWeather调用次数与点击次数相同 changeWeather() { // changeWeather放在Weather的原型对象上,供实例使用 // 由于changeWeather是作为onClick的回调,所以不是通过实例调用的,是直接调用 // 类中的方法默认开启了局部的严格模式,所以changeWeather中的this是undefined // 获取原来的isHot值 const isHot = this.state.isHot // !!! 状态(state)不可直接更改,要借助一个内置的API去更改 // 状态必须通过setState进行更新,且更新是一种合并,不是替换,即:未涉及更新的参数不会丢失 this.setState({isHot: !isHot}) // 不可取: this.state.isHot = !isHot } } // 2.渲染组件到页面 ReactDOM.render(<Weather/>, document.getElementById('test')) </script> </body> </html>

state的简写方式

jsx
class Weather extends React.Component { // 初始化状态 state = {isHot: true} render() { const {isHot} = this.state return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'}</h1> } // 自定义方法——赋值语句+箭头函数 changeWeather = ()=>{ const isHot = this.state.isHot this.setState({isHot: !isHot}) } }

理解

  1. state是组件对象最重要的属性,值是对象(可以包含多个key-value的组合)
  2. 组件被称为“状态机”,通过更新组件的state来更新对应的页面显示(重新渲染组件)

2.props

props是只读的

jsx
class Test extends React.Component { render() { console.log(this) return <div></div> } } ReactDOM.render(<Test key1="value1" key2="value2" />, document.getElementById('test'))

image-20220301203341626

批量传递props

jsx
class Person extends React.Component { render() { console.log(this) const {name, age, sex} = this.props return ( <ul> <li>姓名:{name}</li> <li>年龄:{age}</li> <li>性别:{sex}</li> </ul> ) } } const p = {name: 'Tom', age: 18, sex: '男'} ReactDOM.render(<Person {...p} />, document.getElementById('test1')) ReactDOM.render(<Person name='Amy' age={} sex='女' />, document.getElementById('test2'))

image-20220301204242744

展开运算符

javascript
let arr1 = [1, 3, 5, 7, 9] let arr2 = [2, 4, 6, 8, 10] console.log(arr1) console.log(...arr1) // 展开一个数组 let arr3 = [...arr1, ...arr2] // 连接两个数组 console.log(arr3) function sum(...numbers) { // 数组传参 return numbers.reduce((preValue, currentValue) => { return preValue + currentValue }) } console.log(sum(1, 2, 3, 4, 5)) let person1 = { name: 'tom', age: 18 } let person2 = { ...person1 } // 构造字面量对象 person1.name = 'jerry' console.log('person2 :>> ', person2); let person3 = { ...person1, name: 'jack', address: 'where' } // 复制对象并修改 console.log('person3 :>> ', person3);

image-20220301212807638

对props进行限制

原因:如果不对props加以限制,其他输入数据的人无法得知props需要的数据类型,例如ReactDOM.render(<Person name='Amy' age='19' sex='女' />, document.getElementById('test2'))ReactDOM.render(<Person name='Amy' age={19} sex='女' />, document.getElementById('test2'))

预期效果:

  • 姓名必须指定,且为字符串类型;
  • 性别为字符串类型,如果性别没有指定,默认为男
  • 年龄为字符串类型,且为数字类型,默认值为18

限制方法:

  1. 引入依赖包

    jsx
    <script type="text/javascript" src="../../js/prop-types.js"></script>
  2. 限制标签属性:

    jsx
    Person.propTypes = { name: PropTypes.string.isRequired, // 限制name必传,为字符串 sex: PropTypes.string, // 限制sex为字符串 age: PropTypes.number, // 限制age为数值 speak: PropTypes.func // 限制speak为函数 } Person.defaultProps = { sex: '男', age: 18 }
  3. 效果:

    jsx
    ReactDOM.render(<Person name={120} />, document.getElementById('test2'))

    image-20220312104011074

props的简写方式

jsx
class Person extends React.Component { // 进行限制 static propTypes = { name: PropTypes.string.isRequired, // 限制name必传,为字符串 sex: PropTypes.string, // 限制sex为字符串 age: PropTypes.number, // 限制age为数值 speak: PropTypes.func // 限制speak为函数 } static defaultProps = { sex: '男', age: 18 } render() { /* console.log(this) */ const {name, age, sex} = this.props return ( <ul> <li>姓名:{name}</li> <li>年龄:{age+1}</li> <li>性别:{sex}</li> </ul> ) } }

构造器与props

jsx
constructor(props) { super(props) console.log('constructor',this.props); }

构造器是否接收props,是否传递给super,取决于是否希望在构造器中通过this访问props,极其罕见

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。

通常,在 React 中,构造函数仅用于以下两种情况:

函数式组件使用props

jsx
function Person(props) { console.log(props); const {name, age, sex} = props return ( <ul> <li>姓名:{name}</li> <li>年龄:{age}</li> <li>性别:{sex}</li> </ul> ) } ReactDOM.render(<Person name='Jack' age={20} sex='男' />, document.getElementById('test3'))

image-20220312135358831

添加限制:

jsx
Person.propTypes = { name: PropTypes.string.isRequired, // 限制name必传,为字符串 sex: PropTypes.string, // 限制sex为字符串 age: PropTypes.number, // 限制age为数值 } Person.defaultProps = { sex: '男', age: 18 } ReactDOM.render(<Person name={100}/>, document.getElementById('test3'))

image-20220312135740515

理解

  • 每个组件对象都会有props(properties的简写)属性
  • 组件标签的所有属性都保存在props中

作用:

  1. 通过标签属性从组件外向组件内传递变化的数据
  2. 注意: 组件内部不要修改props数据

3.refs

refs收集多组ref

jsx
class Demo extends React.Component { showThis = () => { console.log(this) } render() { return ( <div> <input ref="input1" type="text" placeholder="点击按钮提示数据"/>&nbsp; <button ref="button" onClick={this.showThis}>点击提示左侧输入框数据</button>&nbsp; <input ref="input2" type="text" placeholder="失去焦点提示数据" /> </div> ) } }

image-20220312154333736

效果

GIF 2022-3-12 15-53-43

字符串形式的ref - 不太推荐

jsx
// 创建组件 class Demo extends React.Component { // 展示左侧输入框数据 showLeft = () => { const {input1} = this.refs alert(input1.value) } // 展示右侧输入框数据 showRight = () => { const {input2} = this.refs alert(input2.value) } render() { return ( <div> <input ref="input1" type="text" placeholder="点击按钮提示数据"/>&nbsp; <button onClick={this.showLeft}>点击提示左侧输入框数据</button>&nbsp; <input ref="input2" onBlur={this.showRight} type="text" placeholder="失去焦点提示数据" /> </div> ) } } // 渲染组件到页面 ReactDOM.render(<Demo />, document.getElementById('test'))

过时 API:String 类型的 Refs

如果你之前使用过 React,你可能了解过之前的 API 中的 string 类型的 ref 属性,例如 "textInput"。你可以通过 this.refs.textInput 来访问 DOM 节点。我们不建议使用它,因为 string 类型的 refs 存在 一些问题。它已过时并可能会在未来的版本被移除。

回调函数形式的ref

jsx
class Demo extends React.Component { // 展示左侧输入框数据 showLeft = () => { const {input1} = this alert(input1.value) } // 展示右侧输入框数据 showRight = () => { const {input2} = this alert(input2.value) } render() { return ( <div> <input ref={(c) => {this.input1 = c}} type="text" placeholder="点击按钮提示数据"/>&nbsp; <button onClick={this.showLeft}>点击提示左侧输入框数据</button>&nbsp; <input ref={c => this.input2 = c} onBlur={this.showRight} type="text" placeholder="失去焦点提示数据" /> </div> ) } }

回调ref中的调用次数:

  • 首次渲染:调用1次
  • 页面更新:调用2次

关于回调 refs 的说明

如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。

jsx
class Demo extends React.Component { state = {isHot: true} showInfo = () => { const {input1} = this alert(input1.value) } changeWeather = () => { const {isHot} = this.state this.setState({isHot: !isHot}) } render() { const {isHot} = this.state return ( <div> <h2>今天天气很{isHot ? '炎热' : '凉爽'}</h2> <input ref={(c) => {this.input1 = c; console.log('@', c)}} type="text" placeholder="点击按钮提示数据"/>&nbsp; <button onClick={this.showInfo}>点击提示左侧输入框数据</button>&nbsp; <button onClick={this.changeWeather}>点击修改天气</button> </div> ) } }

GIF 2022-3-12 16-42-54

避免多次回调:将 ref 的回调函数定义成 class 的绑定函数

jsx
saveInput = (c) => { this.input1 = c; console.log('@', c) } render() { const {isHot} = this.state return ( <div> <h2>今天天气很{isHot ? '炎热' : '凉爽'}</h2> <input ref={this.saveInput} type="text" placeholder="点击按钮提示数据"/>&nbsp; <button onClick={this.showInfo}>点击提示左侧输入框数据</button>&nbsp; <button onClick={this.changeWeather}>点击修改天气</button> </div> ) }

createRef API

官方最推荐的创建ref的形式

jsx
class Demo extends React.Component { /* React.createRef调用后可以返回一个容器,该容器可以存储被ref标识的节点,该容器是“专人专用”的 */ myRefLeft = React.createRef() myRefRight = React.createRef() // 展示左侧输入框数据 showLeft = () => { alert(this.myRefLeft.current.value) } // 展示右侧输入框数据 showRight = () => { alert(this.myRefRight.current.value) } render() { return ( <div> <input ref={this.myRefLeft} type="text" placeholder="点击按钮提示数据"/>&nbsp; <button onClick={this.showLeft}>点击提示左侧输入框数据</button>&nbsp; <input ref={this.myRefRight} onBlur={this.showRight} type="text" placeholder="失去焦点提示数据" /> </div> ) } }

事件处理

  1. 通过onXxx属性指定事件处理函数(注意大小写)

    (1)React使用的是自定义(合成)事件, 而不是使用的原生DOM事件——为了更好的兼容性

    (2)React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)——高效

  2. 通过event.target得到发生事件的DOM元素对象——不要过度使用ref

收集表单数据

效果:

GIF 2022-3-12 20-15-45

非受控组件

页面内输入类DOM(input、checkbox、radio等)都是现用现取

jsx
class Login extends React.Component { handleSubmit = (event) => { event.preventDefault() // 阻止默认事件——组织表单提交 const {username, password} = this alert(`你输入的用户名是${username.value},密码是${password.value}`) } render() { return ( <form onSubmit={this.handleSubmit}> 用户名:<input ref={c => this.username = c} type="text" name="username" /><br/> 密码:<input ref={c => this.password = c} type="password" name="password" /><br/> <button>登录</button> </form> ) } }

受控组件

随着输入直接维护到状态中,需要时直接从状态中取得(类比:vue的双向数据绑定)

jsx
class Login extends React.Component { // 初始化状态 state = {username: '', password: ''} // 保存用户名和密码到状态中 saveUsername = (event) => { this.setState({username:event.target.value}) } savePassword = (event) => { this.setState({password:event.target.value}) } // 表单提交的回调 handleSubmit = (event) => { event.preventDefault() // 阻止默认事件——组织表单提交 const {username, password} = this.state alert(`你输入的用户名是${username},密码是${password}`) } render() { return ( <form onSubmit={this.handleSubmit}> 用户名:<input onChange={this.saveUsername} type="text" name="username" /><br/> 密码:<input onChange={this.savePassword} type="password" name="password" /><br/> <button>登录</button> </form> ) } }

GIF 2022-3-12 20-57-33

高阶函数与函数柯里化

jsx
// 保存表单数据到状态中 saveFormData = (dataType) => { return ((event) => { this.setState({[dataType]:event.target.value}) }) } render() { return ( <form onSubmit={this.handleSubmit}> 用户名:<input onChange={this.saveFormData('username')} type="text" name="username" /><br/> 密码:<input onChange={this.saveFormData('password')} type="password" name="password" /><br/> <button>登录</button> </form> ) }

js方括号

javascript
let a = 'name' let obj = {} let obj1 = {} obj[a] = 'tom' obj1.a = 'tommy' console.log(obj) console.log(obj1)

image-20220315171516405

高阶函数

如果一个函数符合下面两个规范中的任何一个,那该函数就是高阶函数

  1. A函数接收的参数是一个函数
  2. A函数调用的返回值是一个函数

常见的高阶函数:PromisesetTimeoutarr.map()

函数的柯里化

通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式

jsx
/*function sum(a, b, c) { return a+b+c } result = sum(1, 2, 3)*/ function sum(a) { return (b) => { return (c) => { return a+b+c } } } result = sum(1)(2)(3) console.log(result);

不用柯里化的写法

jsx
// 不用柯里化 saveFormDataN = (dataType, event) => { this.setState({[dataType]:event.target.value}) } render() { return ( <form onSubmit={this.handleSubmit}> 用户名:<input onChange={(event)=>{this.saveFormDataN('username', event)}} type="text" name="username" /><br/> 密码:<input onChange={(event)=>{this.saveFormDataN('password', event)}} type="password" name="password" /><br/> <button>登录</button> </form> ) }

组件的生命周期

理解生命周期

生命周期回调函数 <=> 生命周期钩子函数 <=> 生命周期函数 <=> 生命周期钩子

jsx
// 创建组件 // 生命周期回调函数 <=> 生命周期钩子函数 <=> 生命周期函数 <=> 生命周期钩子 class Demo extends React.Component { state = { opacity: 1 } death = () => { // 卸载组件 ReactDOM.unmountComponentAtNode(document.getElementById('test')) } // 组件挂载完毕调用 componentDidMount() { this.timer = setInterval(() => { // 获取原状态 let { opacity } = this.state // -0.1 opacity -= 0.1 if (opacity <= 0) opacity = 1 // 设置新状态 this.setState({ opacity }) }, 200) } // 组件将要卸载调用 componentWillUnmount() { clearInterval(this.timer) } // 初始化渲染、状态更新调用 render() { console.log('render'); return ( <div> <h2 style={{ opacity: this.state.opacity }}>React学不会了可咋办捏</h2> <button onClick={this.death}>不活了</button> </div> ) } } // 渲染组件到页面 ReactDOM.render(<Demo />, document.getElementById('test'))
  1. 组件从创建到死亡它会经历一些特定的阶段。
  2. React组件中包含一系列钩子函数(生命周期回调函数), 会在特定的时刻调用。
  3. 我们在定义组件时,会在特定的生命周期回调函数中,做特定的工作。

旧版生命周期

流程图

react生命周期(旧)

setState()与forceUpdate()流程

jsx
// 创建组件 class Count extends React.Component { // 构造器 constructor(props) { console.log('Count - constructor') super(props) // 初始化状态 this.state = {count: 0} } // +1按钮的回调 add = () => { // 获取原状态 const {count} = this.state // 更新状态 this.setState({count: count+1}) } // 卸载组件按钮的回调 death = () => { ReactDOM.unmountComponentAtNode(document.getElementById('test')) } // 强制更新按钮的回调 force = () => { this.forceUpdate() } // 组件将要挂载的钩子 componentWillMount() { console.log('Count - componentWillMount'); } // 组件挂载完毕的钩子 componentDidMount() { console.log('Count - componentDidMount'); } // 组件将要卸载的钩子 componentWillUnmount() { console.log('Count - componentWillUnmount'); } // 控制组件更新的“阀门” shouldComponentUpdate() { console.log('Count - shouldComponentUpdate'); return true // 重写shouldComponentUpdate()方法,必须有返回值 } // 组件将要更新的钩子 componentWillUpdate() { console.log('Count - componentWillUpdate'); } // 组件更新完毕的钩子 componentDidUpdate() { console.log('Count - componentDidUpdate'); } render() { console.log('Count - render'); const {count} = this.state return ( <div> <h2>当前计数为{count}</h2> <button onClick={this.add}>点我+1</button> <button onClick={this.death}>卸载组件</button> <button onClick={this.force}>不更改状态强制更新组件</button> </div> ) } } // 渲染组件到页面 ReactDOM.render(<Count />, document.getElementById('test'))

setState

forceUpdate

父组件render流程

jsx
// 创建组件 class A extends React.Component { state = {carName: '奔驰'} change = () => { this.setState({carName: '特斯拉'}) } render() { return ( <div> <h2>我是A组件</h2> <button onClick={this.change}>换车</button> <B carName={this.state.carName}/> </div> ) } } class B extends React.Component { // 组件将要接收新的props的钩子 componentWillReceiveProps(props) { // 坑:第一次调用的不算 console.log('B - componentWillReceiveProps', props); } // 控制组件更新的“阀门” shouldComponentUpdate() { console.log('B - shouldComponentUpdate'); return true // 重写shouldComponentUpdate()方法,必须有返回值 } // 组件将要更新的钩子 componentWillUpdate() { console.log('B - componentWillUpdate'); } // 组件更新完毕的钩子 componentDidUpdate() { console.log('B - componentDidUpdate'); } render() { console.log('B - render'); return( <div> <h2>我是B组件,我的车是{this.props.carName}</h2> </div> ) } } // 渲染组件到页面 ReactDOM.render(<A />, document.getElementById('test'))

父组件render

总结旧版生命周期的三个阶段

1.初始化阶段: 由ReactDOM.render()触发---初次渲染

  • constructor()
  • componentWillMount()
  • render()
  • componentDidMount() ====> 常用 一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息

2.更新阶段: 由组件内部this.setState()或父组件重新render()触发

  • shouldComponentUpdate()
  • componentWillUpdate()
  • render() ====> 必须
  • componentDidUpdate()

3.卸载组件: 由ReactDOM.unmountComponentAtNode()触发

  • componentWillUnmount() ====> 常用 一般在这个钩子中做收尾的事,例如:关闭定时器、取消订阅

新版生命周期

更新js文件,升级为17.0.1版本

html
<!-- 引入 React 核心库 --> <script type="text/javascript" src="../js/17.0.1/react.development.js"></script> <!-- 引入 react-dom --> <script type="text/javascript" src="../js/17.0.1/react-dom.development.js"></script> <!-- 引入babel 将jsx转为js --> <script type="text/javascript" src="../js/17.0.1/babel.min.js"></script>

流程图

react生命周期(新)

getDerivedStateFromProps()

getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。

此方法适用于罕见的用例,即 state 的值在任何时候都取决于 props。例如,实现 <Transition> 组件可能很方便,该组件会比较当前组件与下一组件,以决定针对哪些组件进行转场动画。

jsx
class Count extends React.Component { ... static getDerivedStateFromProps(props, state) { console.log('getDerivedStateFromProps', props, state); return props } ... } ReactDOM.render(<Count count={568}/>, document.getElementById('test'))

GIF 2022-3-19 18-47-30

getSnapshotBeforeUpdate()

getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()

此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。

应返回 snapshot 的值(或 null)。

jsx
class NewsList extends React.Component { state = {newsArr: []} componentDidMount() { setInterval(() => { const {newsArr} = this.state const news = '新闻' + (newsArr.length+1) this.setState({newsArr: [news, ...newsArr]}) }, 1000) } getSnapshotBeforeUpdate() { return this.refs.list.scrollHeight } componentDidUpdate(preProps, preState, height) { this.refs.list.scrollTop += this.refs.list.scrollHeight - height } render() { return( <div className="newsList" ref="list"> {this.state.newsArr.map((n, index) => { return <div key={index} className="news">{n}</div> })} </div> ) } } ReactDOM.render(<NewsList/>, document.getElementById('test'))

GIF 2022-3-20 20-36-43

总结新版生命周期的三个阶段

1.初始化阶段: 由ReactDOM.render()触发---初次渲染

  • constructor()
  • getDerivedStateFromProps
  • render()
  • componentDidMount() ====> 常用 一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息

2.更新阶段: 由组件内部this.setSate()或父组件重新render触发

  • getDerivedStateFromProps
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate
  • componentDidUpdate()

3.卸载组件: 由ReactDOM.unmountComponentAtNode()触发

  • componentWillUnmount() ====> 常用 一般在这个钩子中做收尾的事,例如:关闭定时器、取消订阅

重要的钩子

  • render:初始化渲染或更新渲染调用
  • componentDidMount:开启监听, 发送ajax请求
  • componentWillUnmount:做一些收尾工作, 如: 清理定时器

即将废弃的钩子

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

现在使用会出现警告,下一个大版本需要加上UNSAFE_前缀才能使用,以后可能会被彻底废弃,不建议使用。

DOM的Diffing算法

当对比两棵树时,React 首先比较两棵树的根节点。不同类型的根节点元素会有不同的形态。

Diffing最小的粒度是标签。

“粒度”表示的是精确程度问题。粗粒度角度描述一个系统,是关注系统中大的组件;细粒度角度描述一个系统是从组成大组件的小组件,或者更小组件的角度认识系统。

系统功能一般又分为多个模块,大的功能又会分为若干模块或者步骤,粒度一步一步细化,直到最终的某个用户操作(输入内容,下拉选择,上传文件,点击按钮等),具体的功能最终得到实现。这是一个粒度由粗到细的过程。反之就是由细到粗。 再从代码设计的角度来说。代码的结构是由类型之间的关联起来的系统。系统的整体结构(架构)就是系统最粗的粒度,代码也同系统功能一样,也是有模块划分(可能类似功能结构划分,也可能有所区别)。那么从复杂结构代码模块,到其细小的组成部分就是粒度由粗到细的过程。

验证Diffing算法

jsx
class Time extends React.Component { state = {date: new Date()} componentDidMount() { setInterval(() => { this.setState({date: new Date()}) }, 1000) } render() { return ( <div> <h1>Hello</h1> <input type="text"/> <span> 现在是:{this.state.date.toTimeString()} <input type="text"/> </span> </div> ) } } ReactDOM.render(<Time/>, document.getElementById('test'))

GIF 2022-3-20 21-00-40

两个输入框中的输入内容均没有丢失,证明Diffing算法是真实存在的

虚拟DOM中key的作用

简单地说:key是虚拟DOM对象的标识,在更新显示时key起着极其重要的作用

详细的说:当状态中的数据发生变化时,react会根据新数据生成新的虚拟DOM,随后react进行新虚拟DOM与旧虚拟DOM的diff比较,规则如下:

  1. 旧虚拟DOM中找到了与新虚拟DOM相同的key:
    • 若虚拟DOM中内容未变,直接使用之前的真实DOM
    • 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
  2. 旧虚拟DOM中未找到与新虚拟DOM相同的key:
    • 根据数据创建新的真实DOM,随后渲染到页面

用index作为key可能会引发的问题

  1. 若对数据进行:逆序添加、逆序删除等破坏顺序的操作:
    • 会产生没有必要的真实DOM更新 ===> 界面效果没问题,但效率低
  2. 如果结构中包含输入类的DOM:
    • 会产生错误DOM更新 ===> 界面有问题

注意:如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key没有问题

jsx
class Person extends React.Component { state = { persons:[ {id:1, name:'小张', age:18}, {id:2, name:'小王', age:20} ] } add = () => { const {persons} = this.state const p = {id:persons.length+1, name:'小李', age:35} this.setState({persons: [p, ...persons]}) } render() { return( <div> <h2>展示人员信息</h2> <h3>使用index(索引值)作为key</h3> <button onClick={this.add}>添加一个小李</button> <ul> { this.state.persons.map((personObj, index) => { return <li key={index}>{personObj.name}----{personObj.age}<input type="text"/></li> }) } </ul> <hr/> <h3>使用obj.id(数据的唯一标识)作为key</h3> <ul> { this.state.persons.map((personObj) => { return <li key={personObj.id}>{personObj.name}----{personObj.age}<input type="text"/></li> }) } </ul> </div> ) } } ReactDOM.render(<Person/>, document.getElementById('test'))

GIF 2022-3-20 22-40-32

开发中如何选择key

  1. 最好使用每条数据的唯一标识作为key,比如id、手机号、学号、身份证号等
  2. 如果确定只是简单的展示数据,使用index亦可

本文作者:Morales

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 License 许可协议。转载请注明出处!