从Vue到React(一)基础篇

react基础

# 前言

gap一段时间,终于有空把好久不碰的react捡起来了。
初学前端的时候觉得react入门比较难,现在回看起来不算很难,特别是有一定经验之后。而且从vue到react,挺多相似的理念,这种类似的框架学起来还是比较容易的。
不过react的一些用法比起vue还是有些复杂的,因为有些内容都需要我们自己去写,如表单绑定,vue有v-model这个语法糖,写起来就很简洁,react就需要自己去写绑定和触发。不过基于这一点,react用起来也更灵活。

这一篇是,有vue的基础上,去了解react的基本语法。

# 正文

# 1. 基本结构

在实际开发中,常使用单文件组件的形式
vue中,创建一个.vue结尾文件,包含templatescriptstyle三个部分

而react,则是一个.js/.jsx文件作为组件,其中其中写逻辑和渲染,即vue中的templatescript,而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-modelv-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.syncv-model、作用域插槽、$children$parent$refprovide/injectv-bind='$attrs'v-on='$listeners'、bus事件总线、vuex

vue3中去掉了.sync$children、bus事件总线
支持多个v-model v-on='$listeners'合并到了v-bind='$attrs'

相比较起来,react的通信方法就简单多了,对应vue可以分为以下五类:

  1. props:对应vue中的props$emit.syncv-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: () => { }
}
  1. 跨代通信Context.Provider:对应vue中的provide/inject
    通过Context.Provider将子组件包裹起来,value传入相应参数,孙子(后代)组件引入context,通过hook useContext方法拿到父组件传的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>
}

  1. 获取实例useRef:对应vue的ref
    就是使用hook useRef定义得到一个ref对象,然后对该组件传ref为定义的ref对象即可。但是注意,只能获取到原生标签或类组件的实例,不能获取函数组件
  const xxxRef = useRef(null)

  <div ref={xxxRef}>xxx</div>
  <button onClick={() => console.log(xxxRef.current)}>获取xxx实例</button>
  1. 事件总线

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);
  };
}, []);
  1. redux:对应vuex,这个后面细说。

# 6. 样式

  • 类名
    vue中可以直接写class类名,class可以是对象、数组、字符串;
    react中类名不为class,而是className,只能是字符串,所以操作动态类名就是对字符串拼接的修改。不过直接改字符串拼接有些麻烦,我们可以借助第三方库classnames

  • style行内样式
    vue的行内样式stylestyle可以是对象,也可以是字符串;
    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中主要的生命周期有beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeDestroydestroyed

vue3中有些不同,去掉了create相关两个钩子,可用setup替代;函数名的变化,都有on开头,销毁的钩子名不再是destroy,而是unMount

而在react中,class组件的生命周期如图(新版的,旧的一些钩子已不推荐使用)

react生命周期

下面介绍几个常用的

  • constructor,类似于vue的created
  • render,视图渲染时会触发,初次挂载和更新都会触发,类似于vue的mountedupdated,但是在这个钩子不能再去修改响应式数据触发视图更新,因为这样会形成死循环。
  • componentsDidMount,类似于vue的mounted
  • shouldComponentUpdate,判断是否需要更新组件,是react优化的重要生命周期钩子
  • componentDidUpdate,类似于vue的updated
  • componentWillUnMount,类似于vue的beforeDestroy

而函数组件那样有明确的"生命周期"钩子,但是可以使用React的钩子(hooks)来实现类似的功能。

一般我们借助useEffect来实现,使用起来有几种情况:

  1. useEffect的第一个参数为函数,无第二个参数时,该函数会在组件挂载后或更新后执行,类似于componentsDidMountcomponentDidUpdate
  2. useEffect可以返回一个函数,该函数会在组件卸载前调用,类似于componentWillUnMount
useEffect(() => {
  // 组件挂载后或更新后的操作
  return () => {
    // 组件卸载前的操作
  };
});
  1. useEffect有第二个参数,为一个数组,若是一个空数组,函数仅在组件挂载后执行,类似于componentsDidMount
useEffect(() => {
  // 仅在组件挂载后执行
}, []);
  1. useEffect有第二个参数,为一个数组,数组里面是相关的依赖,则函数会在组件挂载后和依赖更新后执行,类似于vue的watchimmediatetrue的情况
useEffect(() => {
  // 组件挂载 和 sonValue更新后执行
}, [sonValue])

# 8. 逻辑复用

一般的复用就是引入组件复用,但是有时我们不需要复用dom或者不关注其dom,只是希望js方面的逻辑复用

在vue2中可以通过mixins进行逻辑复用,vue3去掉了mixins,可以用组合式api的写法,用组合式函数代替,进行逻辑复用

在react中,则是通过高阶组件的方式进行逻辑复用
概念上,高阶函数是接受一个或多个函数作为参数,并且/或者返回一个函数作为结果的函数。而高阶组件就是一个高阶函数,该函数接受一个组件作为参数,并返回一个新的组件。

使用上,如下,定义一个高阶函数Hoc,接收组件WrappedComponent。然后return一个函数组件(也可以是类组件),该组件中写一些逻辑,将props和想要包装组件的参数mouseXmouseY传给WrappedComponentreturnWrappedComponent。包装组件GrandSon引入该组件,调用HocGrandSon,这样GrandSon就可以通过props获取到相应的参数mouseXmouseY了。

// 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使用即可

Wonderwall
Oasis