学习的过程,就是把已经实现的功能反复地,变着花样地重构,直到找到最合适的点。
如果连这点觉悟都没有,那就不是一个合格的程序员。而雇主的本质是逐利,最忌讳的是重构,这个问题可以请高水平的工程师来得到缓解,但不可能彻底解决。
本文知识要点
- Hook
- 高阶组件
- 组件通信
- 上下文
- React.cloneElement
Hook
文档地址:https://zh-hans.reactjs.org/docs/hooks-intro.html#___gatsby
hook是16.8版本新增的特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
它具有如下特点:
- 在无需修改状态的情况下,复用状态逻辑。
- 将相关联的部分拆分为更小的函数,复杂组件将更容易理解。
- 更简洁,更易理解。
状态钩子 State Hook
函数型组件可以使用状态:
代码语言:javascript复制function Example() {
// 声明一个新的叫做 “count” 的 state 变量,
// 数组第二个值是变更函数,参数接收的是新状态
// useState参数是初始值。
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count 1)}>
Click me
</button>
</div>
);
}
你点击一次,count就加1.这样就在函数型组件中实现了state。
你可以执行更加复杂的操作:
代码语言:javascript复制// 添加水果
function AddFruit({setFruits,fruits}){
return (
<input type='text' onKeyUp={(e)=>{
if(e.keyCode==13){
setFruits(fruits.concat(e.target.value))
e.target.value='';
}
e.persist()
}}/>
)
}
export default function HooksTest() {
const [fruit, setFruit] = useState("草莓");
const [fruits, setFruits ] = useState(['草莓','苹果','鸭梨']);
return (
<div>
<p>{fruit === "" ? "请选择喜爱的水果:" : `您的选择是:${fruit}`}</p>
<AddFruit setFruits={setFruits} fruits={fruits}></AddFruit>
<FruitList setFruit={setFruit} fruits={fruits}></FruitList>
</div>
);
}
如果用以前的写法,难以想象,用这么短的代码就实现了一个购物车。
副作用钩子 Effect Hook (类似watch)
函数组件执行副作用操作。
副作用是什么鬼?它包括数据获取,设置订阅,手动更改dom等。最典型的就是异步数据获取
基本使用
代码语言:javascript复制import { useEffect } from "react";
export default function HooksTest() {
const [fruit, setFruit] = useState("草莓");
const [fruits, setFruits] = useState([]);
// 使用useEffect异步获取数据
useEffect(() => {
setTimeout(() => {
setFruits(['香蕉', '苹果'])
}, 1000);
})
return (
<div>
<p>{fruit === "" ? "请选择喜爱的水果:" : `您的选择是:${fruit}`}</p>
<AddFruit setFruits={setFruits} fruits={fruits}></AddFruit>
<FruitList setFruit={setFruit} fruits={fruits}></FruitList>
</div>
);
}
这样就实现了异步获取数据.
这个和直接settimout有什么区别呢?如果在useEffect中,会发现不断在执行(每隔一秒),如果执行点击,他会越来越快。
代码语言:javascript复制export default function HooksTest() {
const [fruit, setFruit] = useState("草莓");
const [fruits, setFruits] = useState(['草莓','香蕉']);
// 使用useEffect异步获取数据
useEffect(() => {
document.title=fruit;
},[fruit])
return (
<div>
<p>{fruit === "" ? "请选择喜爱的水果:" : `您的选择是:${fruit}`}</p>
<AddFruit setFruits={setFruits} fruits={fruits}></AddFruit>
<FruitList setFruit={setFruit} fruits={fruits}></FruitList>
</div>
);
}
清务必设置依赖选项(在何时执行),如果没有则放一个空数组。
这在设置做分页时非常管用
清除依赖:
代码语言:javascript复制useEffect(()=>{...}, [])
useEffect(() => {
const timer = setInterval(() => {
console.log('msg');
}, 1000);
return function(){
clearInterval(timer);
} }, []);
useReducer (状态管理lowb实现)
useState的可选项,常用于组件有复杂状态逻辑时,类似于redux中reducer概念。
在redux中,reducer类似vuex中的mutation,接收action,改变state。
代码语言:javascript复制import { useReducer } from "react";
// 状态维护reducer
function fruitReducer(state, action) {
switch (action.type) {
case "init":
return action.payload;
case "add":
return [...state, action.payload];
default:
return state;
}
}
// 添加水果
function AddFruit({ onAddFruit, fruits }) {
return (
<input type='text' onKeyUp={(e) => {
if (e.keyCode == 13) {
onAddFruit(e.target.value)
e.target.value = '';
}
e.persist()
}} />
)
}
export default function HooksTest() {
const [fruits, dispatch] = useReducer(fruitReducer, []);
const [fruit, setFruit] = useState("草莓");
useEffect(() => {
setTimeout(() => {
// 变更状态,提交
dispatch({ type: "init", payload: ["香蕉", "西瓜"] });
}, 1000);
}, []);
return (
<div>
{/*变更状态*/}
<AddFruit onAddFruit={pname => dispatch({ type: 'add', payload: pname })} fruits={fruits}></AddFruit>
<FruitList setFruit={setFruit} fruits={fruits}></FruitList>
</div>
);
}
实现的功能完全一样。但是一个全局的状态就实现了共享。
useContext
上面有个问题,就是AddFruit组件与父组件存在耦合。这时应该考虑解耦的问题。
useContext用于在快速在函数组件中导入上下文。把provide作为所有元素的老爹。隔代传参。
代码语言:javascript复制import React, { useContext } from "react"; // 创建上下文
const Context = React.createContext();
export default function HooksTest() {
// ...
return (
{/* 提供上下文的值 */ }
< Context.Provider value = {{ fruits, dispatch }}>
<div>
{/* 这里不再需要给FruitAdd传递变更函数,实现了解耦 */}
<AddFruit />
</div>
</Context.Provider >
); }
function AddFruit(props) {
// 获取上下文
const { dispatch } = useContext(Context)
const onAddFruit = e => {
if (e.key === "Enter") {
// 直接派发动作修改状态
dispatch({ type: "add", payload: pname }) setPname("");
}
};
// ...
}
清爽多了!
不过对于傻瓜组件,可以不考虑接耦。也不见得这种方法完全取代redux。
React表单组件设计
除了重构,还有一个重要的地方是造轮子。
antd的表单实现
import
React
from
'react'
import antd from
'antd'
const
{
Form,
Icon,
Input,
Button}
= antd;
class
NormalLoginForm
extends
React.Component
{
handleSubmit = e =>
{
e.preventDefault();
-
this.props.form.validateFields((err, values)
=>
{
-
if
(!err)
{
console.log('Received values of form: ', values);
-
}
-
});
-
};
render()
{
-
const
{ getFieldDecorator }
=
this.props.form;
-
return
(
-
<Form onSubmit={this.handleSubmit} className="login-form">
-
<Form.Item>
-
{getFieldDecorator('username',
{
rules:
[{ required:
true, message:
'Please input your username!'
}],
-
})(
-
<Input
prefix={<Icon type="user" style={{ color:
'rgba(0,0,0,.25)'
}}
/>}
placeholder="Username"
-
/>,
-
)}
-
</Form.Item>
-
<Form.Item>
-
{getFieldDecorator('password',
{
rules:
[{ required:
true, message:
'Please input your Password!'
}],
-
})(
-
<Input
prefix={<Icon type="lock" style={{ color:
'rgba(0,0,0,.25)'
}}
/>}
type="password"
placeholder="Password"
-
/>,
-
)}
-
</Form.Item>
-
<Form.Item>
-
<Button type="primary" htmlType="submit" className="login-form-button">
-
Log
in
-
</Button>
-
</Form.Item>
-
</Form>
-
);
-
}
}
const
WrappedNormalLoginForm
=
Form.create({ name:
'normal_login'
})(NormalLoginForm);
export
default
WrappedNormalLoginForm;
这是一个带有完整校验功能的表单。开发表单组件,至少考虑三个问题:
- 数据收集
- 校验
- 提交
表单的结构如下
代码语言:javascript复制| - Form
|-FormItem
|-校验规则渲染下的表单组件
校验是怎么实现的?留意 getFieldDecorator
:作用是封装表单组件为更强功能(可校验)的组件。
const { getFieldDecorator } = this.props.form;
代码里没有提及 this.props.form
是如何创建的,这其实是一个高阶组件,参数包括字段名/校验规则/返回的表单元素。
倒数第二行:
代码语言:javascript复制const WrappedNormalLoginForm = Form.create({ name: 'normal_login' })(NormalLoginForm);
通过一个create工厂函数变成一个高阶组件,返回功能正式的组件。
设计思想:假设有一个组件,只管样式。通过高阶组件的处理,就成了一个完整功能的表单。
如何收集数据?那得看提交方法:
代码语言:javascript复制handleSubmit = e => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
}
});
};
又是 this.props.form
提供了一个 validateFields
方法。包括校验结果 err
和 values
值。
造轮子第一步
做一个类似antd的表单组件,不妨叫他为 dantd
.
需求:先实现一个登录表单吧!
代码语言:javascript复制import React, { Component } from "react";
export default class DFormTest extends Component {
render() {
return (
<div>
<input type="text" />
<input type="password" />
<button>登录</Button>
</div>
);
}
}
那么就有一个样子了。
getFieldDec怎样才能加载上到form上?
于是问题就是做高阶组件,可以扩展现有表单,包括以上三个功能:
- 控件包装
- 时间处理
- 事件处理
于是我们可以着手来写这个高阶组件函数
代码语言:javascript复制function dFormCreate(Component){
return class extends React.Component{
constructor(props){
super(props)
// 期望用户能给传配置选项
this.option={};
this.state={};
}
/**
* 包装函数
* 接收字段名/校验配置
* 返回一个高阶组件
*/
getFieldDec=(field,option)=>{
this.option[field]=option;//选项告诉我们如何校验
return InputComp =>(
<div>
{/* vdom不能修改,克隆一份再扩展 */}
{React.cloneElement(InputComp,{
name:field,
value:this.state[field]||'',
onChange:this.handleChange //执行校验,设置状态
})}
</div>
)
}
render(){
return (
<Component {...this.props} getFieldDec={this.getFieldDec}/>
)
}
}
}
@dFormCreate
class DFormTest extends Component {...}
此时DFormTest已经有了 getFieldDec
。这个高阶修饰器就可以进一步处理表单元素:
让表单元素获得各种属性
代码语言:javascript复制@dFormCreate
class DFormTest extends Component {
render() {
const { getFieldDec } = this.props;
return (
<div style={{ width: '60%', margin: 'auto' }}>
{getFieldDec('username', {
rules: [{ required: true, message: 'Please input your username!' }],
})(<input type="text" />)}
{getFieldDec('password', {
rules: [{ required: true, message: 'Please input your password' }],
})(<input type="password" />)}
<button>login</button>
</div>
);
}
}
收集表单数据
代码语言:javascript复制handleChange=(e)=>{
const {name,value}=e.target;
this.setState({
[name]:value
})
}
添加校验(validateField)
代码语言:javascript复制function dFormCreate(Component) {
return class extends React.Component {
constructor(props) {
super(props)
// 期望用户能给传配置选项
this.options = {};
this.state = {};
}
handleChange = (e) => {
const { name, value } = e.target;
this.setState({
[name]: value
}, () => {
//单字段校验
this.validateField(name);
})
}
validateField = (field) => {
//在此校验
const rules = this.options[field].rules;
// some里只要任何一项不通过,就不通过并跳出。
const isValid = !rules.some(rule => {
if (rule.required) {
if (!this.state[field]) {
// 校验失败
this.setState({
[field 'Message']: rule.message
})
return true;
}
}
return false;
});
if (!isValid) {
this.setState(
{ [field 'Message']: '' }
)
}
return isValid;
}
//多个校验
validateFields = (cb) => {
// 将选项中所有field组成的数组转换为它们校验结果数组
const rets = Object.keys(this.options).map((field) => {
return this.validateField(field)
})
// 校验结果中每一项都要求true
const ret = rets.every(v => v === true);
console.log(222,this.state)
cb(ret, this.state);
}
/**
* 包装函数
* 接收字段名/校验配置
* 返回一个高阶组件
*/
getFieldDec = (field, option) => {
this.options[field] = option;//选项,告诉我们如何校验
return InputComp => (
<div>
{/* vdom不能修改,克隆一份再扩展 */}
{React.cloneElement(InputComp, {
name: field,
value: this.state[field] || '',
onChange: this.handleChange //执行校验,设置状态
})}
</div>
)
}
render() {
return (
<Component {...this.props} validateFields={this.validateFields} getFieldDec={this.getFieldDec} validate={this.validate} />
)
}
}
}