起步
资源
- react:https://reactjs.org/
- create-react-app:https://github.com/search?q=create-react-app
- 在线demo练习:http://react.shengxinjing.cn/
安装脚手架
npm install -g create-react-app
- react的api比较少,基本学一次,就再也不用看文档了,核心就是 js 的功力
快速创建项目
- 创建项目的命令:create-react-app react01
- cd react01
- 启动命令:npm start
文件结构
|—— README.md 文档
|—— package-lock.json
|—— package.json npm依赖
|—— public 静态资源
| |—— favicon.ico
| |—— index.html
| |—— manifest.json
|—— src 源码
|—— App.css
|—— App.js 根组件
|—— App.test.js 测试
|—— index.css 全局样式
|—— index.js 入口
|—— logo.svg
|—— serviceWorker.js pwa支持
React 和 ReactDOM
- 删除src下面所有代码文件,新建index.js(项目入口文件)
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App'
ReactDOM.render(<App/>, document.querySelector('#root'));
- 新建App.js(根组件)
import React, { Component } from 'react'
class App extends Component{
render(){
return (
<div>
Hello, React
</div>
)
}
}
export default App;
- React 设计之初,就是使用 JSX 来描述 UI,所以解耦 和 DOM 操作,React 只做逻辑层,ReactDOM去渲染实际的DOM,如果换到移动端,就使用其他的渲染库。
JSX
- 上面的代码会有一些困惑的地方,首先就是 JSX 的语法
ReactDOM.render(<App/>, document.querySelector('#root'));
,看起来就是 js 和 HTML的混合体,被称之为 JSX,实际核心逻辑完全是 js 实现的。 - JSX实质就是 React.createElement 的调用:
state 和 setState
- 在App组件里,我们可以通过 {} 在JSX中渲染变量
import React, { Component } from 'react'
export default class App extends Component{
render(){
const name = 'React';
return (
<div>
Hello, {name}
</div>
)
}
}
- 如果数据需要修改,并且同时页面响应变化,我们需要放在 state 中,并且使用 setState 来修改数据
import React, { Component } from 'react'
export default class App extends Component{
constructor(props){
super(props);
this.state = {
msg = 'React';
}
setTimeout(() => {
this.setState({
msg: 'React is very nice!'
});
}, 2000);
}
render(){
return (
<div>
{this.state.msg}
</div>
)
}
}
- 注意:我们不能使用
this.state.msg='xxxxx'
直接对 state 中维护的数据进行修改,而是要使用 this.setSate 去设置一个新的 state,对数据进行覆盖。
props属性传递
- index.js
ReactDOM.render(<App title="React 学习"/>, document.querySelector('#root'));
- App.js
...
<h2>{this.props.title}</h2>
条件渲染和循环
- React 的 api 不多,条件渲染和循环,都是普通的 js 语法
export default class CartSample extends Component {
constructor(props){
super(props);
this.state = {
goods: [
{id: 1, text: 'Web全栈架构师', price: 666},
{id: 2, text: 'React', price: 888},
{id: 3, text: 'Vue', price: 888}
]
}
}
render() {
const title = this.props.title ? <h1>{this.props.title}</h1> : null;
// 循环:将js对象数组转换为jsx数组
const goods = this.state.goods.map(good => (
<li key={good.id}>{good.text}
<button onClick={() => this.addToCart(good)}>加购</button>
</li>
));
return (
<div>
{/* 条件语句 */}
{/* {this.props.title && <h1>{this.props.title}</h1>} */}
{title}
{/* 列表渲染 */}
<ul>{goods}</ul>
</div>
)
}
}
class VS 函数组件
- 如果一个组件只根据 props 渲染页面,没有内部的 state(即组件是无状态的),我们完全可以用函数组件的形式来实现(hooks的到来会改变这个现状)
function Title(title){
return <h2>{title}</h2>
}
<Title title={this.props.title}></Title>
事件监听
- React 中使用 onClick 类似的写法来监听事件,注意 this 绑定问题 react 里严格遵循单向数据流,没有数据双向绑定,所以输入框要设置 value 和 onChange事件
handleChange(e){
this.setState({
name: e.target.value
});
}
// 写法1:箭头函数自动修正 this
<input type="text" value={this.state.name} onChange={(e) => this.handleChange(e)}/>
// 写法2:需要在构造函数里手动绑定this,否则会报错
<input type="text" value={this.state.name} onChange={this.handleChange}/>
constructor(props){
super(props);
...
this.handleChange = this.handleChange.bind(this);
}
// 写法3:
handleChange = (e) => {
this.setState({name: e.target.value})
}
组件通信
- 购物车组件:
// 购物车组件 : 函数型组件
function Cart(props){
return (
<table>
<tbody>
{props.data.map(d => (
<tr key={d.id}>
<td>{d.text}</td>
<td>
<button onClick={() => props.descCount(d)}>-</button>
{d.count}
<button onClick={() => props.incrCount(d)}>+</button>
</td>
<td>¥{d.price * d.count}</td>
<td></td>
</tr>
))}
</tbody>
</table>
)
}
- 简易购物车组件:
export default class CartSample extends Component {
constructor(props){
super(props);
this.state = {
goods: [
{id: 1, text: 'Web全栈架构师', price: 666},
{id: 2, text: 'React', price: 888},
{id: 3, text: 'Vue', price: 888}
],
inputText: '', // 输入的商品名
cart: []
}
// // 回调的写法1
// this.addGood = this.addGood.bind(this);
}
// addGood(){
// // TODO
// }
// 回调的写法2
addGood = () => {
this.setState(prevState => ({
goods: [...prevState.goods, {id: 4, text: prevState.inputText, price: 999}]
}));
}
textChangeHandle = (ev) => {
this.setState({
inputText: ev.target.value
});
}
addToCart(good){
const newCart = [...this.state.cart];
const idx = newCart.findIndex(c => c.text === good.text);
const item = newCart[idx];
if(item){
newCart.splice(idx, 1, {...item, count: item.count+1});
} else {
newCart.push({...good, count: 1});
}
this.setState({cart: newCart});
}
incrCount = (item) => {
const newCart = [...this.state.cart];
const idx = newCart.findIndex(c => c.text === item.text);
newCart.splice(idx, 1, {...item, count: item.count+1});
this.setState({cart: newCart});
}
descCount = (item) => {
const newCart = [...this.state.cart];
const idx = newCart.findIndex(c => c.text === item.text);
// newCart.splice(idx, 1, {...item, count: item.count-1});
if(item.count === 1){
newCart.splice(idx);
}else{
newCart.splice(idx, 1, {...item, count: item.count-1});
}
this.setState({cart: newCart});
}
render() {
const title = this.props.title ? <h1>{this.props.title}</h1> : null;
// 循环:将js对象数组转换为jsx数组
const goods = this.state.goods.map(good => (
<li key={good.id}>{good.text}
<button onClick={() => this.addToCart(good)}>加购</button>
</li>
));
return (
<div>
{/* 条件语句 */}
{/* {this.props.title && <h1>{this.props.title}</h1>} */}
{title}
{/* 添加商品 */}
<div>
<input type="text" value={this.state.inputText} onChange={(e) => this.textChangeHandle(e)}/>
<button onClick={this.addGood}>添加商品</button>
</div>
{/* 列表渲染 */}
<ul>{goods}</ul>
{/* 购物车组件 */}
<Cart data={this.state.cart} incrCount={this.incrCount} descCount={this.descCount}/>
</div>
)
}
}
虚拟DOM
- 浏览器渲染图
- 浏览器的渲染过程主要包括以下几步:
* 解析HTML生成DOM树。
* 解析CSS生成CSSOM规则树。
* 将DOM树与CSSOM规则树合并在一起生成渲染树。
* 遍历渲染树开始布局,计算每个节点的位置大小信息。
* 将渲染树每个节点绘制到屏幕。
- 实际的DOM节点很重:
- DOM操作成本实在是太高,所以才有了在js里模拟和对比,JSX里使用 react,createElement构建虚拟DOM。每次有修改,先对比 js 里的虚拟DOM树:
生命周期
- React 生命周期分为三种状态:初始化、更新、销毁
初始化
- getDefaultProps():设置默认的props,也可以用defaultProps设置组件的默认属性.
- getInitialState():在使用es6的class语法时是没有这个钩子函数的,可以直接在constructor中定义this.state,此时可以访问this.props
- componentWillMount():组件初始化时只调用,以后组件更新不调用,整个生命周期只调用一次,此时可以修改state。
- render():react最重要的步骤,创建虚拟dom,进行diff算法,更新dom树都在此进行。此时就不能更改state了。
- componentDidMount():组件渲染之后调用,只调用一次。
更新
- componentWillReceiveProps(nextProps):组件初始化时不调用,组件接受新的props时调用。
- shouldComponentUpdate(nextProps, nextState):react性能优化非常重要的一环。组件接受新的state或者props时调用,我们可以设置在此对比前后两个props和state是否相同,如果相同则返回false阻止更新,因为相同的属性状态一定会生成相同的dom树,这样就不需要创造新的dom树和旧的dom树进行diff算法对比,节省大量性能,尤其是在dom结构复杂的时候
- componentWillUpdata(nextProps, nextState):组件初始化时不调用,只有在组件将要更新时才调用,此时可以修改state
- render():组件渲染
- componentDidUpdate():组件初始化时不调用,组件更新完成后调用,此时可以获取dom节点。
卸载
- componentWillUnmount():组件将要卸载时调用,一些事件监听和定时器需要在此时清除。
演示代码
- index.js:
ReactDOM.render(<Lifecycle/>, document.querySelector('#root'));
- Lifecycle.js:
import React, { Component } from 'react'
class Lifecycle extends Component {
constructor(props){
super(props);
console.log('1. 构造函数');
this.state = {
mag: '来自属性传递:' + props.prop
}
}
componentWillMount(){
// 此时可以访问属性和状态了,可以进行api调用,但没办法做DOM相关操作
console.log('2. 组件将要挂载');
}
componentDidMount(){
// 组件已挂载,可进行状态更新状态
console.log('3. 组件已经挂载');
}
componentWillReceiveProps(){
// 父组件传递的属性有变化,做相应的响应
console.log('4. 组件属性更新了');
}
shouldComponentUpdate(){
// 组件是否需要更新,返回布尔值,优化点
console.log('5. 组件是否应该更新');
return true
}
componentWillUpdate(){
console.log('6. 组件将要更新');
}
componentDidUpdate(){
console.log('7. 组件已经更新');
}
render() {
console.log('组件渲染');
return (
<div>
React 组件生命周期探究
</div>
)
}
}
export default class extends Component {
constructor(props){
super(props);
this.state = {
someProp: 'some prop'
}
setTimeout(() => {
this.setState({
someProp: 'a new prop'
})
}, 2000);
}
render() {
return (
<Lifecycle prop={this.state.someProp}/>
)
}
}
组件化
React没有Vue那么多的API,基本全部都是组件,React的开发模式,大体可以用一个公式表达
UI = F(state)
ant-design组件库
试用 ant-design组件库
- ant-design组件库网址:https://ant.design/docs/react/use-with-create-react-app-cn
- 安装:
npm install antd --save
- 试用:
import React, { Component } from 'react'
import Button from 'antd/lib/button'
import 'antd/dist/antd.css'
class App extends Component{
render(){
return (
<div>
{/* antd的试用 */}
<Button type="primary">button</Button>
</div>
)
}
}
配置按需加载
- 安装 react-app-rewired 取代 react-scripts,可以扩展 webpack 的配置,类似于 vue.config.js
npm install react-app-rewired@2.0.2-next.0 babel-plugin-import --save
- 修改 package.json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
}
- 项目根目录新建文件 config-overrides.js:
const {injectBabelPlugin} = require('react-app-rewired')
module.exports = function override(config, env){
config = injectBabelPlugin([
'import', {libraryName: 'antd', libraryDirectory: 'es', style: 'css'}
], config);
return config;
}
- 接下来就可以在组件中导入了:
import {Button} from 'antd' // 具名导入
容器组件 vs 展示组件
- 基本原则:容器组件负责数据获取,展示组件负责根据 props 显示信息
- 优势:
* 如何工作(业务逻辑)和如何展示分离
* 重用性高
* 更高的可用性
* 更易于测试
// 容器
export default class CommentList extends Component {
constructor(props){
super(props);
this.state = {
comments: []
}
}
componentDidMount(){
setInterval(() => {
this.setState({
comments: [
{body: 'react is very good', author: 'facebook'},
{body: 'vue is very good', author: 'youyuxi'}
]
})
}, 1000);
}
render() {
return (
<div>
{this.state.comments.map((c, i) => (
<Comment key={i} data={c}></Comment>
))}
</div>
)
}
}
// 展示组件
function Comment({data}){
console.log('render');
return (
<div>
<p>{data.body}</p>
<p>--{data.author}</p>
</div>
)
}
- 问题:每隔1秒,不论数据有没有发生变化,Comment组件都会渲染,如何解决?
- 我们可以想到利用生命周期函数 shouldComponentUpdate 判断数据是否发生变化,从而控制是否重新渲染
class Comment extends Component{
shouldComponentUpdate(nextProps){
if(nextProps.data.body === this.props.data.body &&
nextProps.data.author === this.props.data.author){
return false;
}
return true;
}
render(){
console.log('render');
return (
<div>
<p>{this.props.data.body}</p>
<p>--{this.props.data.author}</p>
</div>
)
}
}
- 这样确实是可以解决组件重复渲染的问题,但是在 Comment 中的shouldComponentUpdate中写一大段的逻辑,显然不是我们所希望的,有没有更好的办法?
- 回答是:YES,PureComponent(纯组件)
PureComponent
- PureComponent定制了 shouldComponentUpdate 后的 Component 浅比较
class Comment extends PureComponent{
render(){
console.log('render');
return (
<div>
<p>{this.props.body}</p>
<p>--{this.props.author}</p>
</div>
)
}
}
// 传值改变
<Comment key={i} body={c.body} author={c.author}></Comment>
- 缺点就是必须使用 class 形式
- 想要利用 PureComponent 特性,原则:
* 确保数据类型是值类型
* 果是引用类型,确保地址不变,同时不应当有深层次数据变化
React.memo
- React v16.6.0之后的版本,可以使用一个新功能
React.memo
来完美实现 React 组件,让函数式的组件也有了PureComponent的功能 - 我们继续改造 Comment 组件:
// memo只是让函数组件可以拥有PureComponent的功能
const Comment = React.memo(({body, author}) => {
console.log('render');
return (
<div>
<p>{body}</p>
<p>--{author}</p>
</div>
)
});
- 父组件解构传参:
<Comment key={i} {...c}></Comment>
组件复合
- 代码:
import React, { Component } from 'react'
// Dialog
function Dialog(props){
return (
<div style={{border: `4px solid ${props.color || 'blue'}`}}>
{/* 等效于Vue中匿名插槽 */}
{props.children}
{/* 等效于Vue中具名插槽 */}
<div className="footer">{props.footer}</div>
</div>
)
}
function WelcomeDialog(){
const confirmBtn = <button onClick={() => alert('React确实好!')}>确定</button>
return (
<Dialog color="green" footer={confirmBtn}>
<h1>欢迎光临</h1>
<p>感谢使用React!!!</p>
</Dialog>
)
}
export default class Composition extends Component {
render() {
return (
<div>
<WelcomeDialog></WelcomeDialog>
</div>
)
}
}
- 问题1:children到底是什么?
答:是任意合法的 JS 表达式,包括函数,例如:
// 模拟接口
const api = {
getUser: () => ({name: 'jerry', age: 20})
}
function Fetcher(props){
let user = api[props.name]();
return props.children(user);
}
export default class Composition extends Component {
render() {
return (
<div>
{/* children内容可以是任意的表达式 */}
<Fetcher name="getUser">
{({name, age}) => (<p>{name} - {age}</p>)}
</Fetcher>
</div>
)
}
}
- 问题2:children可以修改吗?
(1)可以修改的情况,例如:
function FilterP(props){
return (
<div>
{/* React.Children:提供了若干操作嵌套内容的帮助方法 */}
{React.Children.map(props.children, child => {
console.log(child); // vdom
if(child.type != 'p'){// 过滤掉非 p 标签
return;
}
return child;
})}
</div>
)
}
export default class Composition extends Component {
render() {
return (
<div>
{/* 操作children,过滤掉非 p 标签 */}
<FilterP>
<div>div标签</div>
<h3>h3标签</h3>
<p>p标签</p>
<a>a标签</a>
</FilterP>
</div>
)
}
}
(2)不可以修改的情况,例如:
function RadioGroup(props){
return (
<div>
{React.Children.forEach(props.children, child => {
child.props.name = props.name;
})}
</div>
)
}
function Radio({children, ...rest}){
return (
<label>
<input type="radio" {...rest}/>
{children}
</label>
)
}
export default class Composition extends Component {
render() {
return (
<div>
{/* 编辑children */}
<RadioGroup name="mvvm">
<Radio value="vue">vue</Radio>
<Radio value="react">react</Radio>
<Radio value="angular">angular</Radio>
</RadioGroup>
</div>
)
}
}
当我们试图给children增加一个属性 name 的时候,会报错,如下图:
那么我们应该怎么做?回答是,我们可以利用 map 方法返回一个全新的:
function RadioGroup(props){
return (
<div>
{React.Children.map(props.children, child => {
return React.cloneElement(child, {name: props.name});
})}
</div>
)
}
高阶组件
- 为了进一步提高组件的复用性,在React里就有了HOC(Higher-Order Components)的概念。高阶组件也是一个组件,但是她返回另一个组件,产生新的组件可以对属性进行包装,也可以重写部分生命周期。
- 示例:
// 高阶组件
const withName = Comp => {
// 写法一
// 假设通过某种特殊手段获取了本节课名字
// return props => <Comp {...props} name="高阶组件使用介绍"/>
// 写法二
// 甚至可以重写组件的生命周期
class NewComponent extends Component {
componentDidMount(){
console.log('do something');
}
render() {
return <Comp {...this.props} name="高阶组件使用介绍"/>
}
}
return NewComponent
}
- 上面的 withName 组件,其实就是代理了Comp,只是多传递了一个 name 参数。
高阶链式调用
- 高阶组件最巧妙的一点,是可以进行链式调用。假设我们再定义了高阶组件 withLog:
const withLog = Comp => {
console.log(Comp.name + '渲染了');
return props => <Comp {...props}/>
}
- 我们要对 Kaikeba 这个组件功能进行增强,然后导出 Kaikeba 组件,就可以这样做:
function Kaikeba(props){
return (
<div>{props.stage} - {props.name}</div>
)
}
export default withLog(withName(Kaikeba));
- 问题:这种链式的写法显然不够优雅,有没有更好的方式呢?
高阶组件装饰器写法
- ES7有一个优秀的语法——装饰器,专门用来处理上面这种问题
- 安装:npm install --save-dev babel-plugin-transform-decorators-legacy
- 配置:
const { injectBabelPlugin } = require("react-app-rewired");
module.exports = function override(config, env) {
// antd的按需加载功能
config = injectBabelPlugin(
["import", { libraryName: "antd", libraryDirectory: "es", style: "css" }],
config
);
// 添加装饰器的功能
config = injectBabelPlugin(
["@babel/plugin-proposal-decorators", { legacy: true }],
config
);
return config;
};
- 使用:
@withLog
@withName
@withLog
class Kaikeba extends Component{
render() {
return (
<div>{this.props.stage} - {this.props.name}</div>
)
}
}
export default Kaikeba;
组件通信–上下文
- Vue 中的 privide & inject 模式的来源——context
- 这种模式下有两个角色,一个是 Provider,一个是 Consumer,Provider 是在外层的组件,内部需要数据的,就用 Consumer 来读取
老版本的上下文
- getChildContext 用来返回数据
- 定义 childContextTypes 声明
新版本的上下文
示例
import React, { Component } from 'react'
// 1. 创建上下文
const Context = React.createContext();
const store = {
name: '开课吧',
sayHi(){
console.log(this.name);
}
}
export default class ContextSample extends Component {
render() {
return (
// 2. Provider提供数据 store
<Context.Provider value={store}>
<div>
{/* 3. Consumer获取数据 */}
<Context.Consumer>
{/* 4. 必须内嵌一个函数 */}
{value => <div onClick={() => value.sayHi()}>{value.name}</div>}
</Context.Consumer>
</div>
</Context.Provider>
)
}
}
高阶组件的写法:
// 供应者
const withProvider = Comp => props => (
<Context.Provider value={store}>
<Comp {...props}></Comp>
</Context.Provider>
)
// 消费者
const withConsumer = Comp => props => (
<Context.Consumer value={store}>
{/* 必须内嵌一个函数 */}
{value => <Comp {...props} value={value}/>}
</Context.Consumer>
)
// 内嵌到Consumer中的函数
@withConsumer
class Inner extends Component {
render(){
return (
<div>{this.props.value.name}</div>
)
}
}
// ContextSample
@withProvider
class ContextSample extends Component {
render() {
return (
<div>
<Inner></Inner>
</div>
)
}
}
export default ContextSample
createContext实现原理
function createContext(){
let instance = {
value: null
}
class Provider extends React.Component{
constructor(props){
super(props);
instance.value = props.value;
}
render() {
return this.props.children;
}
}
class Consumer extends React.Component{
constructor(props){
super(props);
this.state = {
value: instance.value
}
}
render() {
return this.props.children(this.state.value);
}
}
return {Provider, Comsumer};
}
自定义表单组件
试用antd的Form表单
import React from 'react'
import { Form, Icon, Input, Button } from "antd";
function hasErrors(fieldsError) {
return Object.keys(fieldsError).some(field => fieldsError[field]);
}
class HorizontalLoginForm extends React.Component {
componentDidMount() {
// To disabled submit button at the beginning.
this.props.form.validateFields();
}
handleSubmit = e => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
console.log("Received values of form: ", values);
}
});
};
render() {
const {
getFieldDecorator,
getFieldsError,
getFieldError,
isFieldTouched
} = this.props.form;
// Only show error after a field is touched.
const userNameError =
isFieldTouched("userName") && getFieldError("userName");
const passwordError =
isFieldTouched("password") && getFieldError("password");
return (
<Form layout="inline" onSubmit={this.handleSubmit}>
<Form.Item
validateStatus={userNameError ? "error" : ""}
help={userNameError || ""}
>
{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
validateStatus={passwordError ? "error" : ""}
help={passwordError || ""}
>
{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"
disabled={hasErrors(getFieldsError())}
>
Log in
</Button>
</Form.Item>
</Form>
);
}
}
const WrappedHorizontalLoginForm = Form.create({ name: "horizontal_login" })(
HorizontalLoginForm
);
// ReactDOM.render(<WrappedHorizontalLoginForm />, mountNode);
export default WrappedHorizontalLoginForm;
KFormSample
- kFormCreate:包装用户表单,增加数据管理能力、校验
function kFormCreate(Comp) {
return class extends Component {
constructor(props) {
super(props);
this.options = {}; // 字段选项设置
this.state = {}; // 各字段的值
}
// 处理表单项输入事件
handleChange = e => {
const { name, value } = e.target;
this.setState(
{
[name]: value // 这是异步的
},
() => {
// 数值变化后再校验
this.validateField(name);
}
);
};
// 处理表单项焦点事件
handleFocus = e => {
const field = e.target.name;
this.setState({
[field + "Focus"]: true
});
};
// 表单校验
validateField = field => {
const rules = this.options[field].rules;
// 校验结果,只有任何一项失败,就失败
const result = rules.some(rule => {
if (rule.required) {
// 仅校验必填项
if (!this.state[field]) {
// 校验失败
this.setState({
[field + "Message"]: rule.message
});
return true; // 若有校验失败,返回true
}
}
});
if (!result) {
// 没失败
this.setState({
[field + "Message"]: ""
});
}
return !result;
};
// 提交表单时,校验所有字段
validate = cb => {
const results = Object.keys(this.options).map(field =>
this.validateField(field)
);
// 如果校验结果数组中全部为true,则校验成功
const result = results.every(v => v === true);
// 回调校验结果
cb(result);
};
//
getFieldDec = (field, option, InputComp) => {
this.options[field] = option;
return (
<div>
{React.cloneElement(InputComp, {
name: field, // 控件name
value: this.state[field] || "",
onChange: this.handleChange, // change事件处理
onFocus: this.handleFocus // 判断控件是否获得焦点
})}
{/* 错误信息 */}
{/* {this.state[field + "Message"] && (
<p style={{ color: "red" }}>{this.state[field + "Message"]}</p>
)} */}
</div>
);
};
// 判断组件是否被用户点过
isFieldTouched = field => !!this.state[field + "Focus"];
// 获取字段错误信息
getFieldError = field => this.state[field + "Message"];
render() {
return (
<Comp
{...this.props}
getFieldDec={this.getFieldDec}
value={this.state}
validate={this.validate}
isFieldTouched={this.isFieldTouched}
getFieldError={this.getFieldError}
/>
);
}
};
}
- KFormItem:只做展示
class KFormItem extends Component {
render() {
console.log(this.props)
return (
<div className="formItem">
{this.props.children}
{this.props.validateStatus === "error" && (
<p style={{ color: "red" }}>{this.props.help}</p>
)}
</div>
);
}
}
- KInput:重构 input 输入控件
class KInput extends Component{
render(){
return (
<div>
{/* 前缀图标 */}
{this.props.prefix || ''}
<input {...this.props}/>
</div>
)
}
}
- KFormSample:最终导出的简易表单组件
@kFormCreate
class KFormSample extends Component {
onSubmit = () => {
this.props.validate(isValid => {
if (isValid) {
alert("校验成功,提交登陆");
console.log(this.props.value);
} else {
alert("校验失败");
}
});
};
render() {
const { getFieldDec, isFieldTouched, getFieldError } = this.props;
const userNameError = isFieldTouched("uname") && getFieldError("uname");
const passwordError = isFieldTouched("pwd") && getFieldError("pwd");
return (
<div>
<KFormItem
validateStatus={userNameError ? "error" : ""}
help={userNameError || ""}
>
{getFieldDec(
"uname",
{
rules: [{ required: true, message: "请输入用户名" }]
},
// <input type="text" />
<KInput type="text" prefix={<Icon type="user"></Icon>}/>
)}
</KFormItem>
<KFormItem
validateStatus={passwordError ? "error" : ""}
help={passwordError || ""}
>
{getFieldDec(
"pwd",
{
rules: [{ required: true, message: "请输入密码" }]
},
// <input type="password" />
<KInput type="text" prefix={<Icon type="lock"></Icon>}/>
)}
</KFormItem>
<button onClick={this.onSubmit}>登陆</button>
</div>
);
}
}
export default KFormSample;
React未来
Fiber
- 同步渲染 vs 异步渲染
- React的新引擎 Fiber 的关键特性如下:
* 增量渲染(把渲染任务拆分成块,均分到帧)
* 更新是能够暂停、终止,复用渲染任务
* 给不同类型的更新赋予优先级
* 并发方面新的基础能力
Suspense
用同步代码来实现异步操作
- 常见的异步操作代码:
class Foo extends React.Component{
state = {data: null};
render(){
if(!this.state.data){
return null;
} else {
return <div>this.state.data</div>
}
}
componentDidMount(){
callAPI().then(result => {
this.setState({data: result});
})
}
}
- 组件必须要有自己的 state 和 componentDidMount 函数实现,也就不可能做成纯函数形式的组件
- 需要两次渲染过程,第一次是 mount 引发的渲染,由 componentDidMount 触发 AJAX 然后修改 state,然后第二次渲染才真的渲染出内容
- 代码啰嗦,十分啰嗦
使用Suspense
const Foo = () => {
const data = createFetcher(callAJAX).read();
return <div>{data}</div>
}
- createFetcher还没有正式发布
- 变革:函数组件可以做数据获取,扩展了 FP 在 react 中的应用
Hooks
React v16.7.0-alpha中第一次引入了 Hooks的概念,因为这是一个alpha版本,不是正式版本,所以将来正式发布时 API 可能会有变化
useState
Hooks 的目的,是开发者可以完全抛弃class,每天早晨起床,拥抱函数式提供一个叫useState的方法,它开启了一扇新的定义 state 的门,对应的 Counter 的代码可以这么写:
import { useState } from 'react'
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
<div>
);
}
- 还可以设置多个
const [foo, updateFoo] = useState('foo');
useEffect
- 除了useState,React还提供 useEffect ,用于支持组件中增加副作用的支持。对应的 class 写法中的生命周期:
componentDidMount(){
document.title = `开课吧:{this.state.count}`;
}
componentDidUpdate(){
document.title = `开课吧:{this.state.count}`;
}
import { useState, useEffect } from 'react'
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `开课吧:{count}`;
});
return (
<div>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
<div>
);
}
- useEffect 的参数是一个函数,组件每次渲染之后,都会调用这个函数,这样就达到了 componentDidMount 和 componentDidUpdate 一样的效果。
- Hooks会大大减少 react 的代码。