React修仙笔记,筑基初期之更新数据

简介: 在之前的一篇文章中我们有了解到react函数组件和class组件,以及react数据流,状态提升,以及react设计哲学,在我们了解了这些基本的知识后,我们需要了解react内部更深的一些知识

在之前的一篇文章中我们有了解到react函数组件和class组件,以及react数据流,状态提升,以及react设计哲学,在我们了解了这些基本的知识后,我们需要了解react内部更深的一些知识


在开始本文之前,主要会从以下几个点去认识react,以及我们那些我们常常遇到的坑


  • react是如何更新数据的,更新数据到底有些注意点
  • reactsetState有哪些你需要知道的
  • 如何优化组件渲染
  • Context[1]跨组件通信


正文开始...


react是如何更新数据的


我们在react中更新数据都是调用setState这个方法去更新的,这个更新也是批量异步更新的,在setState更新数据,主要发生了什么,我们看一个简单的栗子,这也是我们上次的一个例子

import React from "react";
class List extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0,
            price: 0,
            type: 0, // 0 人民币 1 代表美元
        }
    }
    handleAdd = () => {
        // this.setState({
        //     count: ++this.state.count
        // })
        this.setState(state => {
            return {
                ...state,
                count: state.count + 1
            }
        })
    }
    handleReduce = () => {
        this.setState(state => {
            return {
                count: state.count - 1
            }
        })
    }
    handleRmbInput = (e) => {
        const val = e.target.value;
        console.log(e)
        this.setState({
            price: val,
            type: 0
        })
    }
    handleDollInput = (e) => {
        const val = e.target.value;
        this.setState({
            price: val,
            type: 1
        })
    }
    render() {
        const { count, price, type } = this.state;
        const ExchangeInput = ({ price, handleChange, type }) => {
            return (<fieldset>
                <legend>{type === "RmbInput" ? '人民币' : '美元'}</legend>
                <input value={price}
                    onChange={handleChange} />
            </fieldset>)
        }
        const rmbVal = type === 0 ? price : price * 7.34;
        const dollVal = type === 1 ? price : 0.14 * price;
        return (<div className="list-app" style={{ padding: '10px' }}>
            <hr />
            <button onClick={this.handleAdd}>+</button>
            <span>{count}</span>
            <button onClick={this.handleReduce}>-</button>
            <hr />
            <ExchangeInput handleChange={this.handleRmbInput} price={rmbVal} type="RmbInput"></ExchangeInput>
            <hr></hr>
            <ExchangeInput handleChange={this.handleDollInput} price={dollVal} type="DollarInput"></ExchangeInput>
        </div>)
    }
}
export default List

我们先看下这个计数器,当我点击+时,数字就会+1,当我点击-时,就会-1

handleAdd = () => {
    this.setState({
        count: ++this.state.count
    });
    console.log(this.state, 'count')
}


这样貌似就可以更新数据了,我们会发现state打印的值是{count: 1, price: 0, type: 0}


在这之前不是说setState是一个异步的操作吗?怎么打印是好像是同步的一样?但事实并非如此,如果你是这么改,那么你会发现

handleAdd = () => {
    this.setState({
        count: 2
    });
    console.log(this.state, 'count')
}

此时的结果就是{count: 0, price: 0, type: 0} 'count'


这就证明setState这个方法就是异步的,也就说明在handleAdd这个内部方法中,优先执行了同步任务执行打印操作,然后再执行setState


但是上面的一个例子貌似表现跟这个不太一样,那是因为对象值引入的问题,上面那段代码可以拆分下面这样

handleAdd = () => {
    this.state.count+=1;
    const initState = {
      count: this.state.count
    }
    this.setState(initState);
    console.log(this.state, 'count')
}

因为++this.state.count相当于this.state.count+=1,在调用setState方法时,实际上这个statecount就已经更改,而this.setState这个方法是异步,所以会先执行打印,所以打印的值自然是修改的值了,都是同一个对象的引用。


setState


