React 和 Vue 差异,React 开发总结
React 和 Vue 差异,React 业务项目开发总结
文档地址
学习方式
- 边做边学,请从实践教程开始。
- 一步步学习概念,请从 Hello World 开始。
- ...
编写特点
- vue
- 传参以 数据 为主,事件传递较少,组件以插槽形式传入
- HTML 的模板语法(也支持 JSX)
- react
- 万物皆可 props,数据、事件、组件都可以作为 props 传递
- JSX,也可以不使用 JSX 的 React
差异
生命周期-初始渲染
Vue
- created
- mounted
React
Hook 钩子函数
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
常见的Hook
- useState 修改 state
- useEffect 操作副作用,对 DOM 的更改后运行你的“副作用”函数
- useMemo 返回一个缓存的值
- 类似 vue 的 computed
- 计算结果缓存
- 优化有助于避免在每次渲染时都进行高开销的计算
- 在渲染期间执行,即在 DOM 更新前触发的
- 类比生命周期就是 shouldComponentUpdate
- useRef 通过 ref 访问 DOM,useRef 会在每次渲染时返回同一个 ref 对象
- useContext 实现跨组件间的数据传输
- useCallback 返回的是缓存的函数
const fnA = useCallback(fnB, [a])
- 当属性 a 发生变动时,返回新的函数 fnB
- 和 useMemo 类似,但是返回的是缓存的函数
- useReducer 小范围内的状态管理工具
const [state, dispatch] = useReducer(reducer, initialState)
- 作为 useState 的替代方案
- 【React全解6】useReducer的使用详解和代替Redux
- React 中的useReducer是个什么东西
- 【React】useContext与useReducer结合实现状态管理
useEffect
useEffect(Function, Array)
- useEffect 和生命周期有关,类似 mounted + watch
- useMemo 类似 computed
useEffect 的第二个参数,有三种情况
- 什么都不传,组件每次 render 之后 useEffect 都会调用,相当于 componentDidMount 和 componentDidUpdate
- 传入一个空数组 [], 只会调用一次,相当于 componentDidMount 和 componentWillUnmount
- 传入一个数组,其中包括变量,只有这些变量任意一个变动时,useEffect 才会执行;有点像 useWacth
const [count, setCount] = useState(0);
useEffect(() => {
console.log('xxx') // 表示你要执行的动作
}, [count]); // 表示在count的值发生变化的时候,useEffect中的方法再执行一次,类似componentDidUpdate函数
useEffect(() => {
const timer = setInterval(() => {
// do something
}, 1000);
// 表示在组件销毁的时候执行清楚定时器的动作,类似于componentWillUnmount函数
return () => clearInterval(timer);
}, []);
useMemo(主要针对当前组件中使用函数,优化性能)
useEffect和组件的生命周期有关,但 useMemo 跟生命周期不挂钩。
useMemo 是性能优化的手段,类似于类组件中的 shouldComponentUpdate,在子组件中使用 shouldComponentUpdate, 判定该组件的 props 和 state 是否有变化,从而避免每次父组件render时都去重新渲染子组件。
const [ count, setCount ] = useState(0)
const add = useMemo(() => count + 1 , [count])
<div>
点击次数: { count }<br/>
次数加一: { add }<br/>
<button onClick={() => { setCount(count + 1)}}>点我</button>
</div>
实例
// 表单数据回显
useEffect(() => {
console.log(1);
form.setFieldsValue({
...data
});
}, [data]);
useEffect(() => {
form.setFieldValue('roles', Array.isArray(dataSource) ? dataSource.map(item => ({
multiPerson: item.multiPerson,
name: item.name,
id: item.id,
employees: [],
})) : []);
}, [dataSource]);
数据流
Vue
- 双向绑定
- 单向数据流
this.name = '张三'
<input v-model="value" />
React
- 单向数据流
- 万物皆 Props
- 主要通过 onChange/setState()
- 不支持修改 props 数据,会报错
const { name, setName } = useState('');
setName('张三');
// 会报错,props的值不可修改
<input value={this.props.value}/>
// 在onChange调用setState修改数据,需要调用setState修改绑定数据
<input value={this.state.value} onChange={this.onChange}/>
传参
React
事件/函数
<AppCard key={index} title={item.title} onClick={item.onClick} />;
<Button onClick={cancel}>取消</Button>
变量
<Button type='primary' disabled={requesting} onClick={confirm}>确定</Button>
样式
<Title style={{ margin: 0, paddingLeft: 32 }} level={4}>新增采购单</Title>
<Form form={form} className={styles.form}>
组件
<Layout bodyStyle={{ height: 'calc(100vh - 160px)' }} actions={Operate()}>
实例
<div className={styles.container}>
<PageContainer noMargin>
<Layout actions={Operate()} bodyStyle={{ height: 'calc(100vh - 215px)' }}>
<div className={styles.formContainer}>
<Form {...layout} labelAlign='left' form={form} colon={false} className={styles.form}>
<div className='fn-20 lh-28 font-weight-500 mb-16'>新增采购单</div>
<Basic form={form} updateExtraFormData={updateExtraFormData} />
<Places form={form} />
<Role />
</Form>
</div>
</Layout>
</PageContainer>
</div>
判断隐藏/显示
Vue v-if
<Process v-if="curStep === 1"/>
React
- 短路运算符
{curStep === 1 && <Process/>}
- 三目运算符
{curStep === 1 ? <Process/> : null}
循环
Vue v-for
<AppCard v-for="(item, index) in list" :key="index" :title="item.title" @click="item.onClick" />
React map
{
list.map((item, index) => {
return <AppCard key={index} title={item.title} onClick={item.onClick} />;
});
}
样式
Vue 不做赘述
React
单个 className
import styles from './entry.module.less';
<Form form={form} className={styles.form}>
entry.module.less
.form {
width: 80%;
margin: 0 auto;
:global {
.resant-form-item {
&-label {
width: 80px;
// > label {
// color: #768098;
// }
}
&.role .resant-form-item-label {
white-space: normal;
word-break: break-all;
overflow: initial;
}
}
}
}
:global
样式局部作用域,类似 /deep/
多样式写法
import cs from 'classnames';
<div className={cs(styles['extra-amount-item'], styles['extra-amount-total'], !Array.isArray(extra_amounts) || !extra_amounts.length ? styles['no-amount'] : '')}>
style
<Title style={{ margin: 0, paddingLeft: 32 }} level={4}>新增采购单</Title>
<Form form={form} className={styles.form}>
样式普通写法
import './Container.less';
return <div className='detail-container' style={style}>
{/* 内容主体 */}
<div className='detail-container__main'>{children}</div>
</div>;
src/common/components/Detail/Container.less
.detail-container {
display: flex;
flex-direction: column;
height: 100%;
&__main {
overflow-y: auto;
flex-grow: 1;
}
}
样式 module 写法
import styles from './Container.module.less';
return <div className={styles['detail-container']} style={style}>
{/* 内容主体 */}
<div className='detail-container__main'>{children}</div>
</div>;
src/common/components/Detail/Container.module.less
只有加上 :global
后,.detail-container__main
才生效
.detail-container {
display: flex;
flex-direction: column;
height: 100%;
:global {
.detail-container__main {
overflow-y: auto;
flex-grow: 1;
}
}
}
组件嵌套
Vue
- 默认插槽
<slot></slot>
- 具名插槽
<slot name="label"></slot>
React
- 默认插槽,类似 Vue 的default 默认插槽,通过组件的 children 参数使用该插槽
- 具名插槽,通过组件的其他 props 属性入参
跳转
Vue
this.$router.push('/purchase');
React
项目中的跳转
import { dispatchNavigate } from '@/common/document-event/dispatch';
dispatchNavigate('/purchase');
组件通信
Vue
- props/emit
- provide/inject
- ref
- vuex(双向数据绑定,响应式)
- event bus
- ref
React
- props(子传父通过
props.function(...)
) - ref
- context
- redux(单向数据流)
表单
Vue
el-form
React
内部封装了 value、change,https://ant.design/components/form-cn#formitem
被设置了 name 属性的 Form.Item 包装的控件,表单控件会自动添加 value(或 valuePropName 指定的其他属性) onChange(或 trigger 指定的其他属性),数据同步将被 Form 接管
示例
<Form form={form}>
<div className='fn-16 lh-22 font-weight-500 mb-20'>{fieldName}</div>
<div className='fn-14 lh-20 font-weight-500 mb-16'>1.点位信息</div>
<div className='flex-row'>
<FormRangePicker label='活动日期' name='date' rules={[{ required: true, message: '请选择活动日期' }]} formItemConfig={{ className: 'mr-20' }} />
<FormDatePicker label='进场时间' name='enter_time' placeholder='请选择进场时间' />
</div>
<div className='flex-row'>
<FormDatePicker label='撤场时间' name='exit_time' formItemConfig={{ className: 'mr-20' }} placeholder='请选择撤场时间' />
<Form.Item label='场地面积' name='size'><div className={styles.size}>{size}</div></Form.Item>
</div>
</Form>
常用表单事件
-
form.validateFields();
校验 -
form.getFieldValue('user_id');
获取表单字段 -
form.getFieldsValue();
获取表单所有字段 -
Form.useWatch('user_id', form);
监听表单字段 -
form.setFieldValue('user_id', 1);
设置表单字段 -
form.setFieldsValue({ 'user_id': 1 });
批量设置表单字段
获取表单字段
const [formData, setFormData] = useState<any>({});
const onChange = (values, a) => {
console.log('onChange values', values, a);
setFormData(values);
};
const user_id = Form.useWatch('user_id', form);
<div>form.getFieldValue:{form.getFieldValue('user_id')}</div>
<div>form.getFieldsValue:{form.getFieldsValue().user_id}</div>
<div>formData.user_id:{formData.user_id}</div>
<div>user_id:{user_id}</div>
表单校验规则
antd 的校验方式基本和 element 相似
Vue
组件上使用
<FormInput
v-model="row.remark"
:prop="`plans.${index}.remark`"
type="textarea"
maxlength="200"
:form-item-config="{ 'label-width': '0' }"
placeholder="请填写备注,最多可输入200字"
/>
validator
区别:
- 成功
callback();
- 失败
return callback(Error('请输入两位小数点小数'));
'purchased_money': [{
validator: (rule, value, callback) => {
if ((purchased_money_from && !TWO_DECIMAL_NUMBER_REG.test(purchased_money_from)) ||
(purchased_money_to && !TWO_DECIMAL_NUMBER_REG.test(purchased_money_to))) {
return callback(Error('请输入两位小数点小数'));
}
if (String(purchased_money_from) && String(purchased_money_to) && +purchased_money_to < +purchased_money_from) {
return callback(Error('最小值不能大于最大值'));
}
callback();
},
}],
React
组件上使用
<FormInputPassword
label='新密码'
name='newPassword'
rules={[{ required: true, message: '请输入新密码' }, { pattern: EIGHT_SIXTEEN_PASSWORD_REG, message: '密码格式错误' }]}
/>
validator
区别:
- 成功
return Promise.resolve();
- 失败
return Promise.reject(new Error('请输入两位小数点小数'));
// 金额付款计划校验
const totalAmount = 100;
const amountPlansRules = [{
validator: (rule, value) => {
if (value.some(item => isNotEmpty(item.amount) && !TWO_DECIMAL_NUMBER_REG.test(item.amount))) {
return Promise.reject(new Error('请输入两位小数点小数'));
}
return Promise.resolve();
},
}, {
validator: (rule, value) => {
const result = Array.isArray(value) ? value.reduce((result, item) => {
return floorKeep(result, item.amount, 2);
}, 0) : 0;
if (result !== +totalAmount) {
return Promise.reject(new Error(`总金额 ${totalAmount} 元与分期总金额不一致,请检查`));
}
return Promise.resolve();
},
}];
获取表单多层级的字段
下面以动态表单获取 amount_plans 数组内的 name 字段为例
formData = {
amount_plans: [
{ name: null, amount: 200 },
{ name: null, amount: 300 },
]
}
Vue
:prop="`amount_plans.${index}.name`"
<FormInput v-model="row.name" :prop="`plans.${index}.name`"/>
React
name={['amount_plans', index, 'name']}
<FormInput name={['amount_plans', index, 'name']}/>
动态表单
React
Form.List
组件
使用 form.setFieldsValue({
amount_plans: [
{ name: null, amount: 200 },
{ name: null, amount: 300 },
]
});
// 用于实时打印 amount_plans
const amount_plans = Form.useWatch('amount_plans', form);
{JSON.stringify(amount_plans, null, 2)}
{/* 绑定 amount_plans[0].amount */}
<FormInputNumber
name={['amount_plans', 0, 'amount']}
min={0}
max={9999999999.99}
config={{ addonAfter: '元' }}
placeholder='请输入收款金额'
/>
<Form.List name='amount_plans'>
{(fields) => (
<>
{Array.isArray(fields) && fields.map((item: any, index) => (
<div key={index} className={cs(styles.flex, styles.plans)}>
{/* { "name": 0, "key": 0, "isListField": true, "fieldKey": 0 } */}
{JSON.stringify(item, null, 2)}
<FormDatePicker
name={[item.name, 'time']}
rules={[{ required: true, message: '请选择付款日期' }]}
formItemConfig={{ className: cs('mr-20', 'mb-8', styles['plans__date']) }}
placeholder='请选择收款日期'
/>
<FormInputNumber
name={[item.name, 'amount']}
min={0}
max={9999999999.99}
config={{ addonAfter: '元' }}
rules={[{ pattern: TWO_DECIMAL_NUMBER_REG, message: '请输入两位小数点小数', }]}
formItemConfig={{ className: cs('mr-20', 'mb-8', styles['plans__amount']) }}
placeholder='请输入收款金额'
/>
{/* 等同于 name={[item.name, 'amount']} 的写法 */}
<FormInputNumber
name={[index, 'amount']}
min={0}
max={9999999999.99}
config={{ addonAfter: '元' }}
placeholder='请输入收款金额'
/>
</div>
))}
</>
)}
</Form.List>
Group
组件
使用 <Form.Item name='amount_plans' label='付款计划' className={styles.plans} rules={amountPlansRules} >
<Group value={amount_plans} setValue={setPlans} getOriData={getOriPlan} dittoCoverData={{ amount: null }}>
{(item, index) => <>
<FormDatePicker
name={['amount_plans', index, 'time']}
rules={[{ required: true, message: '请选择付款日期' }]}
formItemConfig={{ className: cs('mr-20', 'mb-8', styles['plans__date']) }}
placeholder='请选择收款日期'
/>
<FormInputNumber
name={['amount_plans', index, 'amount']}
min={0}
max={9999999999.99}
config={{ addonAfter: '元', onChange: validateAmountPlans }}
rules={[{ pattern: TWO_DECIMAL_NUMBER_REG, message: '请输入两位小数点小数', }]}
formItemConfig={{ className: cs('mr-20', 'mb-8', styles['plans__amount']) }}
placeholder='请输入收款金额'
/>
</>}
</Group>
</Form.Item>
Ref 组件实例
Vue
<PointEditor ref="pointEditor"/>
// 调用组件实例事件
this.$refs.pointEditor.init();
React
import { useRef } from 'react';
// 设置组件实例
const pointEditor = useRef(null);
// 调用组件实例事件
const ref = (pointEditor as any).current;
ref && ref.init(row);
<PointEditor ref={pointEditor}/>
// PointEditor 组件内抛出事件
// PointEditor.tsx
import { forwardRef, useImperativeHandle } from 'react';
const PointEditor:FC<any> = forwardRef(({ onConfirm }, ref) => {
// 抛出给 ref 事件
useImperativeHandle(ref, () => ({
init
}));
const init = () => {};
});