一、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,unshift | concat,[...arr] 展开语法 |
删除元素 | pop,shift,splice | filter,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个案例的功能都正常了。