React基础学习笔记

120 阅读15分钟

React元素

React元素是创建一个开销极小的普通对象。ReactDOM会负责更新DOM来与React元素保持一致。

<div id="root"></div>

上述代码描述为“根节点”,因为该节点内的所有内容都将由React DOM管理。 仅使用React构建的应用通常只有单一的根DOM节点。

const root = ReactDOM.createRoot(
    document.getElementById('root')
);
const element = <h1>hello, react</h1>;

root.render(element)

React元素是不可变对象。一旦被创建,就无法更改它的子元素或者属性。一个元素就像电影的单帧:它代表了某个特定时刻的UI。

React DOM会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使DOM达到预期的状态。

组件 && props

什么是组件?

组件,从概念上类似于JavaScript函数。它接受任意的入参(即“props”),并返回用于描述页面展示内容的React元素。

  1. 函数组件
function RenderText(props) {
    return <h1>hello,{props.name}</h1>
}

该函数组件是一个有效的React组件,因为它接受唯一带有数据的“props”(代表属性)对象并返回一个React元素。

  1. class组件
class RenderText extends React.Component {
    constructor(props){
        super(props)
    }
    render(){
        return <h1>hello,{this.props.name}</h1>
    }
}

元素与组件

当React元素为用户自定义组件时,它会将jsx所接受的属性(attributes)以及子组件(children)转换为单个对象传递给组件,这个对象被称为“props”。

function RenderText(props) {
    return <h1>hello,{props.name}</h1>
}
const root = ReactDOM.createRoot(document.getElementById('root'));
const element = <RenderText name="ccc"/>
root.render(element)
流程概述
  1. 我们调用root.render()函数,并传入<RenderText name="ccc"/>作为参数。
  2. React调用RenderText组件,并将{name: 'ccc'}作为props传入。
  3. RednerText组件将<h1>hello,ccc</h1>元素作为返回值,
  4. React DOM将DOM高效地更新为<h1>hello,ccc</h1>

组件名称必须一大写字母开头 React会将以小写字母开头的组件视为原生DOM标签。例如

代表html的div标签,而则代表一个组件,并且需要在作用域内使用RenderText

props的只读性

组件无论是使用函数声明还是通过class声明,都决不能修改自身的props。(类似JavaScript纯函数思想)。
所有React组件都必须像纯函数一样保护它们的props不被更改。

State && 声明周期

State与props类似,但是state是私有的,并完全受控于当前组件。

向class组件中添加局部的state

class Clock extends React.Component {
    constructor(props){
        super(props)
        this.state = {date:new Date(),timerID:null}
    }
    componentDidMount(){
        this.timerID = setInterval(()=>this.tick(),1000)
    }
    componentWillUnmount(){
        clearInterval(this.timerID)
    }
    tick(){
        this.setState({
            date: new Date()
        })
    }
    render(){
        return (
            <div>
                <h1>hello,world</h1>
                <h2>It is {this.state.date.toLocaleTimeString()}</h2>
            </div>
        )
    }
}
const root = document.getElementById('root')
root.render(<Clock/>)
流程概述
  1. <Clock/>被传给root.render的时候,React会调用Clock组件的构造函数。因为Clock需要显示当前的时间,所以他会用一个包含当前时间的对象来初始化this.state。并且会在之后更新state。
  2. 之后React会调用组件的render方法。这就是React确定该在页面上展示什么的方式。然后React更新DOM来匹配Clock渲染的输出。
  3. Clock的输出被插入到DOM中后,React就会调用componentDidMount生命周期方法。在这个方法中,Clock组件向浏览器请求设置一个计时器来每一秒调用一次组件的tick方法。
  4. 浏览器每一秒都会调用一次tick方法。在这个方法中,Clock组件会通过调用setState来计划进行一次UI更新。因为setState的调用,React能够知道state已经改变了,然后会重新调用Clock组件的render方法来确定页面上该显示什么。这一次,render方法中的this.state.date就不一样了,如此就会渲染输出更新过的时间。React也会相应的更新DOM。
  5. 一旦Clock组件从DOM中被移除,React就会调用componentWillUnmount生命周期方法,这样计时器就停止了。