对于setState设置数据是异步的,我们还需要知道如果想立即获取修改后的数据呢,我们先看下setState这个方法

handleAdd = () => {
   console.log(this.setState);
   this.setState({
      count: 2
  });
  console.trace()
}


在源码里,就是这样的

Component.prototype.setState = function (partialState, callback) {
  if (typeof partialState !== 'object' && typeof partialState !== 'function' && partialState != null) {
    throw new Error('setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.');
  }
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

我们看到源码setState方法是挂载在Componentprototype上的,所以我们用this就能访问该组件上的setState,而Component就是一个构造函数

function Component(props, context, updater) {
  this.props = props;
  this.context = context; // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject; // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}

我们看到setState内部方法有对入参进行类型判断,也就是说必须是传入一个对象或者函数而且不为null否则就会抛出错误


所以我们可以把setState的第一个参数也可以改成函数试试,但是必须返回一个对象,否则并不会更新UI

handleAdd = () => {
    this.setState(state => {
        return {
            ...state,
            count: state.count + 1
        }
    })
    // 这里依然是上一次的值
    console.log(this.state, 'count')
}

从源码我们知道setState除了第一个参数是对象或者函数外,也提供了第二个回调参数callback,这个通常在项目中也会很有用,就是在你更新state后想立即拿着state去做一些其他操作时

handleAdd = () => {
    this.setState(state => {
        return {
            ...state,
            count: state.count + 1
        }
    }, () => {
        // 立即获取修改后的值
        console.log(this.state)
    })
}


看下结果

ecbb812de2ed9de929658202e9f2a337.png

我们可以修改值后,在回调函数后就立即更新值了,我们从执行setState这个方法中也看到实际上更新UI的过程中也调用内部其他很多方法,每次触发setState都会执行render函数


而我们注意到在开发环境render内部的console.log会被打印两次,这点,官方有解释[2],主要是开发环境双调用了渲染生命周期,帮助在渲染中可以查找出副作用引出的问题.


所以我们可以把ExchangeInput组件可以提出去,不在render中定义,这样性能上会更好


组件渲染优化


一个组件当state或者props发生变化时,就会更新组件,具体写个例子


新建一个组件Box组件,我可以通过父组件可以控制其宽度与高度,并且点击背景可以换肤

// Box.js
import React from "react";
class Box extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            divStyle: {
                width: props.count ? `${props.count * 100}px` : '100px',
                height: props.count ? `${props.count * 100}px` : '100px',
            },
            color: 'red'
        }
    }
    // 优化,默认是返回true,如果返回false,则不会渲染组件
    shouldComponentUpdate(nextProps, nextState) {
        if (this.props.count !== nextProps.count) {
            return true
        }
        if (this.state.color !== nextState.color) {
            return true;
        }
        return false
    }
    updateStyle = () => {
        const { divStyle } = this.state;
        if (!this.props.count) {
            return divStyle
        }
        return {
            width: `${this.props.count * 100}px`,
            height: `${this.props.count * 100}px`,
        }
    }
    handleChangeBg = () => {
        this.setState({
            color: this.state.color === 'red' ? 'green' : 'red'
        })
    }
    render() {
        return (<div style={{ ...this.updateStyle(), backgroundColor: this.state.color }} onClick={this.handleChangeBg}>
            <p>color: {this.state.color}</p>
            <p>count: {this.props.count}</p>
        </div>)
    }
}
export default Box

index.js中引入

import React from "react";
import Box from './Box'
class List extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 1,
            price: 0,
            type: 0, // 0 人民币 1 代表美元
        }
    }
    render() {
        // console.log(222)
        // console.trace();
        const { count, price, type } = this.state;
        const rmbVal = type === 0 ? price : price * 7.34;
        const dollVal = type === 1 ? price : 0.14 * price;
        return (<div className="list-app" style={{ padding: '10px' }}>
            <hr />
            <button onClick={this.handleAdd}>+</button>
            <span>{count}</span>
            <button onClick={this.handleReduce}>-</button>
            <hr />
            <Box count={count}></Box>
        </div>)
    }
}
export default List

