跟着尚硅谷的天禹老师学习React
看视频可以直接点击 b站视频地址
React中的事件处理
补充ref
上面的ref在React官网中提到不要被过度使用,在一些情况下可以使用其他方法来获取数据,比如可以通过event.target.value来操作,或者当触发事件的节点恰好是我们要操作的节点时,都可以从event里面拿到数据。或者受控组件也并不需要ref拿数据。
非受控组件
页面中所有输入类DOM的值“现用现取”的组件就叫非受控组件
<div id="container"></div>
<script>
// 创建组件
class Login extends React.Component{
handleSubmit = (event) => {
event.preventDefault()
const { username, password} = this
alert(`用户名为${username.value},密码为${password.value}`)
}
render(){
return (
<form action="#" onSubmit={this.handleSubmit}>
用户名:<input ref={(currentNode)=>{this.username = currentNode}} type="text" />
密码:<input ref={(currentNode)=>{this.password = currentNode}} type="password" />
<button>登录</button>
</form>
)
}
}
// 渲染组件
ReactDOM.render(<Login/>,document.getElementById('container'))
</script>
受控组件
随着输入保存到状态中,可以叫作受控组件。
原生也提供了受控组件的写法,onchange事件。
这样做似乎可以减少写ref,但是state变多了。
<div id="container"></div>
<script>
// 创建组件
class Login extends React.Component{
// 初始化状态
state = {
username:'',
password:''
}
// 表单提交的回调
handleSubmit = (event) => {
event.preventDefault()
const { username, password} = this.state // 这里直接从state里面取了
alert(`用户名为${username},密码为${password}`) // 这里把.value给去掉了
}
// 保存用户名到状态中
saveUsername = (event) => {
this.setState({
username:event.target.value
})
}
// 保存密码到状态中
savePassword = (event) => {
this.setState({
password:event.target.value
})
}
render(){
return (
<form action="#" onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveUsername} type="text" />
密码:<input onChange={this.savePassword} type="password" />
<button>登录</button>
</form>
)
}
}
// 渲染组件
ReactDOM.render(<Login/>,document.getElementById('container'))
</script>
组件生命周期
高阶函数与函数柯里化
上面的代码,如果遇到很多表单项,那就要写很多的事件处理函数,这样编码效率就太低了。
- 高阶函数:如果一个函数符合下面两个规范中的任意一个,那么这个函数就是高阶函数
- 若A函数,接受的参数是一个函数,那么A就可以称为高阶函数
- 若A函数,调用的返回值依然是一个函数,那么A就可以称为高阶函数
- 函数柯里化:通过函数调用继续返回函数的方式,实现多次接受参数最后统一处理的函数编写形式。
<div id="container"></div>
<script>
// 创建组件
class Login extends React.Component{
// 初始化状态
state = {
username:'',
password:''
}
// 表单提交的回调
handleSubmit = (event) => {
event.preventDefault()
const { username, password} = this.state // 这里直接从state里面取了
alert(`用户名为${username},密码为${password}`) // 这里把.value给去掉了
}
// 保存表单数据到状态中,返回一个函数
saveFormData(dataType){
return (event) => {
this.setState({
[dataType]:event.target.value // 变量作为key,用中括号
})
}
}
render(){
return (
<form action="#" onSubmit={this.handleSubmit}>
用户名:<input onChange={this.saveFormData('username')} type="text" />
密码:<input onChange={this.saveFormData('password')} type="password" />
<button>登录</button>
</form>
)
}
}
// 渲染组件
ReactDOM.render(<Login/>,document.getElementById('container'))
</script>
上面的saveFormData就是一个高阶函数。
常见的高阶函数:
- Promise,new Promise(resolve,reject)
- setTimeout,setTimeout(callback,delay)
- 数组遍历器,map、reduce等等等等
柯里化编程:
function sum(a){
return (b)=>{
return (c)=>{
return a+b+c
}
}
}
console.log(sum(1)(2)(3))
但我们写的saveFormData实际上也是一个柯里化的函数,对于上面的场景,最合适的就是柯里化编程。
不用柯里化的写法
saveFormData(dataType,value){
this.setState({
[dataType]:value // 变量作为key,用中括号
})
}
render(){
return (
<form action="#" onSubmit={this.handleSubmit}>
用户名:<input onChange={(event)=>{this.saveFormData('username',event.target.value)}} type="text" />
密码:<input onChange={(event)=>{this.saveFormData('password',event.target.value)}} type="password" />
<button>登录</button>
</form>
)
}
生命周期
概念
生命周期回调函数、生命周期钩子函数 、生命周期函数 、生命周期钩子。
- 组件从创建到死亡会经历一系列的特定阶段
- React组建中包含一系列钩子函数
- 我们在定义组件时,会在特定的钩子中做特定的工作
<div id="container"></div>
<script>
// 实现渐变透明度功能
// 创建组件
// 生命周期回调函数 <=> 生命周期钩子函数 <=> 生命周期函数 <=> 生命周期钩子
class Life extends React.Component{
state = {
opacity:1
}}
death = () => {
// 卸载组件
ReactDOM.unmountComponentAtNode(document.getElementById('container')))
}
// 挂载后的生命周期,在初次render后执行,有点像Vue的mounted
componentDidMount(){
// 获取定时器id
this.timer = setInterval(()=>{
let { opacity } = this.state
opacity -= 0.1
if(opacity <= 0){
opacity = 1
}
this.setState({opacity})
},200)
}
// 组件将要卸载的生命周期,有点像Vue的beforeDestroy
componentWillUnmount(){
// 清除定时器
clearInterval(this.timer)
}
// render调用的时机:初始化渲染、状态更新后
render(){
// 如果直接写在render中,将触发一个无限的递归调用
// 定时器->setState->render->定时器
// setInterval(()=>{
// let { opacity } = this.state
// opacity -= 0.1
// if(opacity <= 0){
// opacity = 1
// }
// this.setState({opacity})
// },200)
return (
<div>
<h2 style={{opacity:this.state.opacity}}>React学不会怎么办?</h2>
<button onClick={this.death}>不活了</button>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<Life/>,document.getElementById('container'))
</script>
旧版生命周期流程图
图截取自视频中
左侧是挂载右侧是更新
<div id="container"></div>
<script>
class Father extends React.Component{
state = {
carName:'奔驰'
}
render(){
return (
<div>
<div>Father组件</div>
<button onClick={this.changeCar}>换车</button>
<Son carName={this.carName} />
</div>
)
}
}
class Son extends React.Component{
/*
挂载时的三个狗子
*/
// contructor
// 组件将要被挂载时触发的钩子
componentWillMount(){
console.log('componentWillMount')
}
// render
// 组件挂载完毕的钩子
componentDidMount(){
console.log('componentDidMount')
}
// 组件将要卸载的钩子
componentWillUnmount(){
console.log('componentWillUnmount')
}
/*
更新时的钩子
*/
// 组件将要接收【新】的props
componentWillReceiveProps(){
// 注意这里在第一次传入props不会触发这个钩子
console.log('componentWillReceiveProps')
}
// 控制setState以后是否更新的“阀门”
shouldComponentUpdate(){
console.log('shouldComponentUpdate')
return true // 这里如果不写布尔类型的返回值就会报错,写false就会阻塞调后面的钩子
}
// 组件将要更新的钩子
componentWillUpdate(){
console.log('componentWillUpdate')
}
// render
// 组件更新后的钩子
componentDidUpdate(){
console.log('componentDidUpdate')
}
render(){
console.log('render')
return (
<div>
<div>Son组件</div>
<div>{this.props.carName}</div>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<A/>,document.getElementById('container'))
</script>
总结生命周期
- 初始化阶段:由ReactDOM.render()触发 初次渲染
- constructor()
- componentWillMount()
- render()
- componentDidMount()【常用】 一般在这里初始化,例如:开启定时器、订阅消息、发送网络请求
- 更新阶段:由组件内部this.setState()或父组件render触发
- shouldComponentUpdate()
- componentWillUpdate()
- render()【一定会用】
- componentDidUpdate()
- 卸载组件:由ReactDOM.unmountComponentNode触发
- componentWillUnmount()【常用】一般在这里做一些收尾的事情,例如:关闭定时器、取消订阅消息。
新旧生命周期对比
新生命周期
新版本也可以兼容旧钩子,只是会报Warning。
componentWillMount、componentWillReceiveProps、componentWillUpdate前面都要加上“UNSAFE_”
也就是除了componentWillUnmount以外,所有带Will的都要加。
这三个声明周期被React官方认为是不安全的,因为目前React官方正在做“异步渲染更新”。UNSAFE前缀并非只安全性有问题,而是在未来版本有可能会出现bug,尤其是在退出异步渲染之后。同时目前也为这三个钩子开启了弃用警告。
- componentWillReceiveProps => getDerivedStateProps
- render和DidUpdate之间插入了一个getSnapshotBeforeUpdate
- “删三增二”,这两个新的好像也很少用。
getDerivedStateErrorProps
这个生命周期用于一个十分罕见的用例:state的值在任何时候都取决于props。
派生状态会导致代码冗余,并使组件难以维护。确保在以下替代方案中不能实现的情况下在使用这个方法:
- 如果需要执行副作用(比如数据提取或者动画)以响应props中的更改,推荐使用componentDidUpdate
- 如果只想在prop更改时冲i性能计算某些数据,使用memoization helper来代替。
- 如果想在prop更改时“重置”某些state,考虑使用完全受控或使用key使组件完全不受控。
要注意:因为是静态方法,所以无法访问组件实例,如果需要可以通过提取组件props的纯函数和class外的状态,在这个方法和其他class方法之间重用代码。
实现滚动条停留在当前页面,不被挤下去:
// 从props中派生出状态
static getDerivedStateFromPros(props){ // 接收props作为参数
console.log('getDerivedStateFromPros')
return props //
// return null // 需要返回一个state对象 要和state里面的key-value对应上
// 利用返回值可以修改状态 返回值会替代状态对象
}
getSnapshotBeforeUpdate
这个生命周期在最近一次渲染输出(提交到DOM节点)之前调用。它使得组件能在发生更改之前从DOM中捕获一些信息(比如滚动位置)。此生命周期的任何返回值将作为参数传递给componentDidUpdate()
应该返回一个snapshot或者null。此生命周期并不常用。
<div id="container">
</div>
<script>
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(oldProps,oldState,height){
// 接收height值 实现滚动条停留在当前页面,不被挤下去
this.refs.list.scrollTop += this.refs.list.scrollTop - height
}
render(){
return (
<div class="list" ref="list">
{
this.state.newsArr.map((news,index)=>{
return <div className="news" key={index} >{ news }</div>
})
}
</div>
)
}
}
ReactDOM.render(<NewsList/>,document.getElementById('container'))
</script>
<style>
.list{
width:200px;
height:150px;
background-color:skyblue;
overflow:auto;
}
.news{
height:30px;
}
</style>
下面这段代码可以忽略。
<div id="container"></div>
<script>
class Father extends React.Component{
state = {
carName:'奔驰'
}
render(){
return (
<div>
<div>Father组件</div>
<button onClick={this.changeCar}>换车</button>
<Son carName={this.carName} />
</div>
)
}
}
class Son extends React.Component{
/*
挂载时的三个狗子
*/
// contructor
// 从props中派生出状态
static getDerivedStateFromPros(props,state){ // 接收props作为参数
console.log('getDerivedStateFromPros')
return props //
// return null // 需要返回一个state对象 要和state里面的key-value对应上
// 利用返回值可以修改状态 返回值会替代状态对象
}
// render
// 这个生命周期在最近一次渲染输出(提交到DOM节点)之前调用,接收三个参数
getSnapshotBeforeUpdate(oldProps,oldState,snapshotValue){
}
// 组件挂载完毕的钩子
componentDidMount(){
console.log('componentDidMount')
}
// 组件将要卸载的钩子
componentWillUnmount(){
console.log('componentWillUnmount')
}
/*
更新时的钩子
*/
// 控制setState以后是否更新的“阀门”
shouldComponentUpdate(){
console.log('shouldComponentUpdate')
return true // 这里如果不写布尔类型的返回值就会报错,写false就会阻塞调后面的钩子
}
// render
// 注意这个钩子有两个参数
// 组件更新后的钩子 接收两个参数 一个旧的props,一个旧的state
componentDidUpdate(oldProps,oldState){
console.log('componentDidUpdate')
}
render(){
console.log('render')
return (
<div>
<div>Son组件</div>
<div>{this.props.carName}</div>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<A/>,document.getElementById('container'))
</script>
生命周期总结
- 初始化阶段:由React.render()触发——初次渲染
- contructor()
- getDerivedStateFromProps()
- render() 【最重要】
- componentDidMount()【常用】
- 更新阶段:由组件内部this.setState() 或父组件重新render触发
- getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
- 卸载组件:由React.unmountComponentAtNode触发
- componentWiilUnmount() 【常用】
React的diffing算法
现在这段代码,如果每次React都会渲染的话,则当在input中输入时,input会在1s内刷新,不能完成输入。显然React并没有重新渲染这一个组件的全部,而是把span标签中的文字重新渲染了。
<div id="container"></div>
<script>
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()}</span>
</div>
)
}
}
ReactDOM.render(<Time/>,document.getElementById('container'))
</script>
经典面试题
- React/Vue中的key有什么作用?(key的内部原理是什么)
- 为什么遍历列表时,key最好不要用index
- 虚拟DOM中key的作用:
- 简单地说,key时虚拟DOM对象的标识,在更新视图时key起着极其重要的作用
- 详细地说,当状态中的数据发生改变时,React会根据【新数据】生成【新的虚拟DOM】,随后React进行【新虚拟DOM】与【旧虚拟DOM】的diff比较,比较规则如下:
- 旧虚拟DOM中找到了与新虚拟DOM中相同的key:
a.若虚拟DOM中内容没变,直接使用之前的真实DOM
b.若虚拟DOM中的内容改变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM - 旧虚拟DOM中未找到与新虚拟DOM相同的key
根据数据创建新的真实DOM,然后渲染到页面
- 旧虚拟DOM中找到了与新虚拟DOM中相同的key:
- 用index作为key可能会引发的问题:
- 若对数据进行:逆序添加、逆序删除等破坏顺序的操作:
会产生没有必要的真实DOM更新,即不会影响页面效果,但效率低 - 如果结构中还包输入类的DOM:
会产生错误的DOM更新,即页面会有问题 - 注意:如果不存在对数据的逆序添加、逆序删除等破坏顺序的操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。
- 若对数据进行:逆序添加、逆序删除等破坏顺序的操作:
- 开发中如何选择key?:
- 最好使用每条数据的唯一标识作为key,比如id、手机号、身份证号、学号等唯一值。
- 如果确定只是见到那的展示数据,用index也是可以的。
- 后端不给key那就只能打他了
<div id="container"></div>
<script>
class Person extends React.Component{
/*
使用index(索引值)作为key
初始数据:
{id:1,name:'zhang',age:18}
{id:2,name:'li',age:19}
初始化虚拟DOM:
<li key=0>zhang--18<input type="text"/></li>
<li key=1>li--19<input type="text"/></li>
更新后的数据:
{id:3,name:'wang',age:20}
{id:1,name:'zhang',age:18}
{id:2,name:'li',age:19}
更新后的虚拟DOM:
<li key=0>wang--20<input type="text"/></li>
<li key=1>zhang--18<input type="text"/></li>
<li key=2>li--19<input type="text"/></li>
结果:触发了三次视图更新,如果是很多数据,这是非常消耗效率的
包含输入类组件时,添加新的数据后,直接错误,原来zhang的输入信息被添加到了wang的节点上,li的输入信息消失了
*/
/*
使用id(数据唯一标识)作为key
初始数据:
{id:1,name:'zhang',age:18}
{id:2,name:'li',age:19}
初始化虚拟DOM:
<li key=1>zhang--18<input type="text"/></li>
<li key=2>li--19<input type="text"/></li>
更新后的数据:
{id:3,name:'wang',age:20}
{id:1,name:'zhang',age:18}
{id:2,name:'li',age:19}
更新后的虚拟DOM:
<li key=3>wang--20<input type="text"/></li>
<li key=1>zhang--18<input type="text"/></li>
<li key=2>li--19<input type="text"/></li>
结果:只触发了一次视图更新
*/
state = {
persons:{
id:1,name:'zhang',age:18,
id:2,name:'li',age:19
}
}
add = () => {
const {persons} = this.state
const p = {id:persons.length+1,name:'wang',age:20}
this.setState({persons:[p,...persons]})
}
componentDidMount(){
}
render(){
return (
<div>
<h1>使用index(索引值)来作为key</h1>
<button onClick={this.addWang}>添加一个小王</button>
<ul>
{
this.state.persons.map((person,index)=>{
return <li key={index}>{person.name}--{person.age} <input type="text"/></li>
})
}
</ul>
<h1>使用id(数据唯一标识)来作为key</h1>
<button onClick={this.addWang}>添加一个小王</button>
<ul>
{
this.state.persons.map((person,index)=>{
return <li key={person.id}>{person.name}--{person.age} <input type="text"/></li>
})
}
</ul>
</div>
)
}
}
ReactDOM.render(<Person/>,document.getElementById('container'))
</script>
可以理解一下,当添加了一个新的person后,新key为1,发现内容全变了,然后是key为2,内容也完全变了,key为3的是新的依然要重新渲染。如果处理合适的话,其实只需要渲染的是那个新添加的person。