从0开始学react,精读最新版react官方文档-Adding Interactivity

570 阅读16分钟

这几天无意间看见一个新版react官方文档,进去看了看,文档还未完成,主要以介绍hooks为主,看了一章之后,感觉讲的还是很不错的,比老版的文档要条理清晰,还是很有收获的。下面就把自己看的这章的收获与总结分享给大家。

要给大家分享的这章的标题是Add Interactivity,它下面包含了几小节,分别是:

  • Responding to Events
  • State: A component's Memory
  • Render and Commit
  • State as a Snapshot
  • Queueing a Series of State Updates
  • Updating Objects in State
  • Updating Arrays in State

一. Responding to Events

这一节主要讲的是组件的事件,事件冒泡与事件捕获。

1. 定义事件处理函数

我们常见的事件定义是定义在函数体中,然后在JSX中当作一个属性传递给组件,但这里有一点需要注意:

正确错误错误原因
<button onClick={handleClick} > <button onClick={handleClick()} >这样定义,事件会在渲染时自动执行,而不是点击按钮时执行,因为JSX的{}中给函数加上(),函数是会立即执行。

我们除了可以把事件定义在函数体中,也可以定义在JSX中,推荐使用剪头函数定义一个匿名函数

正确错误错误原因
<button onClick={() => alert('...')} ><button onClick={alert('...')} >这个函数也会渲染的时候立即执行

2. 事件传播

事件处理器会捕获来自子组件的事件,这种行为是事件的'传播或冒泡',它开始于事件定义的地方,然后向上传递。 如下这段代码,点击每个按钮,触发完当前事件之后,都会触发父级div的事件click

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('You clicked on the toolbar!');
    }}>
      <button onClick={() => alert('Playing!')}>
        Play Movie
      </button>
      <button onClick={() => alert('Uploading!')}>
        Upload Image
      </button>
    </div>
  );
}

All events propagate in React except onScroll, which only works on the JSX tag you attach it to.在react中除onScroll外的所有事件,都有向上冒泡的特性,onScroll事件只触发在你定义的jsx中

阻止冒泡

function Button({ onClick, children }) {
  return (
    <button onClick={e => {
      e.stopPropagation();
      // 这种方式的好处是:可以在这里添加自己的一些额外逻辑,并且可以跟踪整个代码链
      onClick();
    }}>
      {children}
    </button>
  );
}

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('You clicked on the toolbar!');
    }}>
      <Button onClick={() => alert('Playing!')}>
        Play Movie
      </Button>
      <Button onClick={() => alert('Uploading!')}>
        Upload Image
      </Button>
    </div>
  );
}

事件捕获

即使在子组件阻止冒泡,有时候父组件想捕获每个子组件的事件点击情况,这时就要用到事件捕获,例如:

<div onClickCapture={() => { /* this runs first */ }}>
  <button onClick={e => e.stopPropagation()} />
  <button onClick={e => e.stopPropagation()} />
</div>

实现方法就是在父组件的事件名称末尾加上'Capture',这个例子中,每个事件都分为三个传播阶段

  1. 点击按钮时,先向下传播,会执行父组件的onClickCapture事件
  2. 然后执行每个子元素的onClick事件
  3. 然后会向上传播,执行onClick事件,这里写了阻止冒泡,这个事件不会再执行

3. 阻止默认事件

export default function Signup() {
  return (
    <form onSubmit={e => {
      e.preventDefault(); // 阻止表单默认提交
      alert('Submitting!');
    }}>
      <input />
      <button>Send</button>
    </form>
  );
}

二. State: A Component's Memory

这一章介绍了第一个hook-useState

1. 组件会记住你设置的state

思考一下const [index, setIndex] = useState(0);这段代码在组件渲染时都发生了什么?

  1. 初始化时,给index赋了默认值0,会返回 [0,setIndex],并在组件上渲染出来。
  2. 当点按钮更新setIndex(index + 1)时,index会变成1,这告诉react触发另一个渲染
  3. 第二次渲染时,react仍然会看见初始值useState(0),但发现组件内已经记住了index的最新的值是1,就会取最新的值 [1,setIndex],渲染最新的值。

2. 每个组件内的state都是独立的,私有的。

如果在同一个页面引用了一个组件多次,那么每个组件内的state是私有的,不会互相影响。

三. Render and Commit

这一章主要介绍了react是如何渲染组件的

第一步:触发渲染

有两个原因会引起组件的渲染:

  • 组件的初始化。应用程序启动时会触发程序的初始渲染,主要依靠ReactDOM.render来实现
  • 组件内的状态理新。更新组件内状态会自动触发组件的更新排队

第二步:渲染组件(渲染就是react调用你的组件)

