react初步学习(二)

本文深入探讨React中的状态管理、组件生命周期、事件处理和条件渲染。介绍了如何在类组件中使用state和生命周期方法实现计时器功能,强调了正确使用state的重要性,包括不应直接修改state,state更新的异步性质以及合并状态更新。同时,讲解了React事件处理的特性和注意事项,以及如何进行条件渲染,展示了多种条件渲染的技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、状态和生命周期

  状态state, 生命周期 liftcircle.
  之前说过,一旦元素被渲染了之后就不可改变了,但我们可以通过重新渲染的方法使页面得以刷新,同样我们提到过最常用的方法是编写一个可复用的具有状态的组件,这里的状态,就是我们将要说的 state

  我们对上述提过的计时器tick 中的计时功能封装成一个函数式组件如下:

function Clock(props) {
    return (
        <div>
            <h1>Hello, world!</h1>
            <h2>It is {props.date.toLocaleTimeString()}.</h2>
        </div>
    )
}

  然后把他当做一个元素放入 tick 中进行渲染

function tick() {
    ReactDOM.render(
        <Clock date={new Date()} />,
        document.getElementById('root')
    )
}

setInterval(tick, 1000);

  在这个例子中,我们将计时功能代码封装成了一个独立的可复用的组件,并通过属性date的方式将参数传入,但还不能到达我们想要的结果,那就是不能再组件内部修改参数值,组件中显示的数据依旧受控于父组件中date属性传递过来的值,那如果我们把这个date属性也添加到Clock内部呢?来看下

ReactDOM.render(
    <Clock />,
    document.getElementById('root')
)

  这时父组件中只保留了对计时组件Clock的一个单纯的引用。剩下的事情全部依托以组件Clock自己去实现。要怎么实现这个需求?这里React提出了另一个数据对象,即state,它用来保存组件内部的数据,与props类似,不同的是state是组件私有的,并且由组件本身完全控制。它能实现数据在组件内部的修改和更新。怎么使用这个state?继续往下讲之前我们先拓展一个知识

  我们知道组件有两种定义方式,即函数式组件和类组件,虽然函数式组件更加简洁更加接近原生 javascript,但类组件却拥有一些额外的属性,这个类组件专有特性,就是状态生命周期钩子,到这里也能清楚知道状态的关键作用,然而函数式组件没有这两个特性,因此,在需要使用到状态state情况下,我们需要将函数式组件转换成类组件

函数式组件转化成类组件

  尝试把一个函数式组件转化成类组件,官网给出了以下步骤,以Clock组件为例

  1. 创建一个继承自 React.Component 类的 ES6 class 同名类
  2. 添加一个名为 render() 的空方法
  3. 把原函数中的所有内容移至 render()
  4. render() 方法中使用 this.props 替代 props
  5. 删除保留的空函数声明

class Clock extents React.Component {
    render() {
        return (
            <div>
                <h1>Hello, world</h1>
                <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
            </div>
        )
    }
}

  到此,Clock 组件已经成功被我们修改成了一个类组件,我们便可以在其中添加本地状态state和生命周期钩子

class Clock extends React.Component {
    // 用类构造函数constructor初始化 this.state
    constructor(props) {
        // 使用super()将props传递给基础构造函数
        super(props);
        this.state = {date: new Date()};
    }

    render() {
        return (
            <div>
                <h1>Hello, world</h1>
                <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
            </div>
        )
    }
}

  这样,我们的类组件Clock 就拥有了自己的属性 this.state.date,也就不需要引用组件向其传递值了,因此,我么可以把组件引用中的date属性删掉,最终,我们将其渲染到DOM上,只使用组件引用,其他都交给组件Clock自己去实现

ReactDOM.render(
    <Clock />,
    document.getElementById('root')
)

  到这里就结束了?细心的你会发现,组件Clock只是实现了当前时间的显示,而我们要改装的功能是一个计时器,计时功能去哪里了?没实现啊?我们需要在组件Clock中找到一个合适的时机去实现这个功能,为此,React团队引入了 声明周期方法,也叫生命周期钩子