cd0dfac1d90c0d92e4f037ce054d7f0f.png

当修改count时,就会更新这个box的宽度与高度


Box组件主要看这段代码

shouldComponentUpdate(nextProps, nextState) {
    if (this.props.count !== nextProps.count) {
        return true
    }
    if (this.state.color !== nextState.color) {
        return true;
    }
    return false
}

当我们调用setState后,就会调用这个钩子函数,这个钩子函数默认是返回true,如果你想优化,在某些条件下可以返回false不渲染组件。官方也说明了这个是一个浅比较[3],如果是引用数据类型,最好不要在原有数据上进行操作,因为是同一份引用,容易出问题。


关于浅比较,官方也给出了一种比较替换方案,你可以用PureComponent组件替代你这种现有做的优化


因此你可以替换成下面这样

import React from "react";
class Box extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            divStyle: {
                width: props.count ? `${props.count * 100}px` : '100px',
                height: props.count ? `${props.count * 100}px` : '100px',
            },
            color: 'red'
        }
    }
    // 优化,默认是返回true,如果返回false,则不会渲染组件
    // shouldComponentUpdate(nextProps, nextState) {
    //     if (this.props.count !== nextProps.count) {
    //         return true
    //     }
    //     if (this.state.color !== nextState.color) {
    //         return true;
    //     }
    //     return false
    // }
    updateStyle = () => {
        const { divStyle } = this.state;
        if (!this.props.count) {
            return divStyle
        }
        return {
            width: `${this.props.count * 100}px`,
            height: `${this.props.count * 100}px`,
        }
    }
    handleChangeBg = () => {
        this.setState({
            color: this.state.color === 'red' ? 'green' : 'red'
        })
    }
    render() {
        return (<div style={{ ...this.updateStyle(), backgroundColor: this.state.color }} onClick={this.handleChangeBg}>
            <p>color: {this.state.color}</p>
            <p>count: {this.props.count}</p>
        </div>)
    }
}
export default Box

所以PureComponent算是优化组件渲染简洁最快速的一种方法了


跨组件通信


react父子数据传递都是通过props,如果遇到嵌套组件好几级情况下,那么props传递将会一层一层传入孙组件中,有没有更好的解决办法呢?


有两种通用的方案,在react中你也可以用状态管理工具,比如redux将状态存储到全局的store中,那么跨组件任意一个组件都可以访问了,除了这种还有一种方案那么就是Context,这种方案有点类似vue中的provide/inject也是跨组件传递数据,不过react的做法要稍微复杂一些

// index.js
import React from "react";
import Box from './Box'
const initColor = {
    color: 'yellow',
    text: '黄色'
}
export const ColorContext = React.createContext(initColor);
class List extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 1,
            price: 0,
            type: 0, // 0 人民币 1 代表美元
            initColor
        }
    }
    render() {
        return (<div className="list-app" style={{ padding: '10px' }}>
           ...
            <ColorContext.Provider value={this.state.initColor} >
                <Box count={count}></Box>
            </ColorContext.Provider>
        </div>)
    }
}
export default List


从以上代码中可以看出主要是做了以下几件事


  • 创建createContext对象
  • 创建对象调用Provider包裹子组件Box并传入value初始值


我们继续看下Box这个组件

import React from "react";
import OtherContent from './OtherContent'
class Box extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            divStyle: {
                width: props.count ? `${props.count * 100}px` : '100px',
                height: props.count ? `${props.count * 100}px` : '100px',
            },
            color: 'red'
        }
    }
    ...
    render() {
        return (<div style={{ ...this.updateStyle(), backgroundColor: this.state.color }} onClick={this.handleChangeBg}>
            <p>color: {this.state.color}</p>
            <p>count: {this.props.count}</p>
            <OtherContent></OtherContent>
        </div>)
    }
}
export default Box

