React 面向组件编程
尚硅谷 2021 版 React 技术全家桶全套完整版(零基础入门到精通/男神天禹老师亲授)
React Developer Tools
开发工具
函数式组件
执行了 ReactDOM.render()
之后发生了什么?
- React 解析组件标签,找到了 MyCompoent 组件
- 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟 DOM 转换为真实 DOM,呈现到页面上
function MyComponent() {
console.log(this) // 此处this是undefined,因为babel编译后开启了严格模式
return <h2>函数式组件</h2>
}
ReactDOM.render(<MyComponent />, document.getElementById('test'))
类组件
- 类中的构造器不是必须写的,要对实例进行一些初始化的操作,如添加指定属性时才写
- 如果 A 类继承了 B 类,且 A 类中写了构造器,那么 A 类构造器中的 super 是必须要调用的
- 类中所定义的方法,都是放在类的原型对象上,供实例去使用
执行了 ReactDOM.render()
之后发生了什么?
- React 解析组件标签,找到了 MyCompoent 组件
- 发现组件是使用类定义的,随后 new 出来该类的实例,并通过该实例调用原型上的 render 方法
- 将 render 返回的虚拟 DOM 转为真实 DOM,随后呈现在页面中
class MyComponent extends React.Component {
render() {
return <h2>类组件</h2>
}
}
ReactDOM.render(<MyComponent />, document.getElementById('test'))
原生 JS 铺垫
- 原生 JS 事件绑定
<button id="btn1">按钮1</button>
<button id="btn2">按钮2</button>
<button onclick="demo()">按钮3</button>
<script type="text/javascript">
const btn1 = document.getElementById('btn1')
btn1.addEventListener('click', () => {
alert('按钮1被点击了')
})
const btn2 = document.getElementById('btn2')
btn2.onclick = () => {
alert('按钮2被点击了')
}
function demo() {
alert('按钮3被点击了')
}
</script>
- 原生 JS 中的 this 指向
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
study() {
console.log(this)
}
}
const p1 = new Person('tom', 18)
p1.study() // Person {name: "tom", age: 18}
const x = p1.study
x() // undefined
- 原生 JS 中的扩展运算符
// 构造字面量对象时使用展开语法
let p = {
name: 'tom', age: 18 }
let p2 = {
...p }
// console.log(...p) // Found non-callable @@iterator
let p3 = {
...p, name: 'jack' } // 合并
- 对象相关的知识
let a = 'name'
let obj = {
name: 'tom' }
console.log(obj[a]) // 'tom'
高阶函数:如果一个函数符合下面 2 个规范中的任何一个,那该函数就是高阶函数
- 若 A 函数接收的参数是一个函数,那么 A 就可以称之为高阶函数
- 若 A 函数调用的返回值依然是一个函数,那么 A 就可以称之为高阶函数
- 常见的高阶函数:Promise、setTimeout、arr.map() 等等
函数柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码方式
function sum(a) {
return b => {
return c => {
return a + b + c
}
}
}
组件实例三大属性 state
- state 值是对象(可包含多个 key-value 组合)
- 通过更新组件的 state 来更新对应的页面显示(重新渲染组件)
注意 this 问题:
- 由于
changeWeather
是作为 onClick 的回调,所以不是通过实例调用的,是直接调用,所以 this 应该为window
(非严格模式下) - 但类中的方法默认开启了局部的严格模式,所以执行
changeWeather
时,如不使用bind
更改 this 执行,changeWeather
中的 this 为undefined
- 状态 state 不可以直接更改,状态必须通过
setState
进行更新,且更新是一种合并,不是替换
组件自定义方法中的 this 问题(undefined):
<button onClick={(ev) => {this.handle(ev)} }>click</button>
<button onClick={this.handle.bind(this)}>click</button>
handle = (ev) => { console.log(this, ev) }
(保证 this 是当前类的实例)
class Weather extends React.Component {
constructor(props) {
super(props)
this.state = { isHot: true, wind: '微风' }
// 解决changeWeather中this指向问题
this.changeWeather = this.changeWeather.bind(this)
}
render() {
const { isHot, wind } = this.state
return (
<h2 onClick={this.changeWeather}>
今天天气很{isHot ? '炎热' : '凉爽'},{wind}
</h2>
)
}
changeWeather() {
// state必须通过setState进行更新
this.setState({ isHot: !this.state.isHot })
}
}
- 简写 state
class Weather extends React.Component {
state = { isHot: true, wind: '微风' }
render() {
const { isHot, wind } = this.state
return (
<h1 onClick={this.changeWeather}>
今天天气很{isHot ? '炎热' : '凉爽'},{wind}
</h1>
)
}
// 赋值语句+箭头函数
changeWeather = () => {
const isHot = this.state.isHot
this.setState({ isHot: !isHot })
}
}
组件实例三大属性 props
- 通过标签属性从组件外向组件内传递变化的数据
对 props 进行限制
- 使用
类.propTypes
- React 15.5 版本之前
name: React.PropTypes.string
,如果一直在 React 核心对象上加属性,导致 React 这个核心对象很大 - React 16 版本之后,需要引入
prop-types
库,用于对组件标签属性进行限制
注意:
- props 是只读的,
this.props.name = 'xxx'
会报错 - 在
React.Component
子类实现构造函数时,应在其它语句前调用super(props)
- 函数式组件不能用 state、refs(不使用 Hooks),但可以使用 props
class Person extends React.Component {
state = {}
// 对标签属性进行类型、必要性限制
static propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
sex: PropTypes.string,
speak: PropTypes.func,
}
// 指定默认标签属性值
static defaultProps = {
sex: '不男不女',
age: 18,
}
render() {
const { name, age, sex } = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>年龄:{age + 1}</li>
<li>性别:{sex}</li>
</ul>
)
}
}
const p = { name: 'tom', speak: () => {} }
ReactDOM.render(<Person {...p} />, document.getElementById('test'))
- 函数式组件使用 props(函数式组件可以接收参数)
function Person(props) {
const { name, age, sex } = props
return (
<ul>
<li>姓名:{name}</li>
<li>年龄:{age + 1}</li>
<li>性别:{sex}</li>
</ul>
)
}
Person.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
sex: PropTypes.string,
speak: PropTypes.func,
}
Person.defaultProps = {
sex: '不男不女',
age: 18,
}
const p = { name: 'tom', speak: () => {} }
ReactDOM.render(<Person {...p} />, document.getElementById('test'))
组件实例三大属性 refs
- 字符串形式的 ref
ref="input"
(现在不推荐使用,效率不高) - 函数形式的 ref
ref={c => this.input1 = c}
(回调函数) createRef
创建 ref 容器【React 最推荐的写法】
在条件允许的情况下,尽可能不使用 ref="input"
关于回调 refs
的说明:
-
如果
ref
回调函数是以内联函数方式定义的,在更新过程中它会执行两次,第一次传入参数 null,第二次传入参数 DOM 元素 -
因为在每次渲染时会创建一个新的函数实例,React 情况旧的
ref
并设置新的【无关紧要】如需解决,可以把内联函数放到一个函数里面,这样每次调用都不是一个新的函数,也就不会执行两次了(定义成 class 的绑定)
class Demo extends React.Component {
state = { isHot: false }
changeWeather = () => {
this.setState({ isHot: !this.state.isHot })
}
saveInput = c => {
this.input1 = c
console.log('@', c)
}
render() {
const { isHot } = this.state
return (
<div>
<h2>今天天气很{isHot ? '炎热' : '凉爽'}</h2>
{/*<input
ref={c => {
this.input1 = c
console.log('@', c)
}}
type="text"
/>*/}
<input ref={this.saveInput} type="text" />
<button onClick={this.changeWeather}>切换天气</button>
</div>
)
}
}
React.createRef
调用后可以返回一个容器,该容器可以存储被 ref 所标识的节点,该容器是"专人专用"的
class Demo extends React.Component {
myRef = React.createRef()
showData = () => {
console.log(this.myRef.current.value)
}
render() {
return (
<div>
<input ref={this.myRef} type="text" />
<button onClick={this.showData}>提示左侧数据</button>
</div>
)
}
}
事件处理
-
通过
onXxx
属性指定事件处理函数(注意大小写)React 使用的是自定义(合成)事件,而不是使用的原生 DOM 事件 --> 为了更好的兼容性
React 中的事件是通过事件委托方式处理的(委托给组件最外层元素) --> 为了高效
-
通过
event.target
得到发生事件的 DOM 元素对象不要过度使用 refs,发生事件的元素和操作的元素相同,可以通过
event.target
获取
class Demo extends React.Component {
myRef = React.createRef()
myRef2 = React.createRef()
showData = () => {
console.log(this.myRef.current.value)
}
showData2 = event => {
console.log(event.target.value)
}
render() {
return (
<div>
<input ref={this.myRef} type="text" placeholder="点击按钮提示数据" />
<button onClick={this.showData}>提示左侧数据</button>
<input onBlur={this.showData2} type="text" placeholder="失去焦点提示数据" />
</div>
)
}
}
受控组件与非受控组件
表单的组件分类:
-
受控组件
页面中表单元素随着输入将其维护到状态
state
里去,需要用时直接去状态中取即可(Vue 中双向数据绑定)
class Login extends React.Component {
handleSubmit = event => {
event.preventDefault()
const { username, password } = this
console.log(username.value, password.value)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
用户名:
<input ref={c => (this.username = c)} type="text" name="username" />
密码:
<input ref={c => (this.password = c)} type="password" name="password" />
<button>登录</button>
</form>
)
}
}
-
非受控组件
页面中表单元素现用现取
ref
如果不使用函数柯里化,可以这么写
<input onChange={e => this.saveFormData('username', e)} />
class Login extends React.Component {
state = {
username: '',
password: '',
}
handleSubmit = event => {
event.preventDefault()
const { username, password } = this.state
console.log(username, password)
}
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" />
密码:
<input onChange={this.saveFormData('password')} type="password" name="password" />
<button>登录</button>
</form>
)
}
}
组件生命周期(旧)
当 Clock
组件第一次被渲染到 DOM 中的时候,就为其设置一个定时器,在 React 中被称为挂载(mount)
当 DOM 中Clock
组件被删除的时候,应该清除定时器,在 React 中被称为卸载(unmount)
- 组件从创建到死亡它会经历一些特定的阶段
- React 组件中包含一系列钩子函数(生命周期回调函数),会在特定的时候调用
- 我们在定义组件时,会在特定的生命周期回调函数中做特定的工作
生命周期流程图(旧)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iPbvOddi-1622542494199)(https://gitee.com/lilyn/pic/raw/master/company-img/2_react生命周期(旧)].png)
-
**初始阶段:**由
ReactDOM.render()
触发——初次渲染-
constructor()
-
componentWillMount()
-
render()
-
componentDidMount()
常用:一般在这个钩子做一些初始化的事,例如:开启定时器、
发送网络请求、订阅消息
-
-
**更新阶段:**由组件内部
this.setState()
或父组件重新render
-
shouldComponentUpdate()
-
componentWillUpdate()
-
render()
初始化渲染或更新渲染调用
-
componentDidUpdate()
-
-
**卸载阶段:**由
ReactDOM.unmountComponentAtNode()
触发-
componentWillUnmount()
常用:一般在这个钩子中做一些收尾的事,例如:清理定时器、取消订阅消息
-
class Count extends React.Component {
constructor(props) {
console.log('Count---constructor')
super(props)
this.state = { count: 0 }
}
add = () => {
const { count } = this.state
this.setState({ count: count + 1 })
}
death = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}
force = () => {
this.forceUpdate()
}
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>
)
}
//组件将要挂载的钩子
componentWillMount() {
console.log('Count---componentWillMount')
}
//组件挂载完毕的钩子
componentDidMount() {
console.log('Count---componentDidMount')
}
//控制组件更新的 阀门
shouldComponentUpdate() {
console.log('Count---shouldComponentUpdate')
return true
}
//组件将要更新的钩子
componentWillUpdate() {
console.log('Count---componentWillUpdate')
}
//组件更新完毕的钩子
componentDidUpdate() {
console.log('Count---componentDidUpdate')
}
//组件将要卸载的钩子
componentWillUnmount() {
console.log('Count---componentWillUnmount')
}
}
注意:componentWillReceiveProps
组件将要接收新的 props 的钩子(第一次初始化不算)
//父组件A
class A extends React.Component {
state = { carName: '奔驰' }
changeCar = () => {
this.setState({ carName: '奥拓' })
}
render() {
return (
<div>
<div>我是A组件</div>
<button onClick={this.changeCar}>换车</button>
<B carName={this.state.carName} />
</div>
)
}
}
//子组件B
class B extends React.Component {
componentWillReceiveProps(props) {
console.log('B---componentWillReceiveProps', props)
}
shouldComponentUpdate() {
console.log('B---shouldComponentUpdate')
return true
}
componentWillUpdate() {
console.log('B---componentWillUpdate')
}
componentDidUpdate() {
console.log('B---componentDidUpdate')
}
render() {
console.log('B---render')
return <div>我是B组件,接收到的车是:{this.props.carName}</div>
}
}
组件生命周期(新)
生命周期流程图(新)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rjVWYiit-1622542494202)(https://gitee.com/lilyn/pic/raw/master/company-img/3_react生命周期(新)].png)
-
新版本废弃 3 个钩子:需要给
componentWillMount
、componentWillUpdate
、componentWillReceiveProps
,都需要加UNSAFE_
。所有带will
(除了 componentWillUnmount),都需要加UNSAFE_
-
新版本新增 2 个钩子:
getDerivedStateFromProps
(state 的值在任何时候都取决于 props)、getSnapshotBeforeUpdate
class Count extends React.Component {
constructor(props) {
console.log('Count---constructor')
super(props)
this.state = { count: 0 }
}
add = () => {
const { count } = this.state
this.setState({ count: count + 1 })
}
death = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}
force = () => {
this.forceUpdate()
}
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>
)
}
// 若state的值在任何时候都取决于props,那么可以使用这个钩子
static getDerivedStateFromProps(props, state) {
console.log('Count---getDerivedStateFromProps', props, state)
return null
}
// 在更新之前获取快照
getSnapshotBeforeUpdate() {
console.log('Count---getSnapshotBeforeUpdate')
return 'bird'
}
//组件挂载完毕的钩子
componentDidMount() {
console.log('Count---componentDidMount')
}
//控制组件更新的 阀门
shouldComponentUpdate() {
console.log('Count---shouldComponentUpdate')
return true
}
//组件更新完毕的钩子
componentDidUpdate(preProps, preState, snapshotVal) {
console.log('Count---componentDidUpdate', preProps, preState, snapshotVal)
}
//组件将要卸载的钩子
componentWillUnmount() {
console.log('Count---componentWillUnmount')
}
}
getSnapshotBeforeUpdate
- 在完成更新之前获取一些信息
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="list" ref="list">
{this.state.newsArr.map((n, index) => {
return (
<div key={index} className="news">
{n}
</div>
)
})}
</div>
)
}
}
DOM 的 Diffing 算法
注意:Diff 最小的力度是标签(节点)
面试常问:
- React/Vue 中的 key 有什么作用(key 内部原理)
- 为什么遍历列表时,key 最好不用 index
虚拟 DOM 中 key 的作用:
-
简单来说:key 是虚拟 DOM 对象的标识,在更新显示 key 起着极其重要的作用
-
详细的说:当状态中的数据发生变化时,React 会 新数据 生成 新的虚拟 DOM ,随后 React 进行 新的虚拟 DOM 与 旧虚拟 DOM 的 diff 比较,比较规则如下:
-
旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key
若虚拟 DOM 中内容没变,直接使用之前的真实 DOM
若虚拟 DOM 中内容变了,则生成新的真实 DOM,随后替换掉页面中之前的真实 DOM
-
旧虚拟 DOM 中未找到与新虚拟 DOM 相同的 key
根据数据创建真实 DOM,随后渲染到页面
-
用 index 作为 key 可能会引发的问题:
-
若对数据进行:逆序添加、逆序删除等破坏顺序操作:
会产生没有必要的真实 DOM 更新(页面效果没问题,但效率低)
-
如果结构中还包含输入类的 DOM:
会产生错误 DOM 更新(页面有问题)
-
注意:如果不存在对数据的逆序添加、逆序删除等破坏顺序操作
禁用于渲染列表用于展示,使用 index 作为 key 是没有问题的
开发中如何选择 key:
- 最好使用每条数据的唯一标识作为 key,比如 id、手机号、身份证号、学号等唯一值
- 如果确定只是简单的展示数据,用 index 也是可以的
/*
* 使用index索引作为key
初始数据:
{ id: 1, name: 'bird', age: 18 },
{ id: 2, name: 'dog', age: 19 },
初始的虚拟DOM:
<li key=0>bird---18<input type="text" /></li>
<li key=1>dog---18<input type="text" /></li>
更新后的数据:
{ id: 3, name: 'cat', age: 20 },
{ id: 1, name: 'bird', age: 18 },
{ id: 2, name: 'dog', age: 19 },
更新后的虚拟DOM(出现数据错乱问题)
<li key=0>cat---20<input type="text" /></li>
<li key=1>bird---18<input type="text" /></li>
<li key=2>dog---18<input type="text" /></li>
*/
/*
* 使用id唯一标识作为key
初始数据:
{ id: 1, name: 'bird', age: 18 },
{ id: 2, name: 'dog', age: 19 },
初始的虚拟DOM:
<li key=1>bird---18<input type="text" /></li>
<li key=2>dog---18<input type="text" /></li>
更新后的数据:
{ id: 3, name: 'cat', age: 20 },
{ id: 1, name: 'bird', age: 18 },
{ id: 2, name: 'dog', age: 19 },
更新后的虚拟DOM
<li key=3>cat---20<input type="text" /></li>
<li key=1>bird---18<input type="text" /></li>
<li key=2>dog---18<input type="text" /></li>
*/
class Person extends React.Component {
state = {
persons: [
{ id: 1, name: 'bird', age: 18 },
{ id: 2, name: 'dog', age: 19 },
],
}
add = () => {
const { persons } = this.state
const p = { id: persons.length + 1, name: 'cat', age: 20 }
this.setState({ persons: [p, ...persons] })
}
render() {
return (
<div>
<h2>展示信息</h2>
<button onClick={this.add}>添加一个人</button>
<h3>使用index(索引值)作为key</h3>
<ul>
{this.state.persons.map((personObj, index) => {
return (
<li key={index}>
{personObj.name}---{personObj.age}
<input type="text" />
</li>
)
})}
</ul>
<hr />
<hr />
<h3>使用id(数据唯一标识)作为key</h3>
<ul>
{this.state.persons.map((personObj, index) => {
return (
<li key={personObj.id}>
{personObj.name}---{personObj.age}
<input type="text" />
</li>
)
})}
</ul>
</div>
)
}
}