初识React(二)响应事件、state、useState

(一)响应事件

使用 React 可以在 JSX 中添加 事件处理函数。其中事件处理函数为自定义函数,它将在响应交互(如点击、悬停、表单输入框获得焦点等)时触发

1.给组件添加事件处理函数

就像vue的@click="handleClick"一样 

但这里要特别注意的是,传入的是一个函数,而不是函数的调用

// 正确写法
<button onClick={handleClick}>点我</button>
// 错误写法 会导致渲染时直接调用函数
<button onClick={handleClick()}>点我</button>

// 正确写法
<button onClick={()=>{alert('!')}}>点我</button>
// 错误写法
<button onClick={alert('!')}>点我</button>

2.将事件处理函数作为props传递

父组件的自定义函数可以通过prop传递给子组件,就像vue的@handleClick="handleClick"一样 

function Button({ handleClick, children}) {
  return (
    <button onClick={handleClick}>{children}</button>
  );
}

export default function App() {
  return (
    <div>
      <Button handleClick={() => alert('点击!')}>
        点击按钮
      </Button>
    </div>
  );
}

3.事件传播

和原生js一样,事件也会冒泡

当父组件和子组件都有点击事件时,点击子组件会先触发子组件事件,再触发父组件事件

阻止原生默认行为 

 当不想触发冒泡传播时,可以通过 e.stopPropagation() 阻止事件冒泡

function Button({ onClick, children }) {
  return (
    <button onClick={e => {
      e.stopPropagation();
      onClick();
    }}>
      {children}
    </button>
  );
}

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('你点击了 toolbar !');
    }}>
      <Button onClick={() => alert('你点击了button!')}>
        上传图片
      </Button>
    </div>
  );
}

当点击表单提交按钮,不想触发浏览器自带的提交并刷新事件,可以通过 e.preventDefault() 来阻止

export default function Signup() {
  return (
    <form onSubmit={e => {
      e.preventDefault();
      alert('提交表单!');
    }}>
      <input />
      <button>发送</button>
    </form>
  );
}

(二)State

1.介绍 

组件通常需要根据交互更改屏幕上显示的内容。输入表单应该更新输入字段,单击轮播图上的“下一个”应该更改显示的图片,单击“购买”应该将商品放入购物车。组件需要“记住”某些东西:当前输入值、当前图片、购物车。在 React 中,这种组件特有的记忆被称为 state

与vue的响应式原理不同,vue通过proxy劫持数据的getter和setter实现响应式,如vue3的reactive()

State的创建使用了 const [变量名,变量setter] = useState(初始值) 的形式定义响应式数据

State是隔离且私有的

渲染多个相同组件,其state值是独立的,不受其他组件的影响

2.渲染和提交

  • 应用启动时进行第一次渲染,届时所有组件和节点都会进行渲染;
  • 当数据改变后,react会重新渲染数据有所改变的组件,尽可能小的减少渲染量(diff算法) 
  • 提交阶段,将所有节点绘制到浏览器页面 

当使用state变量setter函数改变数据后,会引起组件的重新渲染

设置 state 只会为下一次渲染变更 state 的值 

如下代码,点击按钮后只会引起一次组件的渲染,number的值是1而不是3 

const [number, setNumber] = useState(0);
  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>
    </>
  )

因为渲染时的操作是进行了三次的 setNumber( 0 +1) ,即渲染后number=1!!!

使用异步函数,例如设置一个定时器,在其内部输出number的话,number该是多少呢?

const [number, setNumber] = useState(0);
  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setTimeout(()=>{
            alert(number);
        },3000);
      }}>+3</button>
    </>
  )

在3秒后,弹出的number值还是为0,这就意味着,state像是拍下的一张照片,在重新拍照片之前,state都不会发生改变

在下次渲染前多次更新同一个 state

那如何实现在统一渲染前就更改number的值呢?

const [number, setNumber] = useState(0);
  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(n => n + 1);
        setNumber(n => n + 1);
        setNumber(n => n + 1);
      }}>+3</button>
    </>
  )

使用箭头表达式(叫作更新函数)就可以啦,但是这个操作并不常用

n只是一个形参,用来指代number,取别的名字也是ok的

3.更新State的对象

应当将state视为只读的,不能直接更改state本身,而是将state复制出来,对复制体进行更改后再替换state进行重新渲染

直接改变本身(如position.x=100)后,并不会引起组件的重新渲染

(1)用展开语法复制对象

setPerson({
  firstName: e.target.value, // 从 input 中获取新的 first name
  lastName: person.lastName,
  email: person.email
});
// 需要更改的数据不多时 使用展开语法
setPerson({
  ...person, // 复制上一个 person 中的所有字段
  firstName: e.target.value // 但是覆盖 firstName 字段 
});

展开语法(...)本质是浅拷贝,只会复制浅层数据,当对象嵌套过多时就不太适用了

(2)更新一个嵌套对象 

const [person, setPerson] = useState({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
    image: 'https://i.imgur.com/Sd1AgUOm.jpg',
  }
});

// 使用展开语法
setPerson({
  ...person, // 复制其它字段的数据 
  artwork: { // 替换 artwork 字段 
    ...person.artwork, // 复制之前 person.artwork 中的数据
    city: 'New Delhi' // 但是将 city 的值替换为 New Delhi!
  }
});
使用Immer库实现扁平化 

Immer可以让你使用简便但可以直接修改的语法编写代码,并会帮你处理好复制的过程。通过使用 Immer,你写出的代码看起来就像是你“打破了规则”而直接修改了对象 

下载immer库:npm install use-immer

使用Immer: 

import { useImmer } from 'use-immer';

updatePerson(draft => {
  draft.artwork.city = 'Lagos';
});

Immer的作用就是使用proxy劫持person数据,将其包装为draft,当draft数据发生改变时Immer能感知到并作出对应的改变,和vue3的reactive函数原理相同

4.更新State的数组

在JS中,数组也是对象的一种,因此使用state时还是要保证只读

直接使用数组方法对数组进行修改就违背了只读规定了,如pop、shift、push等

(1)向数组添加元素

无需使用push、unshift方法,可以直接用展开运算符进行添加

setArtists( // 替换 state
  [ // 是通过传入一个新数组实现的
    ...artists, // 新数组包含原数组的所有元素
    { id: nextId++, name: name } // 并在末尾添加了一个新的元素
  ]
);

(2)从数组中删除元素

无需使用shift、pop、slice等方法,可以通过filter将需要删除的数据过滤,生成的新数组即为目标数据

setArtists(
  artists.filter(a => a.id !== artist.id)
);

(3)转换数组、替换数组元素

使用map方法,根据需求修改数组元素后返回数组即可 

const nextCounters = counters.map((c, i) => {
    if (i === index) {
    // 递增被点击的计数器数值
        return c + 1;
      } else {
        // 其余部分不发生变化
        return c;
      }
    });
setCounters(nextCounters);

(4)向数组中插入元素

通过展开运算符...和slice方法

const insertAt = 1; // 可能是任何索引
const nextArtists = [
      // 插入点之前的元素:
      ...artists.slice(0, insertAt),
      // 新的元素:
      { id: nextId++, name: name },
      // 插入点之后的元素:
      ...artists.slice(insertAt)
    ];
setArtists(nextArtists);

(5)拷贝数组

需要进行修改数组元素数据的操作时,可以先拷贝一份,对复制品进行修改再更新state

// 展开运算符
const newPerson =  [...person]
// 使用slice方法
const newPerson = person.slice()

(6)使用Immer 

不多说了,跟state对象是一样的操作,但是可以直接使用更新数组的方法了 

  • 9
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值