从Vue到React(一)基础篇
# 前言
gap一段时间,终于有空把好久不碰的react捡起来了。
初学前端的时候觉得react入门比较难,现在回看起来不算很难,特别是有一定经验之后。而且从vue到react,挺多相似的理念,这种类似的框架学起来还是比较容易的。
不过react的一些用法比起vue还是有些复杂的,因为有些内容都需要我们自己去写,如表单绑定,vue有v-model这个语法糖,写起来就很简洁,react就需要自己去写绑定和触发。不过基于这一点,react用起来也更灵活。
这一篇是,有vue的基础上,去了解react的基本语法。
# 正文
# 1. 基本结构
在实际开发中,常使用单文件组件的形式
vue中,创建一个.vue结尾文件,包含template
、script
、style
三个部分
而react,则是一个.js/.jsx文件作为组件,其中其中写逻辑和渲染,即vue中的template
、script
,而css另外写在一个.css文件中,然后在该组件中引入
react组件有两种写法,class组件和函数组件,随着hooks的引入,函数组件已经成为react开发中的主流选择
// class组件
import React from 'react'
class BtnCom extends React.Component {
render() {
return <div>类组件</div>
}
}
// 函数组件
function App() {
return (
<div>函数组件</div>
);
}
类组件中通过render
函数,函数组件则是直接返回,都是使用jsx语法写标签。
在vue中传参用引号""
包裹起来,模板中用双大括号{{}}
,而react中的jsx语法,则都是通过单大括号{}
包裹起来,表明里面写的是js。
由于函数组件为现在大多数React中使用,后面更多是关注函数组件的写法,还有示例代码部分会省略
# 2. 响应式数据
vue2中选项式api,在data
中定义;vue3中组合式api,通过ref
/reactive
定义。定义的响应式数据修改后,一般情况能直接触发视图更新。
而react中,数据修改后,需手动调用方法让视图更新。在类组件,通过state
对象中定义,setState
修改完成响应式;函数组件通过hook,useState
,返回响应式数据和更新响应式的方法。
// 函数组件的写法
import { useState } from 'react';
function Xxx(){
const [count, setCount] = useState(0);
...
}
useXxx就是hook,hook只能在函数组件中使用
# 3. 数据绑定
在vue中,通过v-bind
(简写:
)进行数据绑定,若是双向绑定,可通过语法糖v-model
实现。(v-model
是v-bind:value
和 @input
/@change
的语法糖)
在react中,没有语法糖,数据的绑定就是给组件传递prop
,事件的监听类似于原生事件,但是采用驼峰的写法
下面这个例子,就是react中数据双向绑定的写法,也称为受控组件
function Xxx() {
const [value, setValue] = useState('')
const handleInput = (e) => {
setValue(e.target.value)
}
return <input value={value} onInput={handleInput} />
}
若是我们只需要获取表单的值,不需要给表单设值,就可以使用非受控组件,即不需要绑定value
,只需要监听input事件
,拿到值即可
# 4. 条件渲染和列表循环
在vue中通过v-if
进行条件渲染,v-for
进行列表渲染
而在react中,都是直接通过js进行。条件渲染通过if判断、三元表达式、&&
运算符等;列表渲染就是通过各种循环,常用的就是map
。
import { useState } from "react"
export default function ListAndIf() {
const [show, setShow] = useState(true)
const list = [10, 9, 8, 7, 6]
return (
<>
{show && <b>条件渲染</b>}
<button onClick={() => setShow(!show)}>
{!show ? <div>显示</div> : <div>不显示</div>}
</button>
<hr />
<b>列表循环</b>
<ul>
{
list.map(item => {
return <li key={item}>{item}</li>
})
}
</ul>
</>
)
}
# 5. 组件间的通信方式
vue中的通信方式很多,有一些是语法糖,props
、$emit
、.sync
、v-model
、作用域插槽、$children
、$parent
、$ref
、provide/inject
、v-bind='$attrs'
、v-on='$listeners'
、bus事件总线、vuex
vue3中去掉了
.sync
、$children
、bus事件总线
支持多个v-model
v-on='$listeners'
合并到了v-bind='$attrs'
上
相比较起来,react的通信方法就简单多了,对应vue可以分为以下五类:
- props:对应vue中的
props
、$emit
、.sync
、v-model
、作用域插槽。
直接父传子的参数,而子传父,则需要父传子的时候传一个函数,子函数调用该函数,传入相应参数给父组件。
父组件传一个函数给子组件,函数返回dom,子组件调用该函数得到dom,实现插槽的效果;该函数还接收参数,子组件调用的时候传参数,父组件就拿到相应数据了。
匿名插槽:使用子组件间写dom,子组件通过
props.children
拿到
具名插槽:指定props
直接传dom给子组件,子组件通过该prop
拿到
// 父组件中引用并使用
import Son from './Son.js'
const [sonValue, setSonValue] = useState('hello son')
<Son
sonValue={sonValue}
emitFn={(value) => setSonValue(value)}
scopeSlot={(scope)=><div>作用域插槽:{scope}</div>}
slotA={<p>具名插槽</p>}
>
<div><b>插槽内容</b></div>
</Son >
// Son.js组件
// react不像vue,可以直接定义`props`的类型,需要我们自己判断自己写,不过可以引入第三方库帮助我们定义
import PropTypes from 'prop-types'
import GrandSon from './GrandSon.js'
export default function Son(props) {
return (
<>
{/* 一般的prop */}
<div>son组件{props.sonValue}</div>
{/* 子组件接收一个函数,然后传参给父组件 */}
<button onClick={() => props.emitFn('子组件传递数据给父组件')}>传递数据给父组件</button>
{/* 匿名插槽 */}
{props.children}
{/* 具名插槽 */}
{props.slotA}
{/* 作用域插槽 */}
{props.scopeSlot('作用域插槽的内容')}
</>
)
}
Son.propTypes = {
sonValue: PropTypes.string,
emitFn: PropTypes.func,
}
Son.defaultProps = {
sonValue: 'default Son value',
emitFn: () => { }
}
- 跨代通信Context.Provider:对应vue中的
provide/inject
。
通过Context.Provider
将子组件包裹起来,value
传入相应参数,孙子(后代)组件引入context
,通过hookuseContext
方法拿到父组件传的value
只能传
value
这个参数,所以若想传多个参数给后代,两个方法:1.value
是对象,对象里面传多个参数;2.多个Context.Provider
包裹起来,后代使用时引入对应的context
即可
// 父组件App
export const AppContext = createContext()
<AppContext.Provider value={{GrandSonValue:'hello GrandSon'}}>
<Son sonValue={'hello son'} />
</AppContext.Provider>
// Son组件
import GrandSon from './GrandSon.js'
<GrandSon/>
// GrandSon组件
import { useContext } from 'react'
import { AppContext } from './App'
export default function GrandSon() {
return <div>GrandSon组件{useContext(AppContext).GrandSonValue}</div>
}
- 获取实例useRef:对应vue的
ref
就是使用hookuseRef
定义得到一个ref
对象,然后对该组件传ref
为定义的ref
对象即可。但是注意,只能获取到原生标签或类组件的实例,不能获取函数组件
const xxxRef = useRef(null)
<div ref={xxxRef}>xxx</div>
<button onClick={() => console.log(xxxRef.current)}>获取xxx实例</button>
- 事件总线
vue中的事件总线,可以直接在一个js文件中,创建一个vue实例然后暴露出去,组件中就可以引入这个实例进行$emit
分发事件和on
监听事件了
而react需使用第三方库,比如eventemitter3
// eventBus.js
import { EventEmitter } from 'eventemitter3';
const eventBus = new EventEmitter();
export default eventBus;
// A组件引入并使用
import eventBus from './eventBus';
// 一定事件中分发事件(这里省略部分代码)
eventBus.emit('custom-event', '这是传递的数据');
// B组件引入并使用
import eventBus from './eventBus';
// dom加载后中监听事件(这里省略部分代码)
useEffect(() => {
const eventBusListener = (data) => {
console.log('收到事件,数据为:', data);
};
eventBus.on('custom-event', eventBusListener);
// 清理(cleanup) 函数,在组件从 DOM 中移除后,React 将最后一次运行
return () => {
// 在组件卸载时取消事件监听
eventBus.off('custom-event', eventBusListener);
};
}, []);
- redux:对应
vuex
,这个后面细说。
# 6. 样式
类名
vue中可以直接写class
类名,class
可以是对象、数组、字符串;
react中类名不为class
,而是className
,只能是字符串,所以操作动态类名就是对字符串拼接的修改。不过直接改字符串拼接有些麻烦,我们可以借助第三方库classnames
。style行内样式
vue的行内样式style
,style
可以是对象,也可以是字符串;
react的style
只能是对象,所以在jsx语法中,{{}}
,外层{}
表示使用这是js,内层{}
表示这是个对象。样式隔离
在vue中实现样式隔离,可以在style
标签中加scoped
;
在react中则是在样式文件后缀前加.module
,使用css module
进行样式隔离。
classNames
中想应用css module
中的样式,需要借助它的bind
方法;或是通过中括号的方式配置动态类名
/* moduleClass.module.css */
.myClass{
background-color: blueviolet;
}
/* myClass.css */
.myClass{
color: red
}
// 协助动态增删类名的第三方库
import classNames from 'classnames';
import bindClassnames from 'classnames/bind';
import { useState } from 'react';
import './myClass.css' // 一般样式,全局应用
import moduleClass from './moduleClass.module.css' // 模块化的样式,只在使用该模块的样式才生效
export default function Style() {
const [needClass3, setNeedClass3] = useState(true)
const myClassName = classNames({
'myClass1': true, // 条件为真时添加该类名
'myClass2': false, // 条件为假时不添加该类名
'myClass3': needClass3, // 根据变量值动态添加类名
[moduleClass.myClass]: true // css module使用动态类名
});
// classnames 绑定 css module
const bindClassNames = bindClassnames.bind(moduleClass)
return (
<>
{/* 行内样式 */}
<div style={{ backgroundColor: 'skyBlue' }}>行内样式</div>
{/* 类名 */}
<div className="myClass">类名样式</div>
{/* 直接使用模块化的样式 */}
<div className={moduleClass.myClass}>模块化的样式</div>
{/* 动态修改类名 */}
<button className={myClassName} onClick={() => setNeedClass3(!needClass3)}>修改类名</button>
{/* classnames 绑定 css module */}
<div className={bindClassNames({ myClass: true })}>模块化使用classnames</div>
</>
)
}
# 7. 生命周期
在vue中主要的生命周期有beforeCreate
、created
、beforeMount
、mounted
、beforeUpdate
、updated
、beforeDestroy
、destroyed
vue3中有些不同,去掉了
create
相关两个钩子,可用setup
替代;函数名的变化,都有on
开头,销毁的钩子名不再是destroy
,而是unMount
而在react中,class组件的生命周期如图(新版的,旧的一些钩子已不推荐使用)

