【前端学习】React学习笔记-事件、生命周期、虚拟DOMdiffing

8 篇文章 0 订阅

跟着尚硅谷的天禹老师学习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>

组件生命周期

高阶函数与函数柯里化

上面的代码,如果遇到很多表单项,那就要写很多的事件处理函数,这样编码效率就太低了。

  • 高阶函数:如果一个函数符合下面两个规范中的任意一个,那么这个函数就是高阶函数
    1. 若A函数,接受的参数是一个函数,那么A就可以称为高阶函数
    2. 若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就是一个高阶函数。
常见的高阶函数:

  1. Promise,new Promise(resolve,reject)
  2. setTimeout,setTimeout(callback,delay)
  3. 数组遍历器,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>
总结生命周期
  1. 初始化阶段:由ReactDOM.render()触发 初次渲染
    1. constructor()
    2. componentWillMount()
    3. render()
    4. componentDidMount()【常用】 一般在这里初始化,例如:开启定时器、订阅消息、发送网络请求
  2. 更新阶段:由组件内部this.setState()或父组件render触发
    1. shouldComponentUpdate()
    2. componentWillUpdate()
    3. render()【一定会用】
    4. componentDidUpdate()
  3. 卸载组件:由ReactDOM.unmountComponentNode触发
    1. componentWillUnmount()【常用】一般在这里做一些收尾的事情,例如:关闭定时器、取消订阅消息。
新旧生命周期对比

新生命周期
在这里插入图片描述
新版本也可以兼容旧钩子,只是会报Warning。
componentWillMount、componentWillReceiveProps、componentWillUpdate前面都要加上“UNSAFE_”
也就是除了componentWillUnmount以外,所有带Will的都要加。
这三个声明周期被React官方认为是不安全的,因为目前React官方正在做“异步渲染更新”。UNSAFE前缀并非只安全性有问题,而是在未来版本有可能会出现bug,尤其是在退出异步渲染之后。同时目前也为这三个钩子开启了弃用警告。

  1. componentWillReceiveProps => getDerivedStateProps
  2. render和DidUpdate之间插入了一个getSnapshotBeforeUpdate
  3. “删三增二”,这两个新的好像也很少用。
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>
生命周期总结
  1. 初始化阶段:由React.render()触发——初次渲染
    1. contructor()
    2. getDerivedStateFromProps()
    3. render() 【最重要】
    4. componentDidMount()【常用】
  2. 更新阶段:由组件内部this.setState() 或父组件重新render触发
    1. getDerivedStateFromProps()
    2. shouldComponentUpdate()
    3. render()
    4. getSnapshotBeforeUpdate()
    5. componentDidUpdate()
  3. 卸载组件:由React.unmountComponentAtNode触发
    1. 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>
经典面试题
  1. React/Vue中的key有什么作用?(key的内部原理是什么)
  2. 为什么遍历列表时,key最好不要用index
  • 虚拟DOM中key的作用:
    1. 简单地说,key时虚拟DOM对象的标识,在更新视图时key起着极其重要的作用
    2. 详细地说,当状态中的数据发生改变时,React会根据【新数据】生成【新的虚拟DOM】,随后React进行【新虚拟DOM】与【旧虚拟DOM】的diff比较,比较规则如下:
      • 旧虚拟DOM中找到了与新虚拟DOM中相同的key:
        a.若虚拟DOM中内容没变,直接使用之前的真实DOM
        b.若虚拟DOM中的内容改变了,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
      • 旧虚拟DOM中未找到与新虚拟DOM相同的key
        根据数据创建新的真实DOM,然后渲染到页面
  • 用index作为key可能会引发的问题:
    1. 若对数据进行:逆序添加、逆序删除等破坏顺序的操作:
      会产生没有必要的真实DOM更新,即不会影响页面效果,但效率低
    2. 如果结构中还包输入类的DOM:
      会产生错误的DOM更新,即页面会有问题
    3. 注意:如果不存在对数据的逆序添加、逆序删除等破坏顺序的操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。
  • 开发中如何选择key?:
    1. 最好使用每条数据的唯一标识作为key,比如id、手机号、身份证号、学号等唯一值。
    2. 如果确定只是见到那的展示数据,用index也是可以的。
    3. 后端不给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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值