本系列涵盖了使用 React 的全部知识,分为上、中、下三篇。此为最后一篇。前端
本系列涵盖 React v16.9,但更多的是 React 全面解析,具体 React v16.9 新特性可查看 [译]React v16.9 新特性。react
完整系列包含:git
上篇主讲 React 元素渲染,可查看:深刻解读 React 核心之元素篇。github
中篇主讲 React 元素组件,可查看:深刻解读 React 核心之组件篇。算法
下篇主讲 React Hooks,可查看:深刻解读 React 核心之 Hooks 篇。数组
Hook 是一个特殊的函数,它可让你“钩入” React 的特性。全部 Hooks 都以 use 开头。其中一些可为函数组件增长状态(如 useState
),一些可用于管理反作用(如useEffect),一些可用于缓存 memoize 函数和对象(如useCallback、 useMemo)。浏览器
React hook 函数只能用于函数组件,不能在类组件中使用它们。缓存
下面是一个基本示例:函数
const Button = () => {
let count = 0;
return (
<button>{count}</button>
);
};
ReactDOM.render(<Button />, mountNode); 复制代码
咱们给 button
组件增长一个 onClick
事件:post
const Button = () => {
let count = 0;
return (
<button onClick={() => console.log('Button clicked')}> {count} </button>
);
};
ReactDOM.render(<Button />, mountNode); 复制代码
每次咱们点击 Button
按钮时,onClick
都会调用内联箭头函数,向控制台输出 Button clicked
。
请注意:React 处理的全部与 DOM 相关的属性都必须采用驼峰式,不然,React 将会报错。同时,React 还支持使用自定义 HTML 属性,而且必须采用全小写格式。
React 中的一些 DOM 属性与它们在原生 DOM API 中的属性略有不一样。例如
onChange
事件。在原生浏览器中,当你在表单字段中单击时,一般会触发它。在 React 中,onChange
只要更改了表单字段的值就会触发。React 中的某些属性的命名与 HTML 等效的不一样。例如 React 属性
className
,它至关于class
在 HTML中使用属性。有关 React 属性和 DOM 属性之间差别的完整列表,请参阅jscomplete.com/react-attri…。
跟踪状态更新并触发虚拟树协调算法更新到真实 DOM 上,React 须要了解组件中使用的全部元素发生的任何更改。为了更有效的执行此操做,React 须要为组件中引入的每一个状态元素使用特殊的 getter
和 setter
。这就是 useState
发挥做用的地方。它定义了一个状态元素,并为它提供了一个 getter
和 setter
!
useState
是容许你在 React 函数组件中添加 state 的 Hook。
如下是咱们尝试实现的 count state 元素所需的内容:
const [count, setCount] = React.useState(0);
复制代码
React.useState()
函数返回一个包含 2 个元素的数组。咱们使用数组解构来命名,第一项是 useState
返回的第一个值 count
(getter),第二项是返回的第二个值 setCount
函数(setter)。等价于下面的代码:
var countStateVariable = useState(0); // 返回一个有两个元素的数组
var count = countStateVariable[0]; // 数组里的第一个值
var setCount = countStateVariable[1]; // 数组里的第二个值
复制代码
第一项为定义的 state 变量名称,示例中叫 count
, 可是咱们能够叫他任何名字,好比 hello
。这是一种在函数调用时保存变量的方式 —— useState
是一种新方法,它与 class 里面的 this.state
提供的功能彻底相同。通常来讲,在函数退出后变量就就会”消失”,而 state 中的变量会被 React 保留。
第二项 "function" 将在调用时更改 state
元素的值(若是须要,它将触发DOM处理)。每次 setCount
调用该函数时,React 都将从新渲染 Button
组件,该组件将刷新组件中定义的全部变量(包括 count
值)。咱们传递给 setCount
的参数将成为新的值 count
。
React.useState()
方法里面惟一的参数就是初始 state。不一样于 class 的是,它能够是字符串,数字,数组等,而不必定是对象。在示例中,咱们传了 0
做为变量的初始 state
。(若是咱们想要在 state 中存储两个不一样的变量,只需调用 useState()
两次便可。)
const Button = () => {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}> {count} </button>
);
};
ReactDOM.render(<Button />, mountNode); 复制代码
请注意咱们不对 UI 自己进行任何操做,咱们只是实现了一个 action
来改变 JavaScript 对象(在内存中)!React 负责将咱们的声明性描述转换为浏览器中的实际UI。
除了使用别名
React.useState
,还能够直接导入useState
使用:import React, { useState } from 'react'; 复制代码
将 Button
组件拆分为如下两个部分:
Button
组件为按钮元素,并有一个静态标签;Display
组件以显示计数值。新 Display
组件将是纯粹的表示组件,没有本身的状态或交互。这很正常。并不是每一个 React 组件都必须与状态或交互挂钩。
const Display = (props) => (
<pre>COUNT VALUE HERE...</pre>
);
复制代码
Display
组件的职责是简单地显示它将做为 props
接收的值。例如,pre
元素用于托管值是该职责的一部分。该应用程序中的其余组件对此没有发言权!
咱们如今有两个要渲染的元素:Button
和 Display
。咱们不能将它们直接相互渲染,以下所示:
// This will not work
ReactDOM.render(<Button /><Display />, mountNode); 复制代码
在React中,相邻的元素不能像这样呈现,由于当 JSX 被转换时,它们中的每个都被转换为函数调用。有如下几个解决方案。
方案一:数组形式
ReactDOM.render([<Button />, <Display />], mountNode); 复制代码
当您渲染的全部元素都来自动态源时,这一般是一个很好的解决方案。可是,对于咱们在这里作的状况,它并不理想。
方案二:添加父组件
为全部 React 元素添加一个共同的父组件。例如,咱们能够将它们包含在 div
元素中。
ReactDOM.render(
<div> <Button /> <Display /> </div>,
mountNode
);
复制代码
React API 支持此嵌套。事实上,你可使用 React.Fragment
,它不会引入任何新的 DOM 父节点。
方案三:使用 React.Fragment
ReactDOM.render(
<React.Fragment> <Button /> <Display /> </React.Fragment>, mountNode ); 复制代码
方案三+:使用 <></>
你也能够这样写:
ReactDOM.render(
<> <Button /> <Display /> </>, mountNode ); 复制代码
React 会将空标记转换为 React.Fragment
语法。
可是,咱们应该为 React 建立单一根节点,而不是咱们刚刚执行的嵌套树。
让咱们建立一个顶层组件来托管 Button
和 Display
组件。如今的问题是:咱们应该为这个新的父组件命名什么?
命名组件及其
state
/props
元素是一项很是艰巨的任务,会影响这些组件的工做和执行方式。正确的名称将迫使你作出正确的设计决策。花些时间考虑一下你为 React 应用程序引入的每一个新名称。
因为这种新的父组件包含一个 Display
显示计数值,一个 Button
增长计数值,咱们能够把它命名为 CountManager
。
const CountManager = () => {
return (
<> <Button /> <Display /> </> ); }; ReactDOM.render(<CountManager />, mountNode); 复制代码
因为咱们将在新 Display
组件中显示计数值,所以咱们再也不须要将计数值显示在按钮上。相反,咱们能够在按钮上显示 “+1”。
const Button = () => {
return (
<button onClick={() => console.log('TODO: Increment counter')}> +1 </button>
);
};
复制代码
请注意,我还从 Button
组件中删除了 state
元素,由于咱们不能再使用它了。根据新要求,组件 Button
和Display
组件都须要访问 count
state 元素。
当组件须要访问其兄弟组件所拥有的状态元素时,一种解决方案是将该状态元素“提高”到其最近共同父组件上。对于这种状况,父级是 CountManager
。
经过将状态移动到 CountManager
,咱们如今可使用组件 props
将数据从父级传到子级。
const Display = ({ content }) => (
<pre>{content}</pre>
);
const CountManager = () => {
const [count, setCount] = useState(0);
return (
<>
<Button />
<Display content={count} />
</>
);
};
ReactDOM.render(<CountManager />, mountNode);
复制代码
经过 props
将 count
值传递给 Display
组件时,注意我使用了不一样的名称(content
)。这很正常。你没必要使用彻底相同的名称。事实上,在某些状况下,引入新的通用名称对于子组件更好,由于它使状态可重用更高。
因为 count
state元素如今位于 CountManager
组件中,所以咱们须要在 CountManager
中处理更新它。咱们来命名更新函数为 incrementCounter
。该函数的逻辑实际上与咱们以前在组件中的 handleClick
函数中使用的逻辑相同。新 incrementCounter
函数将更新 CountManager
组件 count
状态:
const CountManager = () => {
// ....
const incrementCounter = () => {
setCount(count + 1);
}
// ...
};
复制代码
为了使 Button
组件可以调用组件中的 incrementCounter
函数,CountManager
将 incrementCounter
做为 props
参数给 Button
组件。
咱们命名这个 props
为 clickAction
,值为 incrementCounter
,它是咱们在 CountManager
组件中定义的函数的引用。
const Button = ({ clickAction }) => {
return (
<button onClick={clickAction}> +1 </button>
);
};
// ...
const CountManager = () => {
// ...
return (
<div>
<Button clickAction={incrementCounter} />
<Display content={count} />
</div>
);
};
复制代码
此 clickAction
属性容许 Button
组件调用 CountManager
组件的incrementCounter
功能。
咱们分析代码就会知道, Button
组件是不知道单击它时会发生什么。它只遵循父级定义的规则并调用泛型 clickAction
。父组件控制该行为的执行内容。这遵循责任隔离的概念。这里的每一个组件都有必定的责任,它们专一于此。
再看一下 Display
。从它的角度来看,计数值不是一个状态。它只是一个 CountManager
组件传递给它的内容。 Display
组件将始终显示该 prop
。这也是职责分离。
做为这些组件的设计者,你能够选择他们的职责级别。
咱们建立 CountManager
组件负责管理计数状态。在项目开发中,又该如何作喃?
我遵循的作法是在共享父节点中定义一个状态元素,该元素尽量接近须要访问该状态元素的全部子节点。对于像这样的小应用程序,这一般意味着顶级组件自己。在较大的应用程序中,子树能够管理本身的状态“分支”,而不是依赖于在顶级根组件上定义的全局状态元素。
顶级组件一般用于管理共享应用程序状态和操做,由于它是全部其余组件的父级。
请注意,更新顶级组件上的state元素意味着将从新呈现整个组件树(在内存中)。
到目前为止,这是此示例的完整代码:
const Button = ({ clickAction }) => {
return (
<button onClick={clickAction}> +1 </button>
);
};
const Display = ({ content }) => (
<pre>{content}</pre>
);
const CountManager = () => {
const [count, setCount] = useState(0);
const incrementCounter = () => {
setCount(count + 1);
};
return (
<div>
<Button clickAction={incrementCounter} />
<Display content={count} />
</div>
);
};
复制代码
组件都须要考虑可重用性。上栗中,Button
组件也能够重用,它可使用任何值增长 count
计数,而不只仅是 +1
。
首先 Button
在 CountManager
组件中添加更多元素,以便咱们能够测试这个新功能:
const CountManager = () => {
// ..
return (
<>
<Button clickAction={incrementCounter} /> {/* +1 */}
<Button clickAction={incrementCounter} /> {/* +5 */}
<Button clickAction={incrementCounter} /> {/* +10 */}
<Display count={count} />
</>
);
};
复制代码
Button
上面呈现的全部元素当前都有一个 +1
标签,它们将使计数增长1。咱们但愿使它们显示特定于每一个按钮的不一样标签,并使它们根据特定于每一个按钮的值执行不一样的操做。请记住,你能够将任何值做为 prop
传递给 React 元素。
例如:
在咱们完成这个练习以前,花点时间尝试本身实现它。
咱们须要作的第一件事是使组件中的 +1
标签 Button
成为可自定义的标签。
为了在 React 组件中进行自定义,咱们引入了一个新的 prop(父组件能够控制)并使组件使用其值。在咱们的例子中,咱们可让 Button
组件接收增量(1
,5
,10
),例如 clickValue
。咱们能够更改 render
方法,CountManager
将咱们想要测试的值传递给这个新的 prop
。
return (
<>
<Button clickAction={incrementCounter} clickValue={1} />
<Button clickAction={incrementCounter} clickValue={5} />
<Button clickAction={incrementCounter} clickValue={10} />
<Display content={count} />
</>
);
复制代码
到目前为止,请注意有关此代码的一些事项:
count
。该 Button
组件无需了解其 click
事件的含义。它只须要触发 click
事件时传递它。clickValue
属性的值 (clickValue={5})
。我没有在那里使用字符串 (clickValue="5")
。这是由于这里这里操做的是数字运算(每次 Button
点击时),我须要这些值为数字。若是我将它们做为字符串传递,我将不得不在执行添加操做时将它转化为数字。将数字做为字符串传递是React中的常见错误。有关更多与React相关的常见错误,请参阅此文章。
在 CountManager
组件中须要作的另外一件事是 incrementCounter
功能。为了使函数通用,咱们让它接收一个参数并使用该参数的值。例如:
incrementCounter = (incrementValue) => {
setCount(count + incrementValue);
};
复制代码
如今咱们须要作的就是让 Button
组件使用 clickValue
prop
做为其标签,并使其做为参数调用其 onClick
事件 clickValue
。
const Button = ({ clickValue, clickAction }) => {
return (
<button onClick={() => clickAction(clickValue)}> +{clickValue} </button>
);
};
复制代码
使用内联箭头函数包装 onClick prop 以使其绑定到 Button 的 clickValue
。
如今,三个按钮应以三个不一样的点击值递增:
const Button = ({ clickValue, clickAction }) => {
return (
<button onClick={() => clickAction(clickValue)}> +{clickValue} </button>
);
};
const Display = ({ content }) => (
<pre>{content}</pre>
);
const CountManager = () => {
const [count, setCount] = useState(0);
const incrementCounter = (increment) =>
setCount(count + increment);
return (
<div>
<Button clickAction={incrementCounter} clickValue={1} />
<Button clickAction={incrementCounter} clickValue={5} />
<Button clickAction={incrementCounter} clickValue={10} />
<Display content={count} />
</div>
);
}
ReactDOM.render(<CountManager />, mountNode);
复制代码
想象一下,咱们须要计算文本区域中用户类型的字符,就像 Twitter 的推文形式同样。对于每一个字符的用户类型,咱们须要使用新的字符数更新 UI 。
这是一个显示 textarea
输入元素的组件,其中包含字符数的占位符 div :
const CharacterCounter = () => {
return (
<div> <textarea cols={80} rows={10} /> <div>Count: X</div> </div> ); }; ReactDOM.render(<CharacterCounter />, mountNode); 复制代码
要在用户输入时更新计数 textarea
,咱们须要自定义用户键入时触发的事件。React 为此事件提供了 onChange
方法。咱们还须要使用 state 元素来计算字符数,并在 onChange
事件中触发其 updater 函数。
在 onChange
咱们须要提出的新事件处理程序中,咱们须要访问在 textarea
元素中键入的文本。
有两种主要方法来读取值。
document.getElementById
DOM API 来获取该元素,而后使用 element.value
调用来读取它的值textarea
元素,咱们能够经过 React ref 来获取 React 元素,而后访问值咱们也能够 onChange
直接经过事件的目标对象访问元素。每一个事件都暴露其目标,而且在目标上的 onChange
事件 textarea
是 textarea
元素。
这意味着咱们须要作的就是:
const CharacterCounter = () => {
const [count, setCount] = useState(0);
const handleChange = (event) => {
// 获取目标元素
const element = event.target;
setCount(element.value.length);
};
return (
<div> <textarea cols={80} rows={10} onChange={handleChange} /> <div>Count: {count}</div> </div> ); }; ReactDOM.render(<CharacterCounter />, mountNode); 复制代码
这是最简单的解决方案,这个解决方案的不理想之处在于咱们正在混淆问题。该 handleChange
事件具备调用 setCount
函数和计算文本长度的反作用。
咱们须要混淆这些问题的缘由是 React 不知道输入的是什么。这是一个 DOM 更新,而不是 React 更新。
咱们能够经过覆盖其值 textarea
并经过 React 将其由状态更新变成 React 更新。在 onChange
处理程序中,咱们只设置在组件状态上键入的值,而不是对字符进行计数。如下是使用此策略的解决方案的一个版本:
const CharacterCounter = () => {
const [inputValue, setInputValue] = useState('');
const handleChange = (event) => {
const element = event.target;
setInputValue(element.value);
};
return (
<div> <textarea cols={80} rows={10} value={inputValue} onChange={handleChange} /> <div>Count: {inputValue.length}</div> </div> ); }; ReactDOM.render(<CharacterCounter />, mountNode); 复制代码
虽然这里代码量更多,但它有明确的关注点分离。React 如今知道并控制输入元素状态。此模式称为 React 中的受控组件模式。
此版本也更容易扩展。若是咱们要计算用户输入的单词数量,这将成为另外一个 UI 计算值。
首次在浏览器中渲染 React 组件称为 “安装” ,将其从浏览器中删除称为“卸载”。
安装,更新和卸载组件可能须要具备“反作用”。例如,React TODOs 应用程序可能须要在浏览器页面的标题中显示活动 TODO 项目的数量。直接使用 React API 是完成不了的。你须要使用 DOM API 。一样,在渲染输入表单时,你可能但愿自动对焦文本框。这也必须使用 DOM API 完成。
反作用一般须要在 React 的渲染任务以前或以后发生。这就是为何 React 在类组件中提供“生命周期方法”以容许你在 render 方法以前或以后执行自定义操做的缘由。你能够在组件首次安装在 componentDidMount
方法中后执行操做,你也能够在组件 componentDidUpdate
方法中获取更新后执行操做,或者能够在 componentWillUnmount
方法中删除以前执行操做。
对于函数组件,使用 React.useEffect
hook 函数管理反作用,该函数有2个参数:回调函数和依赖项数组。
useEffect(() => {
// Do something after each render
// but only if dep1 or dep2 changed
}, [dep1, dep2]);
复制代码
第一次 React 呈现一个有 useEffect
调用的组件时,它将调用它的回调函数。在每一个新组件呈现以后,若是依赖项的值与以前渲染中的值不一样,则 React 将再次调用回调函数。
更新或卸载函数组件时,React 能够调用反作用“cleanup”功能。能够从
useEffect
回调函数返回该清理函数。
反作用方法对于分析应用程序中正在发生的事情以及进一步优化 React 的性能也很是方便。
想看更过系列文章,点击前往 github 博客主页
走在最后,欢迎关注:前端瓶子君,每日更新