开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情。
本次学习以 react 官网为学习材料。(不得不说react 新的官网做的超级棒,图文并茂,甚至还提供练习!)
有些内容我已经看过了,比如 jsx 语法,所以就不记录了。
本篇记录为这5篇的大概翻译:
- 保持纯组件 keeping components pure
- 事件处理(Responding to events)
- State: A Component's Memory
- Render and Commit
- State为快照(State as a Snapshot)
保持纯组件 keeping components pure
纯函数仅仅只做计算,在react中应该把组件当作纯函数来对待,这样可以避免原因不明的bug。
纯函数有以下2个特点:
- 只关心自己内部。它不会改变被调用之前就存在的变量或者对象
- 给它相同的参数,就会返回相同的值。(Same inputs,same outputs)
比如数学公式 y = 2x ,如果 x=2,y输出的值永远是 4。
如果用 JS 来表示,这个 double 函数就是一个纯函数。
function double(number) {
return 2 * number;
}
react就是围绕这个概念来设计的,它假定你写的每一个组件都是纯函数。意味着你写的react组件如果给定了相同的输入,就一定会输出相同的jsx。
function Recipe({ drinkers }) {
return (
<ol>
<li>Boil {drinkers} cups of milk.</li>
<li>Add {2 * drinkers} spoons of masala spices.</li>
<li>Remove from heat, and add {drinkers} spoons of tea.</li>
</ol>
);
}
export default function App() {
return (
<section>
<h1>Spiced Chai Recipe</h1>
<h2>For one</h2>
<Recipe drinkers={1} />
<h2>For a gathering</h2>
<Recipe drinkers={4} />
</section>
);
}
以下的函数就有副作用,不是一个纯函数,会导致意想不到的结果。
Cup 读取和修改了一个之前就存在的变量 guest ,意味着如果调用多次 Cup,就会得到不同的结果。
let guest = 0;
function Cup() {
// Bad: changing a preexisting variable!
guest = guest + 1;
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup />
<Cup />
<Cup />
</>
)
}
React offers a “Strict Mode” in which it calls each component’s function twice during development. By calling the component functions twice, Strict Mode helps find components that break these rules.
什么时候会导致副作用?
更新页面,开始动画,改变数据都是副作用(side effects),因为他们的发生是on the side,而不是在渲染过程中。
在react中,副作用通常伴随着事件处理(event handlers),比如点击了按钮。尽管这些事件处理是在组件内部定义的,但是他们并不是在渲染过程中执行的,所以事件处理不必是纯函数。
如果实在找不到合适的事件处理来管理副作用,还可以使用终极大招 useEffect 。 它告诉 react 稍后执行,在渲染过后再执行。但这个方法应该是你最后的一招。
回顾
-
A component must be pure, meaning:
- It minds its own business. It should not change any objects or variables that existed before rendering.
- Same inputs, same output. Given the same inputs, a component should always return the same JSX.
-
Rendering can happen at any time, so components should not depend on each others’ rendering sequence.
-
You should not mutate any of the inputs that your components use for rendering. That includes props, state, and context. To update the screen, “set” state instead of mutating preexisting objects.
-
Strive to express your component’s logic in the JSX you return. When you need to “change things”, you’ll usually want to do it in an event handler. As a last resort, you can
useEffect. -
Writing pure functions takes a bit of practice, but it unlocks the power of React’s paradigm.
事件处理(Responding to events)
学习摘要
- 编写事件监听(event handler)的多种方法
- 如何从父组件传递事件控制逻辑
- events是如何传播和如何停止传播
添加事件处理
方法一:先定义一个函数,然后当做 prop 传递给对应的 JSX tag。
export default function Button() {
function handleClick() {
alert('You clicked me!');
}
return (
<button onClick={handleClick}>
Click me
</button>
);
}
事件处理函数:
- 通常在组件内定义
- 函数名以
handle开头,以事件名结尾,如handleClick。
方法二: 也可以直接inline地写事件处理函数。
<button onClick={function handleClick() {
alert('You clicked me!');
}}>
<button onClick={function handleClick() {
alert('You clicked me!');
}}>
注意:事件处理函数只能当做prop传递,而不能被调用!
JSX 会直接执行 { 和} 里面的内容,所以第二个例子中,handleClick 在每次重新渲染的时候都会被调用,不需要任何点击!
把事件处理函数作为prop传递给子组件
通常,我们希望为子组件指定事件处理函数。以按钮为例,你可能会想要按钮在不同的地方执行不同的函数,比如一个按钮播放视频,另外按钮一个上传图片。
function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}
function PlayButton({ movieName }) {
function handlePlayClick() {
alert(`Playing ${movieName}!`);
}
return (
<Button onClick={handlePlayClick}>
Play "{movieName}"
</Button>
);
}
function UploadButton() {
return (
<Button onClick={() => alert('Uploading!')}>
Upload Image
</Button>
);
}
<button>和<div> 等内置组件仅支持onClick等浏览器事件名称。然而,当构建自己的组件时,可以以任何方式命名它们的事件处理程序属性。
按照惯例,事件处理函数名称应该以on开头,后跟大写字母。
export default function App() {
return (
<Toolbar
onPlayMovie={() => alert('Playing!')}
onUploadImage={() => alert('Uploading!')}
/>
);
}
function Toolbar({ onPlayMovie, onUploadImage }) {
return (
<div>
<Button onClick={onPlayMovie}>
Play Movie
</Button>
<Button onClick={onUploadImage}>
Upload Image
</Button>
</div>
);
}
function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}
事件传播和停止传播
这些都是js基础,我就懒得搬了。唯一值得注意的是如下:
All events propagate in React except
onScroll, which only works on the JSX tag you attach it to.
In rare cases, you might need to catch all events on child elements, even if they stopped propagation. For example, maybe you want to log every click to analytics, regardless of the propagation logic. You can do this by adding Capture at the end of the event name:
<div onClickCapture={() => { /* this runs first */ }}>
<button onClick={e => e.stopPropagation()} />
<button onClick={e => e.stopPropagation()} />
</div>
Each event propagates in three phases:
- It travels down, calling all
onClickCapturehandlers. - It runs the clicked element’s
onClickhandler. - It travels upwards, calling all
onClickhandlers.
Capture events are useful for code like routers or analytics, but you probably won’t use them in app code.
事件处理程序是否可以有副作用
当然可以!不同于render 函数,事件处理函数不必是纯函数,这是一个很好的地方来更改某些内容,例如,根据键入更改输入值,或根据按下按钮更改列表。
然而,为了更改某些信息,我们首先需要用某种方式来存储它。在react中,是使用 state 来实现的。
回顾
- You can handle events by passing a function as a prop to an element like
<button>. - Event handlers must be passed, not called!
onClick={handleClick}, notonClick={handleClick()}. - You can define an event handler function separately or inline.
- Event handlers are defined inside a component, so they can access props.
- You can declare an event handler in a parent and pass it as a prop to a child.
- You can define your own event handler props with application-specific names.
- Events propagate upwards. Call
e.stopPropagation()on the first argument to prevent that. - Events may have unwanted default browser behavior. Call
e.preventDefault()to prevent that. - Explicitly calling an event handler prop from a child handler is a good alternative to propagation.
State: A Component's Memory
摘要
- 如何使用
useState钩子函数添加state变量 useState调用后返回什么- 如何添加不止一个state变量
- 为什么state要在局部调用
State
为什么普通变量是不行的
如下一个渲染雕塑图片的组件,单击Next按钮应该会通过将索引更改为1、2等来显示下一个雕塑。但是,点击后并不起作用。
import { sculptureList } from './data.js';
export default function Gallery() {
let index = 0;
function handleClick() {
index = index + 1;
}
let sculpture = sculptureList[index];
return (
<>
<button onClick={handleClick}>
Next
</button>
<h2>
<i>{sculpture.name} </i>
by {sculpture.artist}
</h2>
<h3>
({index + 1} of {sculptureList.length})
</h3>
<img
src={sculpture.url}
alt={sculpture.alt}
/>
<p>
{sculpture.description}
</p>
</>
);
}
handleClick 事件处理函数在更新一个局部变量,但是2件事件阻止了这种变化是显现的。
- 每次渲染不会保存局部变量的变化。 当React第二次渲染这个组件,它是从0重新渲染的——并不会考虑任何局部变量的变化。
- 改变局部变量不会造成触发重新渲染。 React不会发现到需要用新数据再次渲染组件。
要用新数据更新组件,需要做两件事:
- 每次渲染期间都保存数据
- 触发React以使用新数据渲染组件(重新渲染)
useState 钩子函数就是用来做这2件事情的。
hooks介绍
在React中,useState以及任何其他以“use”开头的函数都被称为Hook。
hooks 是特殊的函数,只有在React渲染的时候才可用,hooks让我们hook into 其他React特性。
hooks 只能在组件或者自定义hook的顶级(top level)调用。不能在条件语句,循环语句,或者其他嵌套的函数的中调用。
import { useState } from 'react';
import { sculptureList } from './data.js';
export default function Gallery() {
const [index, setIndex] = useState(0);
function handleClick() {
setIndex(index + 1);
}
let sculpture = sculptureList[index];
return (
<>
<button onClick={handleClick}>
Next
</button>
<h2>
<i>{sculpture.name} </i>
by {sculpture.artist}
</h2>
<h3>
({index + 1} of {sculptureList.length})
</h3>
<img
src={sculpture.url}
alt={sculpture.alt}
/>
<p>
{sculpture.description}
</p>
</>
);
}
useState详解(Anatomy of useState)
当你调用useState 时,就是告诉 React,你想要这个组件记住某些事情。
const [index, setIndex] = useState(0)
这个例子中,你想要 React 记住 index 。useState的唯一参数是 state 变量的初始值。在本例中,索引的初始值设置为0,使用 useState(0)。
每次组件渲染时,useState都会给你一个包含两个值的数组:
- state变量(如index)
- 修改state的函数(如
setIndex),这个函数可以修改 state 变量的值,触发react重新渲染组件。
给组件设置多个state变量
可以在一个组件中拥有任意多个的 state 变量。如下的组件有2个 state 变量。
import { useState } from 'react';
import { sculptureList } from './data.js';
export default function Gallery() {
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);
function handleNextClick() {
setIndex(index + 1);
}
function handleMoreClick() {
setShowMore(!showMore);
}
let sculpture = sculptureList[index];
return (
<>
<button onClick={handleNextClick}>
Next
</button>
<h2>
<i>{sculpture.name} </i>
by {sculpture.artist}
</h2>
<h3>
({index + 1} of {sculptureList.length})
</h3>
<button onClick={handleMoreClick}>
{showMore ? 'Hide' : 'Show'} details
</button>
{showMore && <p>{sculpture.description}</p>}
<img
src={sculpture.url}
alt={sculpture.alt}
/>
</>
);
}
如果多个 state 变量的类型不相关,最好使用多个 state 变量,如本例中的 index 和 showMore。
React是如何知道返回哪个state变量
你可能已经注意到,useState 被调用时没有收到关于它所引用的state变量的任何信息。并没有传递给 useState 的“标识符”,所以它是如何知道要返回哪个 state 变量
为了使其语法简洁,hooks 依赖于同一组件每次渲染,稳定调用hook的顺序。如果你遵循上面的规则(only call Hooks at the top level)的话,hooks 每次被调用都是同样的顺序。
在内部,React为每个组件保存了 state 对(state和设置state的函数) 数组,还会记录当前state对的索引(渲染前默认是0)。每次调用useState, react 都会给你下一个state对 并增加索引。
如下的例子没有使用到react,但是可以让你了解 useState 内部工作原理。
let componentHooks = [];
let currentHookIndex = 0;
// How useState works inside React (simplified).
function useState(initialState) {
let pair = componentHooks[currentHookIndex];
if (pair) {
// This is not the first render,
// so the state pair already exists.
// Return it and prepare for next Hook call.
currentHookIndex++;
return pair;
}
// This is the first time we're rendering,
// so create a state pair and store it.
pair = [initialState, setState];
function setState(nextState) {
// When the user requests a state change,
// put the new value into the pair.
pair[0] = nextState;
updateDOM();
}
// Store the pair for future renders
// and prepare for the next Hook call.
componentHooks[currentHookIndex] = pair;
currentHookIndex++;
return pair;
}
function Gallery() {
// Each useState() call will get the next pair.
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);
function handleNextClick() {
setIndex(index + 1);
}
function handleMoreClick() {
setShowMore(!showMore);
}
let sculpture = sculptureList[index];
// This example doesn't use React, so
// return an output object instead of JSX.
return {
onNextClick: handleNextClick,
onMoreClick: handleMoreClick,
header: `${sculpture.name} by ${sculpture.artist}`,
counter: `${index + 1} of ${sculptureList.length}`,
more: `${showMore ? 'Hide' : 'Show'} details`,
description: showMore ? sculpture.description : null,
imageSrc: sculpture.url,
imageAlt: sculpture.alt
};
}
function updateDOM() {
// Reset the current Hook index
// before rendering the component.
currentHookIndex = 0;
let output = Gallery();
// Update the DOM to match the output.
// This is the part React does for you.
nextButton.onclick = output.onNextClick;
header.textContent = output.header;
moreButton.onclick = output.onMoreClick;
moreButton.textContent = output.more;
image.src = output.imageSrc;
image.alt = output.imageAlt;
if (output.description !== null) {
description.textContent = output.description;
description.style.display = '';
} else {
description.style.display = 'none';
}
}
let nextButton = document.getElementById('nextButton');
let header = document.getElementById('header');
let moreButton = document.getElementById('moreButton');
let description = document.getElementById('description');
let image = document.getElementById('image');
let sculptureList = [{
name: 'Homenaje a la Neurocirugía',
artist: 'Marta Colvin Andrade',
description: 'Although Colvin is predominantly known for abstract themes that allude to pre-Hispanic symbols, this gigantic sculpture, an homage to neurosurgery, is one of her most recognizable public art pieces.',
url: 'https://i.imgur.com/Mx7dA2Y.jpg',
alt: 'A bronze statue of two crossed hands delicately holding a human brain in their fingertips.'
}, {
name: 'Floralis Genérica',
artist: 'Eduardo Catalano',
description: 'This enormous (75 ft. or 23m) silver flower is located in Buenos Aires. It is designed to move, closing its petals in the evening or when strong winds blow and opening them in the morning.',
url: 'https://i.imgur.com/ZF6s192m.jpg',
alt: 'A gigantic metallic flower sculpture with reflective mirror-like petals and strong stamens.'
}, {
name: 'Eternal Presence',
artist: 'John Woodrow Wilson',
description: 'Wilson was known for his preoccupation with equality, social justice, as well as the essential and spiritual qualities of humankind. This massive (7ft. or 2,13m) bronze represents what he described as "a symbolic Black presence infused with a sense of universal humanity."',
url: 'https://i.imgur.com/aTtVpES.jpg',
alt: 'The sculpture depicting a human head seems ever-present and solemn. It radiates calm and serenity.'
}];
// Make UI match the initial state.
updateDOM();
State是私有且隔离的
state 是屏幕上组件实例的局部状态。换言之,如果渲染同一组件两次,每个副本都将处于完全隔离状态!更改其中一个不会影响另一个。
回顾
- Use a state variable when a component needs to “remember” some information between renders.
- State variables are declared by calling the
useStateHook. - Hooks are special functions that start with
use. They let you “hook into” React features like state. - Hooks might remind you of imports: they need to be called unconditionally. Calling Hooks, including
useState, is only valid at the top level of a component or another Hook. - The
useStateHook returns a pair of values: the current state and the function to update it. - You can have more than one state variable. Internally, React matches them up by their order.
- State is private to the component. If you render it in two places, each copy gets its own state.
最后,值得注意的是:
state 变量仅用于在组件的重新渲染之间保存信息。在单个事件处理程序中,一个常规变量就可以了。当常规变量运行良好时,不要引入state变量。
A state variable is only necessary to keep information between re-renders of a component. Within a single event handler, a regular variable will do fine. Don’t introduce state variables when a regular variable works well.
例子是这个:
import { useState } from 'react';
export default function FeedbackForm() {
const [name, setName] = useState('');
function handleClick() {
setName(prompt('What is your name?'));
alert(`Hello, ${name}!`);
}
return (
<button onClick={handleClick}>
Greet
</button>
);
}
// 这个例子应该使用常规的变量
export default function FeedbackForm() {
function handleClick() {
const name = prompt('What is your name?');
alert(`Hello, ${name}!`);
}
return (
<button onClick={handleClick}>
Greet
</button>
);
}
Render and Commit
在组件显示在屏幕上之前,它们必须由React渲染。了解此过程中的步骤将帮助你思考代码如何执行并解释其行为。
摘要
- React中的渲染(render)意味着什么
- React渲染组件的时机和原因
- 在屏幕上显示组件所涉及的步骤
- 为什么渲染不总是造成DOM更新
想象一下,你的组件是厨房里的厨师,用食材组装美味菜肴。在这个场景中,React是一个服务员,他提出客户的请求,并为他们带来订单。请求和提供UI的过程有三个步骤:
- 触发渲染(将客人的订单送到厨房)
- 渲染组件(在厨房准备订单)
- 触发变化到DOM(在表上放置订单)
第一步:触发渲染
组件渲染有两个原因:
- 这是组件的初始渲染
- 组件(或其祖先组件之一)的状态已更新。
第二步: react渲染组件
在触发渲染后,React会调用组件来确定要在屏幕上显示的内容。“渲染”是React调用组件。
- 在初始渲染时,React将调用根组件。
- 对于后续渲染,React将调用其状态更新触发渲染的函数组件。
这个过程是递归的:如果更新后的组件返回其他组件,React接下来将呈现该组件,如果该组件也返回了一些东西,它接下来将呈现那个组件,依此类推。这个过程将继续,直到不再有嵌套的组件,并且React确切地知道应该在屏幕上显示什么。
第三步:触发变化到DOM(在表上放置订单)
渲染(调用)组件后,React将修改DOM。
- 对于初始渲染,React将使用
appendChild()DOM API 将其创建的所有DOM节点放到屏幕上。 - 对于重新渲染,React将应用最少的必要操作(在渲染时计算!)以使DOM与最新的渲染输出相匹配。
React仅在渲染之间存在差异时更改DOM节点。
最后一步:浏览器绘制(Browser paint)
渲染完成并React更新DOM后,浏览器将重新绘制屏幕。尽管这个过程被称为“浏览器渲染”,但我们将其称为绘制,以避免在这些文档的其余部分中产生混淆。
回顾
-
React应用程序中的任何屏幕更新都分为三个步骤:
- 触发
- 渲染
- 提交到DOM
State为快照(State as a Snapshot)
state 可能看起来像可以读写的常规JavaScript变量。然而,state的行为更像快照。设置它不会更改您已经拥有的状态变量,而是触发重新渲染。
摘要
- when和how
state会更新 - 为什么修改
state后,state的值没有立即更新 - 事件处理函数是怎么访问
state的快照
渲染时会及时拍摄快照(Render takes a snapshot in time)
“渲染”意味着React正在调用你的组件,这是一个函数。从该函数返回的JSX就像是UI在时间上的快照。它的props、事件处理程序和局部变量都是使用渲染时的state计算的。
当React重新渲染一个组件时:
- React 重新调用你的函数
- 你的组件函数返回新的
JSX快照 - React更新页面来匹配你刚刚返回的快照
作为组件的记忆,state不像每次渲染后就消失的普通变量。当React调用组件时,它会为我们提供特定渲染的state快照。组件返回的 JSX (UI 快照) 中包含一组新的prop和事件处理函数,所有这些都是这次渲染时的state来计算的。
这里有一个小实验,告诉你它是如何工作的。在这个示例中,你可能认为单击“ + 3”按钮会使计数器增加三次,因为它调用 setNumber (number + 1)三次。
注意到每次点击按钮,number只会增加一次!
设置state(如调用setNumber)只会在下次更新状态变化!
在第一次渲染时number是0,这就是为什么在该渲染的onClick处理函数中,即使调用了setNumber(number+1),number的值仍为0的原因:
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
下面是按钮点击后,React执行的操作:
-
setNumber(number+1):number为0,因此setNumber(0+1)。- React准备在下一次渲染时将数字更改为1。
-
setNumber(number+1):number为0,因此setNumber(0+1)。- React准备在下一次渲染时将数字更改为1。
-
setNumber(number+1):number为0,因此setNumber(0+1)。- React准备在下一次渲染时将数字更改为1。
即使调用了setNumber(number+1)三次,在此渲染中,事件处理函数的number始终为0,因此三次将state设置为1。这就是为什么在事件处理函数完成后,React会重新呈现数字等于1而不是3的组件。
您还可以通过在代码中用变量实际的的值替换state来可视化这一点。由于此渲染的number状态变量为0,因此其事件处理函数如下所示:
<button onClick={() => {
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
}}>+3</button>
状态随时间的变化(state over time)
试着猜猜点击这个按钮会弹出什么:
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
alert(number);
}}>+5</button>
</>
)
}
如果使用之前的替换方法,可以猜测出弹窗会显示“0”:
setNumber(0+5);
alert(0);
但是如果在alert上加一个延时器,猜猜会弹窗0还是5?
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的state的“快照”
setNumber(0 + 5);
setTimeout(() => {
alert(0);
}, 3000);
等到alert运行的时候,react可能已经改变了state的值,但还是被设置成使用用户交互时的state快照的值。
state变量的值在渲染过程中从不更改,即使事件处理函数是异步的。在该渲染的onClick中,即使在调用setNumber(number+1)之后,number的值仍然为0。
回顾
- Setting state requests a new render.
- React stores state outside of your component, as if on a shelf.
- When you call
useState, React gives you a snapshot of the state for that render. - Variables and event handlers don’t “survive” re-renders. Every render has its own event handlers.
- Every render (and functions inside it) will always “see” the snapshot of the state that React gave to that render.
- You can mentally substitute state in event handlers, similarly to how you think about the rendered JSX.
- Event handlers created in the past have the state values from the render in which they were created.