React03-props 和 state 详解

一、props 组件传参

1. props 基本使用

我们在使用组件时可以向组件传递数据,在组件内可以使用 props 对象来调用传入的数据。

function Person(props) {
  return <div>
    <h3>姓名:{props.name}</h3>
    <h3>年龄:{props.age}</h3>
  </div>;
}

export default function () {
  return <Person name='zhangsan' age='20'></Person>;
}

直接在组件定义中传入 props 参数,使用组件时直接在属性列表中就可以使用了,props 即为传入的参数组成的对象。

注意:

  • props 是只读的,组件内不能修改 props。
  • 如果 props 的数据源被修改,那组件内得到的 props 数据也会随着修改,即数据驱动 DOM。

2. 设置 props 初始值

(1) 设置 defaultProps

可以给组件设置 defaultProps 属性的方式来设置 props 的初始值。

function Person(props) {
  return <div>
    <h3>姓名:{props.name}</h3>
    <h3>年龄:{props.age}</h3>
  </div>;
}
Person.defaultProps = {
  age: 25
};

export default function () {
  return <Person name='zhangsan'></Person>;
}

这里我们给 Person 组件设置了 age 参数的默认值为25,传递参数时没有传 age,页面上的 age 就为默认的25。

(2) 使用解构语法

使用 es6 的解构语法可以轻松拆分 props 并设置默认值。

function Person({ name, age = 30 }) {
  return <div>
    <h3>姓名:{name}</h3>
    <h3>年龄:{age}</h3>
  </div>;
}

export default function () {
  return <Person name='zhangsan'></Person>;
}

使用解构语法提取出 props 中的 name 和 age,并为 age 属性设置了默认值为30。

注意:使用解构语法,在获取 props 中的 name 属性时直接写 name,而不需要 props.name。

3. props.children

props.children 是我们在调用组件时,填充在组件标签中的内容。

function Person(props) {
  return (
    <div>
      <h3>hello world</h3>
      {props.children}
    </div>
  );
}

export default function () {
  return (
    <Person>
      <h3>hello web</h3>
      <h3>hello</h3>
    </Person>
  );
}

这里我们给 Person 标签内传入了一个 h3 标签,在 Person 组件中通过 props.children 获取到并渲染到页面上。

props.children 的内容如下:

可以看到, props.children 是一个由传入的标签组成的虚拟 DOM 数组。

props 的注意事项:

  • props 用于传递数据,这个数据是单向传递,从父组件传递给子组件,逆向传递是不被允许的。
  • 子组件如果想修改父组件的状态,需要父组件给子组件传递事件方法。

4. 传递事件方法

使用组件时可以以参数的形式传递事件方法给子组件,子组件可以直接使用。

function Person(props) {
  function click() {
    console.log(props.onEvent());
  }
  return (
    <div>
      <button onClick={click}>事件点击</button>
    </div>
  );
}

export default function () {
  function click() {
    return '我来自于父组件';
  }
  return (
    <Person onEvent={click}></Person>
  );
}

在这个案例中,我们传递了一个 click 方法给子组件,子组件中在自己的 click 方法中调用父组件传来的 click 方法。点击按钮时,控制台会打印出 “我来自于父组件”。 

二、state 状态管理

组件通常需要根据用户交互更改页面上显示的内容,使用普通的变量来渲染数据,在变量改变时并不会触发页面改变。这时就需要使用 state 来记录动态数据的状态,在 state 更新时进行页面渲染修改数据。

如以下计数器案例:

function Counter() {
  let count = 0;
  function add() {
    count++;
  }
  return (
    <div>
      <h3>{count}</h3>
      <button onClick={add}>+</button>
    </div>
  );
}

export default function () {
  return (
    <Counter />
  );
}

 

count 是一个普通变量,点击按钮 count 变量自增,但并不会触发页面重新渲染,因此点击按钮不会起作用。

1. useState Hook 的使用

要添加 state,需要先导入 useSate。

import { useState } from 'react';

下面是修改后的计数器应用:

function Counter() {
  const [count, setCount] = useState(0);
  function add() {
    setCount(count + 1);
  }
  return (
    <div>
      <h3>{count}</h3>
      <button onClick={add}>+</button>
    </div>
  );
}

export default function () {
  return (
    <Counter />
  );
}