在类组件中添加生命周期方法

  在一个具有许多组件的应用程序中,在组件被销毁时释放所占用的资源是非常重要的。就像浏览器的垃圾回收机制,近期内不需要再用的资源,应该及时清除。

  当 Clock 第一次渲染到DOM时,我们要设置一个定时器 。 这在 React 中称为 “挂载(mounting)” 。它有一个生命钩子componentDidMount()

Clock 产生的 DOM 被销毁时,我们也想清除该计时器。 这在 React 中称为 “卸载(unmounting)” 。它的生命钩子是componentWillUnmount()

  我们的计时器是在页面加载之后,页面生成初始化状态,然后由计时器去触发状态的刷新,因此,在挂载完成是去设置计时器是个非常不错的选择

componentDidMount() {
    this.timerID = setInterval(
        () => this.tick(), 1000
    )
}

  这样我们就实现了组件计时功能,或许你注意到了,在该例中,我们把timerID存放在this中而不是this.state中。

  其实,this.propsthis.state也是数据对象与普通对象一样用来存放数据,只是他们被React团队赋予了新的职能, this.props由React本身设定,用来存放在组件引用时的属性键值对对象集,不允许Coder们自己去修改;而this.state也具有特殊的含义,即存放组件本身的、用于视觉输出的数据,但也不是说在编写React程序的时候就必须用用这两个,我们依然可以自己定义普通的数据结构。

  既然state是用于存放组件视觉输出的数据,那在render()方法中没有被引用的,就不应该出现在state中了。

  养成良好的编码习惯,编写好计时器时,及时的编写卸载事件。卸载时我们清除的数据也是从this中拿的。

componentWillUnmount() {
    clearInterval(this.timerID);
}

  挂载时我们声明了一个tick()方法,接下来我们就要实现这个方法,是用来触发UI更新。嗯哼?UI更新?我们的页面状态state不是已经更新了吗?为啥还要UI更新?

  这里有一个非常重要的方法:setState()。我们先把代码补充完整再说明

componentDidMount() {
    // ...
}

tick() {
    this.setState({
        date: new Date()
    })
}

componentWillUnmount() {
    // ...
}

setState()是React触发页面更新的第二个办法,第一个办法开篇即说过,即render()方法。setState作用就是通知React检查带状态的组件中是否含有脏值。此时react会生成一个虚拟DOM与之前的版本进行对比,只有有必要更新时才会更新。关于 state 与 setState过程 在我的另一篇文章中有详细说明,有兴趣的可以翻过去看看。

  为什么不把tick()方法写到componentDidMount()中?因为tick()只是一个普通方法,他不需要在生命周期中触发,也不用自动触发。只要谁调用了触发即可。因此不需要也不能放在生命周期钩子函数中。

  现在这个时钟每秒都会走了。整理一下,我们整个计时器代码如下:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  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>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

整个流程的执行过程是这样的:

  1. <Clock /> 被传入ReactDOM.render() 时, React 会调用 Clock组件的构造函数。 因为 Clock 要显示的是当前时间,所以它将使用包含当前时间的对象来初始化 this.state。我们稍后会更新此状态。

  2. 然后 React 调用了 Clock 组件的 render() 方法。 React 从该方法返回内容中得到要显示在屏幕上的内容。然后,React 然后更新 DOM 以匹配 Clock 的渲染输出。

  3. Clock 输出被插入到 DOM 中时,React 调用 componentDidMount() 生命周期钩子。在该方法中,Clock 组件请求浏览器设置一个定时器来一次调用 tick()

  4. 浏览器会每隔一秒调用一次 tick()方法。在该方法中, Clock 组件通过 setState() 方法并传递一个包含当前时间的对象来安排一个 UI 的更新。通过 setState(), React 得知了组件 state(状态)的变化, 随即再次调用 render() 方法,获取了当前应该显示的内容。 这次,render() 方法中的 this.state.date 的值已经发生了改变, 从而,其输出的内容也随之改变。React 于是据此对 DOM 进行更新。

  5. 如果通过其他操作将 Clock 组件从 DOM 中移除了, React 会调用 componentWillUnmount() 生命周期钩子, 所以计时器也会被停止。