下面介绍几个常用的
constructor
,类似于vue的created
render
,视图渲染时会触发,初次挂载和更新都会触发,类似于vue的mounted
和updated
,但是在这个钩子不能再去修改响应式数据触发视图更新,因为这样会形成死循环。componentsDidMount
,类似于vue的mounted
shouldComponentUpdate
,判断是否需要更新组件,是react优化的重要生命周期钩子componentDidUpdate
,类似于vue的updated
componentWillUnMount
,类似于vue的beforeDestroy
而函数组件那样有明确的"生命周期"钩子,但是可以使用React的钩子(hooks)来实现类似的功能。
一般我们借助useEffect
来实现,使用起来有几种情况:
useEffect
的第一个参数为函数,无第二个参数时,该函数会在组件挂载后或更新后执行,类似于componentsDidMount
和componentDidUpdate
useEffect
可以返回一个函数,该函数会在组件卸载前调用,类似于componentWillUnMount
useEffect(() => {
// 组件挂载后或更新后的操作
return () => {
// 组件卸载前的操作
};
});
useEffect
有第二个参数,为一个数组,若是一个空数组,函数仅在组件挂载后执行,类似于componentsDidMount
useEffect(() => {
// 仅在组件挂载后执行
}, []);
useEffect
有第二个参数,为一个数组,数组里面是相关的依赖,则函数会在组件挂载后和依赖更新后执行,类似于vue的watch
且immediate
为true
的情况
useEffect(() => {
// 组件挂载 和 sonValue更新后执行
}, [sonValue])
# 8. 逻辑复用
一般的复用就是引入组件复用,但是有时我们不需要复用dom或者不关注其dom,只是希望js方面的逻辑复用
在vue2中可以通过mixins
进行逻辑复用,vue3去掉了mixins
,可以用组合式api的写法,用组合式函数代替,进行逻辑复用
在react中,则是通过高阶组件的方式进行逻辑复用
概念上,高阶函数是接受一个或多个函数作为参数,并且/或者返回一个函数作为结果的函数。而高阶组件就是一个高阶函数,该函数接受一个组件作为参数,并返回一个新的组件。
使用上,如下,定义一个高阶函数Hoc
,接收组件WrappedComponent
。然后return
一个函数组件(也可以是类组件),该组件中写一些逻辑,将props
和想要包装组件的参数mouseX
、mouseY
传给WrappedComponent
,return
该WrappedComponent
。包装组件GrandSon
引入该组件,调用Hoc
传GrandSon
,这样GrandSon
就可以通过props
获取到相应的参数mouseX
、mouseY
了。
// Hoc.js 高阶组件
import { useEffect, useState } from "react"
function MouseMoveCom(props, WrappedComponent) {
let [mouseX, setMouseX] = useState(0)
let [mouseY, setMouseY] = useState(0)
useEffect(() => {
window.addEventListener('mousemove', (event) => {
setMouseX(event.x)
setMouseY(event.y)
})
return () => {
window.removeEventListener('mousemove', () => { })
}
}, [])
return <WrappedComponent mouseX={mouseX} mouseY={mouseY} {...props} />
}
export default function Hoc(WrappedComponent) {
return (props) => (
MouseMoveCom(props, WrappedComponent)
)
}
// GrandSon使用高阶组件
import Hoc from './Hoc'
function GrandSon(props) {
return (
<div>
<p>x:{props.mouseX}</p>
<p>y:{props.mouseY}</p>
</div>
)
}
export default Hoc(GrandSon)
高阶组件中,若是想要使用hook,需要在外层声明一个函数组件才能使用,因为hook需要在函数组件顶层才能使用,不能在回调中使用,也不能直接在高阶函数中使用
其实就上面的功能,比起用高阶组件的方式,更方便的是直接自定义一个hook,GrandSon
引入hook使用即可