在这个案例中,我们使用 const [count, setCount] = useState(0) 定义了一个状态变量 count,默认值为0。其中,count 为状态变量,setCount 为 count 的 set 方法,用于设置 count 变量新的状态值。在点击按钮时调用 setCount 方法设置 count 为 count 自增后的值。

需要注意,state 是只读的,修改它的值需要使用对应的 set 方法。

2. state 触发、渲染和提交

组件显示到页面上,必须被 react 渲染。一次更新页面分为3个过程:触发、渲染和提交。

通过 set 方法更新 state 的状态,触发渲染。每隔一段时间间隔渲染一次,在渲染时 react 会调用触发渲染的组件。渲染完成后将更新提交到页面上。

在通过 set 方法更新 state 的状态触发渲染时,变量的状态不会立即更新,而是在下一次渲染时更新。

看如下示例:

function Counter() {
  const [count, setCount] = useState(0);
  function add() {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  }
  return (
    <div>
      <h3>{count}</h3>
      <button onClick={add}>+3</button>
    </div>
  );
}

你会认为每次点击按钮计数器自增3吗?

  

答案是不会,计数器每次还是自增1。使用 setCount 方法设置 count 的值时,只是触发一次渲染,此时 count 的值还没变,直到下次渲染后 count 的值才会更新。即每一次渲染时,state 的值都是固定的。

react 会等到事件处理函数中的 所有 代码都运行完毕再处理 state 更新。 这就是为什么重新渲染只会发生在所有这些 setCount 调用之后的原因。

如果要实现在一次渲染中更新 state 的值,需要用到更新方法。

function Counter() {
  const [count, setCount] = useState(0);
  function add() {
    setCount(c => c + 1);
    setCount(c => c + 1);
    setCount(c => c + 1);
  }
  return (
    <div>
      <h3>{count}</h3>
      <button onClick={add}>+3</button>
    </div>
  );
}

  

 此时实现了想要的效果,点击一次按钮,计数器自增3。

这里的 c => c + 1 被称为更新函数。当将它传递给一个 state 的 set 函数时,react 会将此函数加入队列,以便在事件处理函数中的所有其他代码运行后进行处理。在下一次渲染期间,react 会遍历队列并更新之后的最终 state。

前面的 setCount(count + 1) 也可以写成 setCount(() => count + 1),() => count + 1 被称为替换函数,使用 count + 1 替换之前的状态,而不是通过之前的状态来更新状态。

接下来看如下案例,猜一猜点击按钮时计数器会如何变化。

function Counter() {
  const [count, setCount] = useState(0);
  function add() {
    setCount(count + 5);
    setCount(count + 2);
    setCount(c => c + 3);
  }
  return (
    <div>
      <h3>{count}</h3>
      <button onClick={add}>+3</button>
    </div>
  );
}

答案:每次点击按钮,计数器会自增5。

来看下一个案例:

function Counter() {
  const [count, setCount] = useState(0);
  function add() {
    setCount(count + 5);
    setCount(c => c + 3);
    setCount(count + 2);
  }
  return (
    <div>
      <h3>{count}</h3>
      <button onClick={add}>+</button>
    </div>
  );
}

 每次点击按钮时,计数器将自增2。

3. 更新 state 中的对象

state 中可以保存任意的 JavaScript 值,包括对象和数组。但是,不应该直接修改存放在 state 中的对象。相反,当我们想要更新一个对象时,需要创建一个新的对象(或者将其拷贝一份),然后将 state 更新为此对象。

虽然严格来说 state 中存放的对象是可变的,但我们应该像处理数字、布尔值、字符串一样将它们视为不可变的。因此我们应该替换它们的值,而不是对它们进行修改。换句话说,我们应该把所有存放在 state 中的 JavaScript 对象都视为只读的。

来看下面这个案例:

function Person() {
  const [person, setPerson] = useState({
    name: 'zhangsan',
    age: 20
  });
  function change() {
    setPerson(p => {
      p.name = 'lisi';
      p.age = 25;
      return p;
    });
  }
  return (
    <div>
      <h3>{person.name}</h3>
      <h3>{person.age}</h3>
      <button onClick={change}>按钮</button>
    </div>
  );
}

   

在这个案例中,我们展示了 zhangsan 的信息,年龄为20。我们期望在点击按钮时,改为展示 lisi 的信息,年龄为25。但点击按钮时并没有改变展示的信息,这是为什么呢?

原因在于 person.name = 'lisi' 直接修改了上次 person 状态变量的值,person 变量实际是一个引用,指向 person 对象所在的地址,调用 setPerson 方法时 react 并没有检测到 person 变量的更改,因此没有重新渲染。