常用的生命周期

  1. componentDidMount() 会在组件已经被渲染到DOM中后运行。
  2. componentWillUnmount() 组件从DOM中被移除调用。

正确使用state

  1. 不要直接修改state,state一切变动都需要this.setState方法实现
  2. state的更新可能是异步的 处于性能考虑,React可能会把多个setState调用合并成一个调用。
    因为this.propsthis.state可能会异步更新(ajax请求等),所以不要依赖他们的值来更新下一个状态。 可以让setState接收一个函数而不是一个对象。这个函数用上一个state作为第一个参数,将此次更新被应用时的props作为第二个参数:
    this.setState((state,props)=>({counter: state.counter + props.increment}))
    

state的更新会被合并

当调用setState的时候,React会把你提供的对象合并到当前的state。

constructor(){
    this.sate = {posts:[],comments:[]}
}
componentDidMount(){
    this.setState({
        posts: [1,2,3,4]
    })
}

这里的合并是浅合并。所以this.setState({posts})完整保留了this.state.comments,但是完全替换了this.state.posts

数据是向下流动的

不管是父组件或是子组件都无法知道某个组件是有状态还是无状态的,并且它们也并不关心它是函数组件还是class组件。

这就是为什么称state为局部的或是封装的原因。除了拥有并设置了它的组件,其他组件都无法访问。

但是组件可以选择把它的state作为props向下传递到它的子组件中

事件处理

  • React事件的命名采用小驼峰(camelCase),而不是纯小写。
  • 使用jsx语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。

在React中,不能通过返回false的方式阻止默认行为。必须显示的调用preventDefault

function App(){
    function handleSubmit(e){
        e.preventDefault();
    }
    return (
        <form onSubmit={handleSubmit}>
            <button type="submit">提交</button>
        </form>
    )
}

在这里,e是一个合成时间。React根据W3C规范来定义这些合成时间,所以不需要担心跨浏览器的兼容性问题。React事件与原生时间不完全相同。

class组件事件this的绑定问题

1.构造函数绑定:

constructor(){
    this.handleClick = this.handleClick.bind(this)
} 
render(){
    return (
        <button onClick={this.handleClick}>点击</button>
    )
}
  1. public class fields语法:
handleClick = () => {}
render(){
    return (
        <button onClick={this.handleClick}>点击</button>
    )
}
  1. 箭头函数语法:
render(){
    return (
        <button onClick={()=>this.handleClick()}>点击</button>
     )
}

必须谨慎对待jsx回调函数中的this问题,在JavaScript中,class的方法默认不会绑定this。如果忘记绑定this.handleClick并把它传入了onClick,当你调用这个函数的时候this的值为undefined。这跟 JavaScript函数工作原理有关。 箭头语法问题在于每次渲染组件时都会创建不同的回调函数。在大多数情况下,这没什么问题,但如果回调函数作为prop传入子组件时,这些组件可能会进行额外的重新渲染。我们通常使用在构造器中绑定或使用class fields语法来避免这类性能问题。

向事件处理程序传递参数

handleClick(e,...rest){
    // e => SyncTheticBaseEvent
    // rest => ['1','2']
}
hanldeClicke(...rest){
    // e => SyncTheticBaseEvent => rest[rest.length - 1]
    // other
},
<button onClick={(e)=>this.handleClick(e,'1','2')}>点击1</button>
<button onClick={this.handleClick2.bind(this,'1','2')}>点击2</button>

条件渲染

在React中,你可以创建不同的组件来封装各种你需要的行为。然后,依据应用的不同状态,你可以只渲染对应状态下的部分内容。
React中的条件渲染和JavaScript中的一样,使用JavaScript运算符if或者条件运算符去创建元素来表现当前的状态,然后让React根据它们来更新UI。

元素变量

可以使用变量来存储元素。