在触发渲染之后,react会调用你的组件在屏幕上显示

  • 初始化渲染时,react会调用根组件。这时会渲染一些html的标签、dom节点,比如ul、li、div等。
  • 后续渲染,react会调用其状态更新触发渲染的组件函数。这个过程是递归的,如果一个触发渲染的组件包含另一个组件,react会渲染被包含的组件,以此类推。react会自动计算自上次渲染以来,组件内的属性发生了什么变化,但是直到下次提到交到DOM前,不会处理这些信息。

第三步:提交到DOM上

渲染组件之后,react会修改DOM

  • 初始渲染,react会用appendChild这个API,把创建的DOM节点,显示在屏幕上。
  • 后续渲染,react会使用最小的DOM操作(在渲染时计算),使DOM与最新的渲染匹配。

react仅在渲染差异的地方更改DOM节点

最终:浏览器绘制屏幕

渲染完成并更新DOM后,浏览器将重新绘制屏幕。

四. State as a Snapshot

这一章主要讲组件内的state在组件渲染时的变化。

state可能看起来像是可以读写的常规javascript变量,但是它的行为更像是一个快照。设置它,并不会更新当前组件内的state,而是触发组件的更新。

1. 设置状态来触发渲染

想象一下下面的代码渲染的过程是怎么样的?

import { useState } from 'react';

export default function Form() {
  const [isSent, setIsSent] = useState(false);
  const [message, setMessage] = useState('Hi!');
  if (isSent) {
    return <h1>Your message is on its way!</h1>
  }
  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      setIsSent(true);
      sendMessage(message);
    }}>
      <textarea
        placeholder="Message"
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
      <button type="submit">Send</button>
    </form>
  );
}

  1. 首先onSubmit执行
  2. setIsSent(true)设置isSenttrue,让当前组件更新进入一个更新队列
  3. react根据isSent的最新值来重新渲染组件

2. 渲染会及时获取快照

Rendering就意味着react正在调用你的组件,组件就是你的函数。从该函数返回的JSX就像是及时的UI快照,它里面的props、事件、变量都是使用它的状态及时计算的。

与照片或者电影不同,返回的UI快照是有交互性的。它包括事件处理函数、指定输入的响应事件等。然后react更新屏幕以匹配当前快照并连接事件处理。所以,当你按下按钮,将从jsx触发click程序。

所以,当react重新渲染时,发生了以下事情:

  1. react再次调用你的函数
  2. 你的函数返回一个新的JSX快照
  3. react更新屏幕用来匹配你返回的JSX快照

作为组件的内存,state在函数执行后并不会消失。

看下面的例子:当我点击+3这个按钮时,number会加几呢?

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>
    </>
  )
}

结果是每次点击,number只会+1

这里有两点要记住

  1. 组件会在执行完当前函数,才会执行更新
  2. 组件更新时,才会拿到最新的值

虽然这个例子执行了3次 setNumber(number + 1),但其实是执行了3次setNumber(0 + 1) ,计算出number的最新值是1,setNumber(0 + 1)会把当前的值替换掉,所以在更新之后拿到的number是1。以此类推,每次点击,都只是+1。

3.随着时间推移的状态

随着时间的变化,状态的值在当前渲染时不会更改,即使是在异步代码中

看下面的例子,点击按钮时,页面和alert分别显示几呢?

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setTimeout(() => {
          alert(number);
        }, 3000);
      }}>+5</button>
    </>
  )
}

答案是: alert显示0,页面显示5

五. Queueing a Series of State Updates

这一章主要讲state的批量更新。

一个状态的更新,将会发起一个渲染的队列。有时你想在更新之前对该值做一些操作,那么这时更新队列就是很有用的。

1. react是批量更新状态

在处理状态更新之前,react会等待当前事件处理程序中的所有代码都已运行。

比如上面的执行3次setNumber(number + 1),它并不是碰见setNumber(number + 1)就更新状态,而是所有setNumber(number + 1)都执行完毕之后,才更新状态,重新渲染。

这很像你去一个餐厅去点菜的场景,你去点菜时,肯定是把所有菜跟服务员说完,服务员都记下了,才会让厨房一起去做这些菜,而不是说点一个菜就去让厨房做一个。

这可以让你更新多个状态,也不会触发太多的重新渲染。也意味着事件处理程序和其中的代码在执行完毕之前,UI不会被更新,这种行为称为批量更新。这让你的程序渲染更新更快,从而也避免了处理仅更新了一半状态的混乱。

react不会跨多个有意义的事件(如单击事件)批量处理,而是保证每个单击事件都是单独处理的。

react只会在相对安全的情况下进行批量处理,例如:不会第一次点击按钮时禁用表单,第二次点击按钮时提交表单。

2. 在下次渲染前多次更新同一个状态

如果想在下次更新前多次修改当前状态,而不是传递下一个状态,可以使用setState(n => n + 1),给更新函数中传入一个回调函数,该函数基于队列中的上一个状态来计算下一个状态。这是一种告诉react用状态值做某事的方法,而不仅仅是替换它。