你会发现又引入了一个其他的组件OtherContent,这个组件就是孙组件,所以想要父组件的数据直接在孙组件上使用,那么我们可以在OtherContent组件中这么做

import React from "react";
import { ColorContext } from './index';
const OtherContent = () => {
    const state = React.useContext(ColorContext);
    console.log(state);
    const Pdom = () => <p style={{ color: state.color }}>context: {state.text}</p>
    return <Pdom></Pdom>
}
export default OtherContent;

从以上代码中我们可以看出,我们必须要做以下几件事


  • 引入创建的Contenxt
  • 调用useContext函数,其中形参就是我们引入创建的Contenxt,返回初始值


因此我们看到最终的结果就是下面这样的

8b388cea6ad7bbfa6050ee378ff94047.png

最顶层的数据就传入了孙子组件中去了。


如果我想改变孙组件OtherContent的状态呢?


只需要在初始数据中挂载一个回调函数,注意changeColor会从value传入子组件中

// index.js
import React from "react";
import Box from './Box'
const initColor = {
    color: 'yellow',
    text: '黄色',
    changeColor: () => { }
}
export const ColorContext = React.createContext(initColor);
class List extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0,
            price: 0,
            type: 0, // 0 人民币 1 代表美元
            initColor: {
                ...initColor,
                changeColor: this.changeColor
            },
        }
    }
    changeColor = () => {
        this.setState((state, props) => {
            return {
                ...state,
                initColor: {
                    ...state.initColor,
                    color: this.state.initColor.color === 'yellow' ? 'blue' : 'yellow',
                    text: this.state.initColor.text === '黄色' ? '蓝色' : '黄色',
                }
            }
        })
    }
    ...
    render() {
        const rmbVal = type === 0 ? price : price * 7.34;
        const dollVal = type === 1 ? price : 0.14 * price;
        return (<div className="list-app" style={{ padding: '10px' }}>
            <ColorContext.Provider value={this.state.initColor} >
                <Box count={count}></Box>
            </ColorContext.Provider>
        </div>)
    }
}
export default List

然后我们看下Box组件

import React from "react";
import OtherContent from './OtherContent'
import { ColorContext } from './index';
class Box extends React.PureComponent {
    ...
    render() {
        return (<div style={{ ...this.updateStyle(), backgroundColor: this.state.color }} onClick={this.handleChangeBg}>
            <p>color: {this.state.color}</p>
            <p>count: {this.props.count}</p>
            <ColorContext.Consumer>
                {
                    ({ changeColor }) => {
                        return (<>
                            <a href="javascript:void(0)" onClick={(e) => {
                                changeColor();
                                e.stopPropagation();
                            }}>改变OtherContent</a>
                            <OtherContent></OtherContent>
                        </>)
                    }
                }
            </ColorContext.Consumer>
        </div>)
    }
}
export default Box

如果我们想在Box组件改变OtherContent的组件状态,那么只需要用ColorContext.Consumer包裹起来,但是这里面必须返回一个组件才行,这个changeColor实际上就是父组件value传入到子组件里面的。


最后修改OtherContent的状态还是调用根组件上changeColor方法,也是调用setState修改了原初始值的数据。

e6a6b8d1b4855c7905d4c0b4a0c836ad.png


当我们点击OtherContent上面的文字时,就可以改变自身元素的state了。


总结


  • 当我们更新state主要是依赖setState这个方法,这个方法修改值是异步调用的
  • 我们要知道setState的第一个参数可以是对象也可以是函数,当是函数时必须返回一个对象才行,第二个回调参数可以立即获取到修改后的state值,而且setState修改数据是批量异步更新的
  • 组件优化可以用React.PureComponent代替原有的React.Component,主要是替代原有的shouldComponentUpdate钩子做了一层浅比较,会帮你做一些优化,不必要的重复渲染,shouldComponentUpdate钩子默认返回true,当返回false时,不会渲染组件
  • 跨组件通信,主要利用内置的APIReact.createContext实现跨组件通信,有点类似vue中的provide/inject功能
  • 本文示例code example[4]