二、正确的使用State(状态)

  对于setState() 有三件事情是我们应该要知道的

(1)不要直接修改state
  真正触发React对比不同版本的虚拟DOM是setState() 方法,直接修改state页面不会刷新,这一点与原生javascript区别较大,需要理解。

// 这么做不会触发React更新页面
this.state.comment = 'hello';
// 使用 setState() 代替
this.setState({ comment: 'hello' });

  【注意】在组件中,唯一可以初始化分配this.state的地方就是构造函数constructor(){}

(2)state(状态)更新可能是异步的
  React为了优化性能,有可能会将多个setState() 调用合并为一次更新。这就导致 this.propsthis.state 可能是异步更新的,你不能依赖他们的值计算下一个state(状态)

// counter 计数更新会失败
this.setState({
    counter: this.state.counter this.props.increment
})

  如果我们有这种需求,可以使用以下setState()办法:

// ES6 箭头函数法
this.setState((prevState, props) => ({
    counter: prevState.counter + props.increment
}));
// 常规函数法
this.setState(function(prevState, props) {
    return {
        counter: prevState.counter + props.increment
    };
})

(3)state(状态)更新会被合并
当你调用setState(), React将合并你提供的对象到当前状态中。例如,你的状态可能包含几个独立的变量,然后你用几个独立的setState方法去调用更新,如下

constructor(props) {
    super(props);
    this.state = {
        posts: [],
        comments: []
    };
}

componentDidMount() {
    fetchPosts().then(response => {
        this.setState({
            posts: response.posts
        });
    });

    fetchComments().then(response => {
        this.setState({
            comments: response.comments
        });
    });
  }

  合并是浅合并,所以,this.setState({comments})在合并过程中不会改变this.state.posts的值,但是会完全替换this.state.comments 的值

数据向下流动

  无论是作为父组件还是子组件,它都无法或者一个组件是否有状体,同时也不需要关心另一个组件是定义为函数组件还是类组件。这就是为什么state经常被称为 本地状态封装状态 的原因, 他不能被拥有并设置它的组件以外的任何组件访问。那如果需要访问怎么处理?
(1)作为其子组件的props(属性)

// 在组件中使用
<h2>It is {this.state.date.toLocaleTimeString()}</h2>
// 传递给子组件作为props
<FormattedDate date={this.state.date} />

  虽然FormattedDate组件通过props接收了date的值,但它仍然不能获知该值是来自于Clockstate, 还是 Clockprops, 或者一个手动创建的变量.

  这种数据关系,一般称为"从上到下"或"单向"的数据流。任何state(状态)始终由某个特定组件所有,并且从该state导出的任何数据 或 UI 只能影响树"下方"的组件

  如果把组件树想像为 props(属性) 的瀑布,所有组件的 state(状态) 就如同一个额外的水源汇入主流,且只能随着主流的方向向下流动。

各组件完全独立

  借用上文的Clock组件,我们创建一个App组件,并在其中渲染三个Clock:

function App() {
    return (
        // 之前说过组件只能返回一个根节点,所以用<div>包起来
        <div>
            <Clock />
            <Clock />
            <Clock />
        </div>
    );
}
ReactDOM.render(
    <App />,
    document.getElementById('root')
);

  每个Clock都设立它自己的计时器并独立更新,如果App中有一个数据变量,也能被三个Clock相互独立修改。

  至于何时使用有状态组件,何时使用无状态组件,被认为是组件的一个实现细节,取决于你当时的需求,你可以在有状态组件中使用无状态组件,也可以在无状态组件中使用有状态组件