function LoginButton(props) {
    return (
        <button onclick={props.onClick}>
            Login
        </button>
    )
}
function LogoutButton (props){
    return (
        <button onClick={props.onClick}>
            Logout
        </button>
    )
}
class LoginControl extends React.Component{
    constructor(props){
        super(props)
        this.handleLoginClick = this.handleLoginClick.bind(this);
        this.handleLogoutClick = this.handleLogoutClick.bind(this);
        this.state = {isLoggedIn: false}
    }
    handleLoginClick(){
        this.setState({isLoggedIn: true})
    }
    handleLogoutClick(){
        this.setState({isLoggedIn: false})
    }
    render(){
        const isLoggedIn = this.state.isLoggedIn;
        let button;
        if(isLoggedIn){
            button = <LogoutButton onClick={this.handleLogoutClick}/>
        } else {
            button = <LoginButton onClick={this.handleLoginClick}/>;
        }
        
        return (
            <div>
                {button}
            </div>
        )
        
    }
}

与运算符&&

通过花括号包裹代码,你可以在jsx中嵌入表达式。这也包括JavaScript中的逻辑与(&&)运算符。它可以很方便地进行元素的条件渲染。

function Mainbox(props) {
    const unreadMessages = props.unreadMessages;
    return (
        <div>
            <h1>Hello!</h1>
            {unreadMessages.length > 0 && <h2>You have {unreadMessage.length} unread messages.</h2>}
        </div>
    )
}
const messages = ['a','b','c']
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Mainbox unreadMessages={messages} />)

能使用与运算符的原因是在JavaScript中,true && expression总是会返回express,而false && expression总是会返回false。 因此,如果条件是true,&&右侧的元素就会被渲染,如果是false,React会忽略并跳过他。

注意,falsy表达式回使&&后面的元素被跳过,但是会返回faly表达式的值。

render(){
    const count = 0;
    return (
        <div>{count && <h1>Message:{count}</h1>}</div>
    )
}

上述代码render的返回值是<div>0</div>

三目运算符

另一种内联条件渲染的方式是使用JavaScript中的三目运算符condition ? true : false

render(){
    const isLoggedIn = this.state.isLoggedIn
    return (
        <div>
            <h1>The user is {isLoggedIn ? 'currently' : 'not'} logged in.</h1> 
            {
                isLoggedIn
                    ? <LogoutButton onClick={this.handleLogoutClick} />
                    : <LoginButton onClick={this.handleLoginClick} />
             }
        </div>
    )
}

阻止组件渲染

在极少数情况下,你可能希望隐藏组件,即使他已经被其他组件渲染。若要完成此操作,可以让render方法直接返回null,而不进行任何渲染。

function WarningBanner(props){
    if(!props.warn) {
        return null;
    }
    return (
        <div>
            Warning!
        </div>
    )
}
class Page extends React.Component{
    constructor(props){
        super(props);
        this.state = {showWarning: true}
        this.handleToggleClick = this.handleToggleClick.bind(this)
    }
    handleToggleClick(){
        this.setState(state=>({
            showWarning: !state.showWarning
        }))
    }
    render(){
        return (
            <div>
                <WarningBanner warn={this.state.showWarning}/>
                <button onClick={this.handleToggleClick}>
                    {this.state.showWarning?'hide':'show'}
                </button>
            </div>
        )
    }
}

列表 & Key

列表组件的渲染

function NumberList(props){
    const numbers = props.numbers
    const listItems = numbers.map(number=><li>{number}</li>)
    return (
        <ul>{listItems}</ul>
    )
}

当运行这段代码,会看到一个警告a key should be provided for list items,意思是当你创建一个元素时,必须包括一个特殊的key属性。 <li key={number.toString()}>number</li>

Key的作用是什么

Key帮助React识别哪些元素改变了,比如被添加或删除。
一个元素的key最好是这个元素在列表中拥有的一个独一无二的字符串。
当元素没有确定唯一字符串的时候,万不得已可以使用元素索引index作为Key。

如果列表项目的顺序可能会发生变化,不建议使用索引来作为key值,这样会导致性能变差,还可能引起组件状态的问题。