例如:

import { useState } from 'react';

export default function Counter() {
  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 => n + 1被称为更新函数,当你把这个函数传递给状态设置函数时:

  1. react将在事件处理程序中的其它代码执行完毕后,处理此函数,放入更新队列
  2. 在下次更新时,react遍历队列并提供最终更新状态

3. 如果你在替换状态后更新状态,会发生什么?

例如:

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setNumber(n => n + 1);
      }}>Increase the number</button>
    </>
  )
}

点击事件处理将会执行以下操作:

  1. setNumber(number + 5),初始化时number是0,通过setNumber(0 + 5) ,计算出放入队列的值是 5
  2. setNumber(n => n + 1) 函数放入队列

最终react将更新number为 6

4. 如果在更新状态后替换它,会发生什么?

例:

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        setNumber(n => n + 1);
        setNumber(42);
      }}>Increase the number</button>
    </>
  )
}

这个事件处理程序的执行顺序是这样的:

  1. setNumber(number + 5)通过计算后,把number = 5,放入更新队列
  2. setNumber(n => n + 1) 把n => n + 1 这个更新函数放入更新队列
  3. 把number = 42 放入更新队列

在下次更新时,react会遍历更新队列并执行:

队列更新nreturn
替换为50(未使用)5
n => n + 155 + 1 = 6
替换为426(未使用)42

所以最终,react存储number的最新值是42,输出在页面上。

你可能发现setState(x)setState(n => x)几乎一样工作,只不过这里n没有被用到

最后总结一下:

  1. 给一个状态更新函数传递函数时,将会把一个函数传入队列中
  2. 给一个状态更新函数传入替换的值时,会忽略已进入队列的内容

事件处理程序执行完毕后,会触发重新渲染。在重新渲染期间,react会处理更新队列。更新程序函数在渲染间运行,所以更新函数必须是纯函数,并且只返回结果。不要从其内部设置其他状态或者运行其它副作用。

5. 命名约定

通常命名更新函数的变量名为状态的首字母

例:

setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);

六. 更新的状态是对象

状态可以是任何的javascript数据,当然也可以是对象。但不要直接更改对象,相反,当你想要更改对象时,要建立一个新对象(或该对象的副本),然后将状态设置为该新对象。

1. 什么是改变或突变?

到目前为止,我们一直使用数字、字符串和布尔值。这些值是不可变的,意思是不可更改或者只读的,你可以触发重新渲染来替换它们的值。

例如setX(5)x的值从0更新到5,但是数字0本身并没有更改,我们不能对JS内置的变量进行任何的更改。

当我给状态设置一个对象时

例如:const [position, setPosition] = useState({ x: 0, y: 0 }); 从技术上讲,可以更改对象本身的内容,这叫做改变或突变:position.x = 5

尽管react中的状态对象在技术上是可变的,但你应该将它们视为不可变的。就像是字符串或者数字一样。你应该永远替换它们,而不是改变它们。

2. 将状态视为只读

换句话说,你应该将任何处于状态的javascript对象视为只读对象。

3. 使用扩展运算符来复制对象

例如:有一个对象,每次只更新其中某个字段,可以用这种扩展运算符(...)来复制一个新的对象,并且只更改更新属性。

setPerson({
  ...person, // 复制旧字段
  firstName: e.target.value // 重新要改变的字段
});

但是...运算符只是浅复制,只能复制一层的对象属性,如果对象有多层属性,就要使用多次。