三、 事件处理

  通过 React 元素处理事件跟在 DOM 元素上处理事件非常相似。但是有一些语法上的区别:

  1. React 事件使用驼峰命名,而不是全部小写
  2. 通过 JSX , 传递一个函数作为事件处理程序,而不是一个字符串

// html usage
<button onclick="todo()">click me</button>
// React usage
<button onClick={todo}>click me></button>
  1. 在React中不能通过返回false来阻止默认行为。必须明确的调用preventDefault

// html usage
<a href="#" onclick="console.log('clicked'); return false">
    Click me
</a>

// React usage
class ActionLink extends React.Component {
    function handleClick(e) {
        e.preventDefault();
        console.log('clicked.');
    }
    return (
        <a href="#" onClick={handleClick}>
            Click me
        </a>
    )
}

  在这里,React团队帮Coder们实现了e事件的跨浏览器兼容问题。当使用React时,我们也不需要调用addEventListener在DOM 元素被创建后添加事件监听器。相反,只要当元素被初始渲染的时候提供一个监听器就可以了。

  当使用ES6类定义一个组件时,通常的一个事件处理程序就是类上的一个方法,看个例子,Toggle 组件渲染一个按钮,让用户在 “ON” 和 “OFF” 状态之间切换:

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // 这个绑定是必要的,使`this`在回调中起作用
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(prevState => ({
      isToggleOn: !prevState.isToggleOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

ReactDOM.render(
  <Toggle />,
  document.getElementById('root')
);

绑定类方法

  在JSX回调中你必须注意 this 的指向。 在 JavaScript 中,类方法默认没有 绑定 的。如果你忘记绑定 this.handleClick 并将其传递给onClick,那么在直接调用该函数时,this 会是 undefined

 这不是 React 特有的行为;这是 JavaScript 中的函数如何工作的一部分,可以使用属性初始值设置来正确地 绑定(bind) 回调,但这是实验性做法,不建议使用,以后有可能会废弃,如果你没有使用属性初始化语法
(1)可以在回调中使用一个 arrow functions

class LoginButton extends React.Component {
    handleClick() {
        console.log('this is: ', this)
    }

    render() {
        // 这个语法确保 `this` 被绑定在 handleClick 中
        return (
            <button onClick={(e) => this.handleClick(e)}>
                Click me
            </button>
        );
    }
}

(2)使用Function.prototype.bind 方法,相对简洁方便

<button onClick={this.handleClick.bind(this)}>
    Click me
</button>

传递参数给事件处理程序

  在循环内部,通常需要将一个额外的参数传递给事件处理程序,常用的有一下两种方案;

<button onClick={(e)  => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this.id)}>Delete Row</button>

  上面两个例子中,参数 e 作为 React 事件对象将会被作为第二个参数进行传递。通过箭头函数的方式,事件对象必须显式的进行传递,但是通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。

四、条件渲染

在 React 中,你可以创建不同的组件封装你所需要的行为。然后,只渲染它们之中的一些,取决于你的应用的状态。

整个组件的条件渲染

  React 中的条件渲染就可在JS中的条件语句一样,使用JS操作符如if或者条件控制符来创建渲染当前的元素,并且让React更新匹配的UI。比如我们有一个需求,需要判断用户是否登录,来显示不同组件

function UserGreeting(props) {
    return <h1>Welcome back!</h1>
}

function GustGrreeting(props) {
    return <h1>Please sign up.</h1>
}

function Greeting(props) {
    const isLoggedIn = props.isLoggedIn;
    if (isLoggedIn) {
        return <UserGreeting />
    } 
    return <GuestGreeting />
}

ReactDOM.render(
    <Greeting isLoggedIn={false} />,
    document.getElementById('root')
);

使用元素变量条件渲染部分内容

  你可以用变量来存储元素。这可以帮助您有条件地渲染组件的一部分,而输出的其余部分不会更改。下方两个组件用于显示登出和登入按钮

function LoginButton() {
    return(
        <button onClick={props.onClick}>Login</button>
    )
}

function LogoutButton(props) {
    return (
        <button onClick={props.onclick}>Logout</button>
    )
}

  登入登出按钮已做好,接下来需要实现有切换功能的一个有状态的组件,为了更系统化学习,我们把前面的Greeting组件一起加进来

class LoginControl extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            isLoginedIn: false
        }
    }

    handleLoginClick() {
        this.setState({   isLoggedIn: true });
    }

    handleLogoutClick() {
        this.setState({ isLoggedIn: false });
    }

    render() {
        const isLoggedIn = this.state.isLoggedIn;

        let button = null;
        if (isLoggedIn) {
            button = <LogoutButton onClick={this.handleLogoutClick.bind(this)} />
        } else {
            button = <LoginButton onclick={this.handleLoginClick.bind(this)} />
        }

        return (
            <div>
                <Greeting isLoggedIn={isLoggedIn} />{button}</div>
            </div>
        )
    }
}