相关文章
|
3月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
254 2
|
2月前
|
前端开发 UED 开发者
React 数据表格分页实现
本文详细介绍了如何在React中实现数据表格的分页功能,包括基础实现、常见问题及解决方案。通过状态管理和事件处理,我们可以有效地减少页面加载时间,提升用户体验。文章提供了完整的代码示例,帮助开发者解决分页按钮样式、按钮过多和初始加载慢等问题,并给出了相应的优化方案。
110 53
|
2月前
|
前端开发 搜索推荐 测试技术
React 数据表格排序与过滤
本文介绍了如何在 React 中实现数据表格的排序和过滤功能,从基础概念到实际代码实现,涵盖排序和过滤的基本原理、实现步骤、常见问题及解决方法。通过合理管理状态、优化性能和避免常见错误,帮助开发者提高用户体验和开发效率。
53 4
|
4月前
|
前端开发 JavaScript UED
react或者vue更改用户所属组,将页面所有数据进行替换(解决问题思路)____一个按钮使得页面所有接口重新请求
在React或Vue中,若需在更改用户所属组后更新页面所有数据但不刷新整个页面,可以通过改变路由出口的key值来实现。在用户切换组成功后,更新key值,这会触发React或Vue重新渲染路由出口下的所有组件,从而请求新的数据。这种方法避免了使用`window.location.reload()`导致的页面闪烁,提供了更流畅的用户体验。
60 1
react或者vue更改用户所属组,将页面所有数据进行替换(解决问题思路)____一个按钮使得页面所有接口重新请求
|
4月前
|
前端开发
react中使用Modal.confirm数据不更新的问题解决
文章讨论了在React中使用Ant Design的`Modal.confirm`时更新数据不生效的问题,并提供了解决方案。原因是React状态更新可能是异步的,导致Modal的内容更新后不会立即反映在UI上。解决办法是在状态更新后使用`useEffect`钩子来调用Modal实例的`update`方法,从而更新Modal的内容。
131 0
react中使用Modal.confirm数据不更新的问题解决
|
5月前
|
存储 前端开发 JavaScript
|
4月前
|
前端开发
React使用hooks遇到的坑_state中的某几个属性数据变成了空字符
本文讨论了在React使用hooks时遇到的一个问题:state中的某些属性数据变成了空字符。作者通过在修改函数中重新解构赋值来获取最新的state值,解决了因数据更新不及时导致的问题。
89 0
|
5月前
|
开发者 Java
JSF EL 表达式:乘技术潮流之风,筑简洁开发之梦,触动开发者心弦的强大语言
【8月更文挑战第31天】JavaServer Faces (JSF) 的表达式语言 (EL) 是一种强大的工具,允许开发者在 JSF 页面和后台 bean 间进行简洁高效的数据绑定。本文介绍了 JSF EL 的基本概念及使用技巧,包括访问 bean 属性和方法、数据绑定、内置对象使用、条件判断和循环等,并分享了最佳实践建议,帮助提升开发效率和代码质量。
59 0
|
5月前
|
前端开发 API 开发者
【前端数据革命】React与GraphQL协同工作:从理论到实践全面解析现代前端数据获取的新范式,开启高效开发之旅!
【8月更文挑战第31天】本文通过具体代码示例,介绍了如何利用 GraphQL 和 React 搭建高效的前端数据获取系统。GraphQL 作为一种新型数据查询语言,能精准获取所需数据、提供强大的类型系统、统一的 API 入口及实时数据订阅功能,有效解决了 RESTful API 在复杂前端应用中遇到的问题。通过集成 Apollo Client,React 应用能轻松实现数据查询与实时更新,大幅提升性能与用户体验。文章详细讲解了从安装配置到查询订阅的全过程,并分享了实践心得,适合各层次前端开发者学习参考。
47 0
|
5月前
|
存储 前端开发 JavaScript