用一个事件处理程序,来更改多个表单项

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleChange(e) {
    setPerson({
      ...person,
      [e.target.name]: e.target.value // es6的计算属性来动态计算当前值
    });
  }

  return (
    <>
      <label>
        First name:
        <input
          name="firstName"
          value={person.firstName}
          onChange={handleChange}
        />
      </label>
      <label>
        Last name:
        <input
          name="lastName"
          value={person.lastName}
          onChange={handleChange}
        />
      </label>
      <label>
        Email:
        <input
          name="email"
          value={person.email}
          onChange={handleChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

4. 更新一个嵌套的对象

例如这样一个嵌套对象,我只想修改 person.artwork.city = 'New Delhi'

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

可以这样来实现

setPerson({
  ...person, // Copy other fields
  artwork: { // but replace the artwork
    ...person.artwork, // with the same one
    city: 'New Delhi' // but in New Delhi!
  }
});

其实嵌套这个词并不准备,因为在代码执行的时候,并没有嵌套对象,其实是两个不同的对象

例如:

let obj1 = {
  title: 'Blue Nana',
  city: 'Hamburg',
  image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};

let obj2 = {
  name: 'Niki de Saint Phalle',
  artwork: obj1
};

let obj3 = {
  name: 'Copycat',
  artwork: obj1
};

在这里,如果你修改了obj3.artwork.city,这将影响到obj2.artwork.cityobj1.city。这是因为

obj3.artwork, obj2.artworkobj1 是一个相同的对象。如果你以嵌套对象来理解的话,就很难体会到这一点。

七. 更新的状态是数组

数组是另一个可以存储在状态中的可变对象,也应该视为不可变的。与对象一样,当要更新存储在状态中的数组时,可以创建一个新数组或者复制一个数组,然后将状态设置为新数组。

1. 更新数组时,不要改变它

不要用arr[0] = 'bird' 或者用 pop()、push() 来变更数组

avoid (mutates the array)prefer (returns a new array)
addingpushunshiftconcat[...arr] spread syntax (example)
removingpopshiftsplicefilterslice (example)
replacingsplicearr[i] = ... assignmentmap (example)
sortingreversesortcopy the array first (example)

2. 给数组添加值

import { useState } from 'react';

let nextId = 0;

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState([]);

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={() => {
        setName('');
        setArtists(
              [ // 新建一个数组
                ...artists, // that contains all the old items
                { id: nextId++, name: name } // and one new item at the end
              ]
        );
      }}>Add</button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

数组展开运算符,还可以让你把项目放置在原始位置之前来设置默认值

setArtists([
  { id: nextId++, name: name },
  ...artists // Put old items at the end
]);

这样就可以模仿在数组结尾添加一个数据(push())和在数组开头添加一个数据(unshift()

3. 从数组中删除一个值

从数组中删除一个元素最简单的方法是filter(),换句话说,你将生成一个不包含要删除值的新数组

4. 变换数组

如果要更改数组的某些或所有项,可以使用map()来生成一个新数组

5. 替换数组中的项

通常情况要替换数据中某个值,例如arr[0] = 'bird'这样,可以用map()来实现

6. 插入到数组

有时想要在数组中间插入一个值,可以使用...扩展运算符配合slice()来实现,先用slice()把要插入值之前的数组复制,然后插入值,再用slice()把插入值之后的数组复制,在拼装成一个数组。

import { useState } from 'react';

let nextId = 3;
const initialArtists = [
  { id: 0, name: 'Marta Colvin Andrade' },
  { id: 1, name: 'Lamidi Olonade Fakeye'},
  { id: 2, name: 'Louise Nevelson'},
];

export default function List() {
  const [name, setName] = useState('');
  const [artists, setArtists] = useState(
    initialArtists
  );

  function handleClick() {
    const insertAt = 1; // Could be any index
    const nextArtists = [
      // Items before the insertion point:
      ...artists.slice(0, insertAt),
      // New item:
      { id: nextId++, name: name },
      // Items after the insertion point:
      ...artists.slice(insertAt)
    ];
    setArtists(nextArtists);
    setName('');
  }

  return (
    <>
      <h1>Inspiring sculptors:</h1>
      <input
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <button onClick={handleClick}>
        Insert
      </button>
      <ul>
        {artists.map(artist => (
          <li key={artist.id}>{artist.name}</li>
        ))}
      </ul>
    </>
  );
}

7. 对数组进行其它的更改

有些操作不能直接使用扩展运算符和map(),比如你想要实现反转和排序。你可以先把数组复制一份,再对他们进行更改。

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies' },
  { id: 1, title: 'Lunar Landscape' },
  { id: 2, title: 'Terracotta Army' },
];

export default function List() {
  const [list, setList] = useState(initialList);

  function handleClick() {
    const nextList = [...list];
    nextList.reverse();
    setList(nextList);
  }

  return (
    <>
      <button onClick={handleClick}>
        Reverse
      </button>
      <ul>
        {list.map(artwork => (
          <li key={artwork.id}>{artwork.title}</li>
        ))}
      </ul>
    </>
  );
}

8. 更新数组内对象

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [myList, setMyList] = useState(initialList);
  const [yourList, setYourList] = useState(
    initialList
  );

  function handleToggleMyList(artworkId, nextSeen) {
    setMyList(myList.map(artwork => { // 用map返回一个新的数组
      if (artwork.id === artworkId) {
        // 要改变的对象,返回一个新对象
        return { ...artwork, seen: nextSeen };
      } else {
        // 没有变化的,默认返回原始对象
        return artwork;
      }
    }));
  }

  function handleToggleYourList(artworkId, nextSeen) {
    setYourList(yourList.map(artwork => {
      if (artwork.id === artworkId) {
        // Create a *new* object with changes
        return { ...artwork, seen: nextSeen };
      } else {
        // No changes
        return artwork;
      }
    }));
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>Your list of art to see:</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

好了,以上就这一章的内容,我个人感觉还是很有收获的,官方给出了一些功能具体的实现方法。文档是英文的,以上只的自己的翻译和理解,如有不正确的地方,还望大家指正。