reactDOM.render(
    <LoginControl />,
    document.getElementById('root')
)

  使用if是很常见的一种做法,当然也有一些更简短的语。JSX中有几种内联条件的方法,

(1)使用逻辑与&&操作符的内联if用法
  我们可以在 JSX 中嵌入任何表达式,方法是将其包裹在花括号中,同样适用于JS的逻辑与&&运算符

function Mailbox(props) {
    const unreadMessages = props.unreadMessages;
    return (
        <div>
            <h1>Hello!</h1>
            { unreadMeaasges.length > 0 &&
                <h2> You have {unreadMessages.length} unread messages.
            }
        </div>
    )
}

cosnt message = ['React', 'Re: React', 'Re:Re: React'];
ReactDOM.render(
    <Mailbox unreadMessages={messages} />,
    document.getElementById('root')
);

  该案例是可以正常运行的,因为在 JavaScript 中, true && expression 总是会评估为 expression ,而 false && expression 总是执行为 false 。并且我们可以在表达式中嵌入表达式

(2)使用条件操作符的内联If-else
  条件操作符 即三目表达式:condition ? trueExpression : falseExpression

// 条件渲染字符串
<div>The user is {isLoggedIn ? 'currently' : 'not'} logged in.</div>
// 条件渲染组件
<div>
    {isLoggedIn ? (
        <LogoutButton onClick={this.handleLogoutClick} />
    ) : (
        <LoginButton onClick={this.handleLoginClick} />
    )}
</div>

  总之,遵循一个原则,哪种方式易于阅读,就选择哪种写法。并且,但条件变得越来越复杂时,可能是提取组件的好时机。

阻止组件渲染

  在极少数情况下,您可能希望组件隐藏自身,即使它是由另一个组件渲染的。为此,返回 null 而不是其渲染输出。注意这里是不渲染,不是不显示。

  在下面的例子中,根据名为warnprops 值,呈现 <WarningBanner /> 。如果 props 值为 false ,则该组件不渲染:

function WarningBanner(props) {
    if (props.warn) { 
        return null;
    }
    
    return (
        <div className="warning">Warning</div>
    )
}

class Page extends React.Component {
    constructor(props) {
        super(props);
        this.state = { showWarning: true }
    }

    handleToggleClick() {
        this.setState(prevState => ({
            showWarning: !prevState.showWarning
        }));
    }

    render() {
        return (
            <div>
                <Warningbanner warn={this.state.showWarning} />
                <button onClick={this.handleToggleClick.bind(this)}>
                    { this.state.showWarning ?   'Hide' : 'Show'}
                 </button>
            </div>
        )
    }
}

ReactDOM.render(
    <Page />,
    document.getElementById('root')
)

  从组件的 render 方法返回 null 不会影响组件生命周期方法的触发。 例如, componentWillUpdatecomponentDidUpdate 仍将被调用。因此需要组件刚载入时就要判断执行返回null

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值