用Key提取组件

元素的key只有放在就近的数组上下文中才有意义。

function ListItem(props){
    return <li>{props.value}</li>
}
function Number(props){
    const numbers = props.numbers
    const listItems = numbers.map(number=><ListItem key={number.toString()} value={number} />)
    return (
        <ul>
            {;istItems}
        </ul>
    )
}

key值在兄弟节点直接必须是唯一

数组元素中使用的key在其兄弟节点时间应该是独一无二的。但是它们不需要是全局唯一的。当我们生成两个不同的数组时,可以使用相同的key值

const posts = [
    {id: 1, title: 'hello world', content: 'content1'},
    {id: 2, title: 'hellow world2', content: 'content2}
]
function Blog(props){
    const sidebar = (
        <ul>
            {props.posts.map(post=>(
                <li key={post.id}>
                    {post.title}
                </li>
            ))
        </ul>
    )
    const content = props.posts.map(post=>(
        <div key={post.id}>
            <h3>{post.title}</h3>
            <p>{post.content}</p>
        </div>
    ))
    return (
        <div>
            {sidebar}
            <hr/>
            {content}
        </div>
    )
}

key会传递信息给React,但不会传递给你的组件。如果你的组件中需要使用key属性的值,需要使用其他属性显示传递这个值 <Post key={post.id} id={post.id} title={post.title} />.
Post组件可以读出props.id,但是不能读出props.key

在JSX中嵌入map()

jsx允许在大括号中嵌入任何表达式。
这么做有时可以使你的代码更清晰,但有时这种风格也会被滥用。

就像在JavaScript中一样,何时需要为了可读性提取出一个变量,这取决于自己。但是map()嵌套了太多层级,那可能就是提取组件的好时机。

表单

在React里,HTML表单元素的工作方式和其他的DOM元素有些不同,这是因为表单元素通常会保持一些内部的state。例如纯HTML表单只接受一个名称。

<form>
    <label>
        名字:
        <input type="text" name="name" />
    </label>
    <input type="submit" value="提交" />
</form>

此表单具有默认的HTML表单行为,即在用户提交表单收浏览器刷新页面。在React中执行相同的代码,依然有效。
使用JavaScript函数可以很方便的处理表单的提交,同时还可以访问用户填写的表单数据。实现这种效果的标准方式是使用 “受控组件”

受控组件

什么是受控组件:在HTML中,表单元素(如<input><textarea><select>)通常自己维护state,并根据用户输入进行更新。而在React中,可变状态(mutable state)通常保存在组件的state属性中,并且只能通过setState来更新。

把两个结合起来,使React的state成为“唯一数据源”。渲染表单的React组件还控制着用户输入过程中表单发生的操作。被React以这种方式控制取值的表单输入元素就叫做“受控组件”

form受控组件
class NameForm extends React.Component {
    constructor(props){
        super(props)
        this.state = {value:''}
        
        this.handleChange = this.handleChange.bind(this)
        this.handleSubmit = this.handleSubmit.bind(this)
    }
    handleChange(event){
        this.setState({value: event.target.value})
    }
    handleSubmit(event){
        alert('提交的名字:'+this.state.value)
        event.preventDefault();
    }
    render(){
        return (
            <form onSubmit={this.handleSubmit}>
                <label>
                    名字:
                    <input type="text" value={this.state.value} onChange={this.handleChange} />
                </label>
                <input type="submit" value="提交" />
            </form>
        )
    }
}

由于在表单元素上设置了value属性,因此显示的值将始终为this.state.value,这使得React的state成为唯一数据源。由于handleChange在每次按键时都会执行并更新React的state,因此显示的值将随着用户输入而更新。

对于手控组件来说,输入的值始终有React的state驱动。也可以将value传递给其他UI元素,或者通过其他事件处理函数重置。

处理select标签

//....
render(){
    return (
        <form onSubmit={this.handleSubmit}>
            <label>
                选择你喜欢的风格:
                <select value={this.state.value} onChange={this.handleChange}>
                    <option value="grapefruit">葡萄柚</option>
                    <option value="mango">芒果</option>
                </select>
            </label>
        </form>
    )
}

处理多个输入

class Reservation extends React.Compontnt {
    constructor(props){
        super(props)
        this.state = {
            isGoing: true,
            numberOfGuests: 2
        }
        this.handleInputChange = this.handleInputChange.bind(this)
    }
    handleInputChange(event){
        const target = event.target;
        const value = target.type === 'checkbox' ? target.checked : target.value;
        const name = target.name
        this.setState({
            [name]: value
        })
        // 或者 ES5
        const partialState = {};
        partialState[name] = value;
        this.setState(partialState)
    }
    render(){
        return (
            <form>
                <label>
                    参与:
                    <input 
                        name="isGoing"
                        type="checkbox"
                        onChange={this.handleInputChange}/>
                </label>
                <br/>
                <label>
                    来宾人数:
                    <input
                        name="numberOfGuests"
                        type="number"
                        value={this.state.numberOfGuests}
                        onChange={this.handleInputChange}/>
                </lable>
            </form>
        )
    }
}

React有受控组件,也会有非受控组件。
受控组件数据的唯一来源为React的state,即可以通过React来处理出具
非受控组件是使用DOM的能力处理或获取表单数据,好处是,可以减少代码量。

状态提升

什么情况下需要状态提升

通常,多个组件需要反应相同的变化数据,这时建议将共享状态提升到最近的共同父组件中去。这样的编写方式叫做状态提升。

const scaleNames = {
    c: 'Celsius',
    f: 'Fahrenheit'
}
function BoilingVerdict(props) {
    if(props.celsius >= 100) {
        return <p>水开了</p>
    }else{
        return <p>水还没开</p>
    }
}
class Temperatrue extends Component{
    constructor(props){
        super(props)
        this.handleChange = this.handleChange.bind(this)
    }
    // state={temperatrue:''}
    handleChange(e){
        // this.setState({
        //     temperatrue: e.target.value
        // })
        this.props.onTemeratrueChange(e.target.value)
    }
    render(){
        // const temperatrue = this.state.temperatrue
        const temperatrue = this.props.temperatrue
        const scale = this.props.scale
        return (
            <fieldset>
                <legend>Enter temperatrue in {scaleNames[scale]}</legend>
                <input value={temperatrue} onChange={this.handleChange} />
            </fieldset>
        )
    }
}

function toCelius(Fahrenheit){ 
    return (Fahrenheit - 32) * 5 / 9
}
 function toFahrenheit(celsius){ 
    return (celsius * 9 / 5) + 32
}
function tryConvert(temperatrue,convert){  
    const input = parseFloat(temperatrue)
    if(Number.isNaN(input)){
        return ''
    }
    const output = convert(input)
    const rounded = Math.round(output * 1000) / 1000
    return rounded.toString()
}

class Calculator extends Component {
    constructor(props) {
        super(props);
        this.handleCelisusChange = this.handleCelisusChange.bind(this)
        this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this)
    }
    state = {scale: 'c',temperatrue: ''}
    handleCelisusChange(temperatrue) {
        this.setState({scale: 'c', temperatrue})
    }
    handleFahrenheitChange(temperatrue) {
        this.setState({scale: 'f', temperatrue})
    }
    render() { 
        const {scale,temperatrue} = this.state
        const celsius = scale === 'f' ? tryConvert(temperatrue,toCelius) : temperatrue
        const Fahrenheit = scale === 'c' ? tryConvert(temperatrue,toFahrenheit): temperatrue
        return (
            <div>
                <Temperatrue scale="c" temperatrue={celsius} onTemeratrueChange={this.handleCelisusChange}/>
                <Temperatrue scale="f" temperatrue={Fahrenheit} onTemeratrueChange={this.handleFahrenheitChange} />
                <BoilingVerdict celsius={parseFloat(celsius)} />
            </div>
        )
    }
}
  • Reac会调用DOM中input的onChange方法(TemperatrueInput组件的handleChange)
  • TemperatrueInput组件中的handleChange方法会调用this.props.onTemperatrueChange方法,并传入新输入的值作为参数。
  • 期初渲染时,用于摄氏度输入的子组件TemperatrueInput中的onTemperatrueChnage方法与Calculator组件中的handleCelisusChange方法相同,而用于华氏度的子组件TemperatrueInput中的onTemperatrueChnageCalculator组件中的handleFahrenheitChnage相同。
  • 这些方法内部,Calculator组件通过使用新的输入值与当前输入框对应的温度计量单位来调用this.setState进而请求React重新渲染自己本身。
  • React调用Calculator组件的render得到组件UI呈现。温度转换在这时进行,两个输入框中的数值通过当前输入温度和其计量单位来重新计算获得。
  • React使用Calculator组件提供的新props分别调用两个TemperatureInput子组件的render方法来获取子组件的UI呈现。
  • React调用BoilingVerdict组件的render方法,并将设施温度值以组件props方式传入
  • React DOM根据输入值匹配水是否沸腾,并将结果更新至DOM。

在React应用中,任何可变数据应当只有一个相应的唯一数据源。通常state都是首先添加到需要渲染数据的组件中去。然后,如果其他组件也需要这个state,那么可以将它提升到这些组件的最近共同父组件中。应当依靠自上而下的数据流而不是尝试在不同组件间同步state。

虽然提升state方式比双向绑定方式需要编写更多的样板代码,但带来的好处是,排查和隔离bug所需的工作量将会变少。由于存在于组件中的任何state,仅有组件自己能够修改它,因此bug的排查范围被大大缩减了。

如果某些数据可以由props或state推导得出,那么它就不应该存在于state中。

组合VS继承

React的组合个人理解相当于Vue的插槽系统。

包含关系

有些组件无法提前知晓他们子组件的具体内筒。在侧边栏sidebar对话框Dialog和展现通用容器的组件中特别容易遇到这种情况。

function FancyBorder(props){
    return (
        <div className={'fancyborder-'+props.color}>{props.children}</div>
    )
}
function WelcomeDialog(){
    return (
        <FancyBorder color="blue">
            <h1>welcome</h1>
            <p>谢谢观看</p>
        </FancyBorder>
    )
}

少数情况下,可能需要在一个组件中预留位置。这时可以不使用children,而是自行约定:将所需内容传入props,并使用相应的prop(相当于Vue中的具名插槽)

function SplitPane(props){
    return (
        <div>
            <div className="left">{props.left}</div>
            <div className="right">{props.right}</div>
        </div>
    )
}
function App(){
    return (
        <SplitPane
            left={<Sidebar/>}
            right={<Container/>}
        ></SplitPane>
    )
}

特例关系

有些时候,我们会把一些组件看作是其他组件的特殊实例,比如WelcomeDialog可以说是Dialog的特殊实例。
在React中,可以通过组合来实现,通过props定制并渲染一般组件。

function Dialog(props){
    return (
        <FancyBorder color={props.color}>
            <h1 className="title">{props.title}</h1>
            <p className="messahe">{props.messahe}</p>
            {props.chidlren}
        </FancyBorder>
    )
}
function WelcomeDialog() {
    const [login,setLogin] = useState('')
    const handleChange = (e) => {
        setLogin(e.target.value)
    }
    const handleSignUp = () => {
        alert(`Welcome, ${login}`)
    }
    return (
    <Dialog      
        title="Welcome"      
        message="Thank you for visiting our spacecraft!" >
            <input type="text" value={login} onChange={handleChange}/>
            <button onClick={handleSignUp}>Sign Me Up!</button>
        </Dialog>  
    );
}

props和组合提供了清晰而安全地定制组件外观和行为的灵活方式。

组件可以接受任意props,包括基本数据类型,React元素以及函数。

问题

React DOM怎么高效的更新DOM树
stetState如何通知React,state更新了呢
React是如何把多个setState合并成一个调用的
什么是组件作用域和组件上下文
什么是React合成事件
深度解析使用索引作为Key的负面影响