要实现我们想要的效果,就需要新建一个对象来更新之前的对象:

function Person() {
  const [person, setPerson] = useState({
    name: 'zhangsan',
    age: 20
  });
  function change() {
    const newPerson = {};
    newPerson.name = 'lisi';
    newPerson.age = 25;
    setPerson(newPerson);
  }
  return (
    <div>
      <h3>姓名:{person.name}</h3>
      <h3>年龄:{person.age}</h3>
      <button onClick={change}>按钮</button>
    </div>
  );
}

这里我们新建了一个新对象 newPerson,给 newPerson 添加属性,最后使用 newPerson 更新 person 的状态,这时点击按钮就会触发页面更新。

下面给出一段万能代码:

function changeObj() {
  const newObj = { ...obj };
  // 对 newObj 进行操作
  setObj(newObj);
}

 obj 为 state 变量。注意:此方法使用展开运算符只是对 obj 进行浅拷贝,深层次不起作用。

4. 更新 state 中的数组

和对象一样,state 中的数组也不能直接修改。它虽然是可变的,但是却应该被视为不可变。同对象一样,当我们想要更新存储于 state 中的数组时,需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state。

下面是常见数组操作的参考表。当你操作 state 中的数组时,需要避免使用左列的方法,而首选右列的方法:

避免使用 (会改变原始数组)推荐使用 (会返回一个新数组)
添加元素push,unshiftconcat,[...arr] 展开语法
删除元素pop,shift,splicefilter,slice
替换元素splice,arr[i] = ... 赋值map
排序reverse,sort先将数组复制一份

来看下面的案例:

function Stydy() {
  const [arr, setArr] = useState(['c', 'c++', 'java']);
  const [value, setValue] = useState('');
  function add() {
    setArr(a => {
      arr.push(value);
      return a;
    });
  }
  return (
    <div>
      {arr.map(item => <h3>{item}</h3>)}
      <input type='text' value={value} onChange={(e) => setValue(e.target.value)}/>
      <button onClick={add}>新增</button>
    </div>
  );
}

h3 列表展示数组 arr 的元素,在输入框输入内容后,我们期望将其加入到列表中,但实际并没有实现。原因在于 push 方法改变的是原数组,设置 arr 的状态并没有用一个新数组。

修改后代码如下:

function Study() {
  const [arr, setArr] = useState(['c', 'c++', 'java']);
  const [value, setValue] = useState('');
  function add() {
    setArr([...arr, value]);
  }
  return (
    <div>
      {arr.map(item => <h3>{item}</h3>)}
      <input type='text' value={value} onChange={(e) => setValue(e.target.value)}/>
      <button onClick={add}>新增</button>
    </div>
  );
}

 

使用展开符展开原数组相当于创建一个新数组,功能已实现。

5. Immer

Immer 是一个非常流行的库,它可以让我们使用简便但可以直接修改的语法编写代码,并会帮我们处理好复制的过程。通过使用 Immer,写出的代码看起来就像是“打破了规则”而直接修改了对象和数组,上面运行失败的案例使用 Immer 都可以顺利运行。

使用 Immer:

(1) 运行 npm install use-immer 添加 Immer 依赖。

(2) 用 import { useImmer } from 'use-immer' 替换掉 import { useState } from 'react'。

import { useImmer } from 'use-immer';

使用 Immer 来重写上面两个案例:

function Person() {
  const [person, setPerson] = useImmer({
    name: 'zhangsan',
    age: 20
  });
  function change() {
    setPerson(p => {
      p.name = 'lisi';
      p.age = 25;
      return p;
    });
  }
  return (
    <div>
      <h3>{person.name}</h3>
      <h3>{person.age}</h3>
      <button onClick={change}>按钮</button>
    </div>
  );
}
function Stydy() {
  const [arr, setArr] = useImmer(['c', 'c++', 'java']);
  const [value, setValue] = useImmer('');
  function add() {
    setArr(a => {
      a.push(value);
      return a;
    });
  }
  return (
    <div>
      {arr.map(item => <h3>{item}</h3>)}
      <input type='text' value={value} onChange={(e) => setValue(e.target.value)}/>
      <button onClick={add}>新增</button>
    </div>
  );
}

运行代码后,可以看到,使用 Immer 后上述2个案例的功能都正常了。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晴雪月乔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值