Vue2 到 React 一遍过
很多同学说自己只会 Vue 不会 React,相信我,React 没那么难,只需要花 2 天时间看完这篇对比教程,马上你就能上手开发啦。
由于近两年工作中一直使用 Vue2,React 知识快忘了。现在市场行情不好,为了减少焦虑,准备捡起来用用。
注意:
- 文章对比的是 Vue2 和 React18
- 多数 React 案例使用 Hooks
- 默认 Vue 项目使用的 UI 框架是 elementUI,默认 React 使用的 UI 框架是 antDesign
- 文章适用于 Vue 转学 React 或者 React 想学 Vue2 的同学(最好有其中一种框架的开发经验)
前置工作
在转换之前建议大家先复习一遍 Vue 中的基础知识,然后过一遍 React 中的概念(官网是是最好的学习资料),学习官方文档让我们更容易实现无痛转换。
Vue 官网
React 官网
Let`s go !
对比目录
- 安装
- 脚手架的使用
- 实例
- 常见的 UI 库组合
- 生命周期
- 模版语法
- 计算属性
- 侦听器
- class
- 条件渲染
- 列表渲染
- 事件处理
- 表单输入
- 获取 DOM
- 组件
- 组件间传参数
- 状态管理器
- 逻辑复用
- scoped css
- 不会渲染到真实 DOM 的标签
- 虚拟 DOM 对比算法
- router
- keep-alive
基础知识对比
安装
-
React
- script 引入
<!-- 适用于开发环境 --> <script crossorigin src="https://unpkg.com/React@18/umd/React.development.js" ></script> <script crossorigin src="https://unpkg.com/React-dom@18/umd/React-dom.development.js" ></script>
- npm 方式引入
npm i react react-dom
-
Vue
- script 引入
<!-- 适用于开发环境 --> <script src="https://cdn.jsdelivr.net/npm/Vue@2.7.8/dist/Vue.js"></script>
- npm 方式引入
npm install vue
脚手架
-
React
- 安装脚手架:
npm install create-react-app
- 使用脚手架创建一个 demo 项目:
npx create-react-app myApp
-
Vue
- 安装脚手架:
npm install -g @vue/cli # OR yarn global add @vue/cli
- 使用脚手架创建一个 demo 项目:
vue create myApp
实例
-
React
一个项目中只有一个 React 实例
import React from "React"; import ReactDom from "React-dom/client"; // 将虚拟dom渲染到文档中变成真实dom import App from "./App.tsx"; const Root = ReactDom.createRoot( document.querySelector("#root") as HTMLElement // tsx element需要写类型 ); Root.render( <React.StrictMode> {" "} // 严格模式只在开发环境生效 不会渲染可见UI <App /> </React.StrictMode> );
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <div id="root"></div> </body> </html>
tips:
StrictMode 目前有助于:
识别不安全的生命周期
关于使用过时字符串 ref API 的警告
关于使用废弃的 findDOMNode 方法的警告
检测意外的副作用
检测过时的 context API
确保可复用的状态 -
Vue
一个应用中只有一个 Vue 实例
import Vue from "Vue"; import App from "./App.js"; new Vue({ el: "#app", render: (h) => h(App), });
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <div id="app"></div> </body> </html>
常见的 UI 库搭档
场景 | 前端库 | UI 库| |
---|---|---|
PC | React | Ant-Design |
PC | Vue | Element-UI、Iview |
Mobile | React | Material-UI、Antd-Mobile |
Mobile | Vue | Vant |
多端 | React | Taro |
多端 | Vue | uni-app 、mpvue |
生命周期
-
React
分为:挂载阶段、更新阶段、销毁阶段
常见的生命周期钩子函数:
- 挂载阶段:
- render 适用于 class 组件 (检查 props 和 state)
- constructor 适用于 class 组件(组件挂载之前会调用他的构造函数,初始化 state 在这里)
- componentDidMount (初始页面依赖的数据 ajax 请求放在这里)
- 更新阶段:
- shouldComponentUpdate
- componentDidUpdate
- 卸载阶段:
-
componentWillUnmount (在这里取消事件监听)
-
- tips
函数组件中,提供了 useEffect 副作用钩子,让开发者只用关心状态,React 负责同步到 DOM。- useEffect 相当于 componentDidMount、componentDidUpdate、componentWillUnMount 三个生命周期钩子的合并方法
- 模拟 componentDidMount
useEffect(fn,[])的第二个参数传空数组,相当于 componentDidMount - 模拟 componentDidUpdate
useEffect(fn,[dep])的第二个参数数组中传入依赖参数,相当于 componentDidUpdate - 模拟 componentWillUnMount
useEffect(() => { return callback}, [])的第一个参数函数返回的 callback 函数会在组件卸载前被调用,相当于 componentWillUnMount
- 挂载阶段:
-
Vue
分为:创建阶段、挂载阶段、更新阶段、卸载阶段
常见的生命周期钩子函数:- 创建阶段:
- beforeCreate
- created
- 挂载阶段:
- beforeMount
- Mounted (初始页面依赖的数据 ajax 请求放在这里)
- 更新阶段:
- beforeUpdate
- updated
- 销毁阶段:
- beforeDestroy (在这里取消事件监听)
- destroied
- 创建阶段:
模版语法
-
React
jsx 语法
- 文本
const Demo = () => { return <div>{message}</div>; };
- html-innerHTML
使用富文本编辑器产生的数据,回到页面时也要按 html 来展示,React 中没有像 Vue 一样的 v-html 指令,但是提供了 dangerouslySetInnerHTML 属性
function MyComponent() { const html = "<div>innerHTML</div>"; return <div dangerouslySetInnerHTML={{ __html: html }} />; }
注意这里的用法比较特殊
- style
style 接收一个对象,width 和 height 的单位 px 可以省略
style 多用于添加动态样式 不推荐直接将 css 属性写在里面 可读性差
style 中的 key 使用小驼峰命名法const Demo = () => { return <div style={{ width: 100, height: 100, fontSize: "16px" }}></div>; };
- 属性
const Demo = () => { return <div loading={loading}></div>; };
- js 表达式
jsx 中可以书写任意表达式,甚至可以使用 map:
const Demo = () => { let flag = true; return <div>{flag ? message : "暂无数据"}</div>; };
-
Vue
基于 HTML 的模版语法
- 文本
Mustache 双大括号语法
数据绑定放在 双大括号 里面
<template> <div>{{ message }}</div> </template>
- html
<template> <div v-html="html"></div> </template> <script> export default { data() { return { html: `<div>v-html directive</div>`, }; }, }; </script>
- style
<template> <div style="width: 100px;height:100px">{{ message }}</div> <div :style={width: 100px;heihgt: 200px}></div> </template>
- 属性
v-bind
<template> <div v-bind:loading="loading"></div> </template>
Vue 中 v-bind 指令 可以缩写为冒号 “:”
- js 表达式
Vue 的模版语法中,只能包含单个表达式:
<template> <div>{{ flag ? message : "暂无数据" }}</div> </template> <script> export default { data(){ return { flag: false, } } } </script>
- 动态参数
<template> <div v-bind[attributeName]="url">动态参数</div> </template>
- 修饰符
-
.stop 阻止冒泡 相当于 event.stopPropagation
-
.prevent 阻止默认 相当于event.preventDefault()
-
.lazy 数据绑定放在 change 事件之后
-
.number 自动转换用户输入为 number 类型
-
.trim 自动去除用户输入的头尾空白字符
-
.native 将原生事件绑定到组件
-
.sync 对一个 prop 进行 “双向绑定”
-
- tips:
Vue 也支持 jsx 语法,这里仅用模版语法做对比
Vue 中 template 标签内只能有一个根节点,每个组件必须只有一个根元素
计算属性
计算属性的常见使用场景是:
- 对复杂逻辑的抽象
- 缓存计算结果
-
React
React 中使用 useMemo Hook 实现 Vue 中的 computed 实现的功能import { useMemo, useState } from "react"; import { Input } from "antd"; const demo = () => { const [firstName, setFirstName] = useState(""); const [secondName, setSecondName] = useState(""); const name = useMemo(() => { return firstName + " " + secondName; }, [firstName, secondName]); const handleFisrtNameChange = (e: any) => { setFirstName(e.target.value); }; const handleSecondNameChange = (e: any) => { setSecondName(e.target.values); }; return ( <div> <div>hello {name}</div> <Input placeholder="first name" onChange={handleFisrtNameChange} ></Input> <Input placeholder="second name" onChange={handleSecondNameChange} ></Input> </div> ); };
- tips:
useMemo 可以缓存计算结果或缓存组件
- tips:
-
Vue
Vue 提供 computed 实现计算属性:
<template> <div> <!-- 模版当中应该是简单的声明式逻辑 复杂逻辑我们使用 computed --> <div>hello {{ name }}</div> </div> </template> <script> export default { data() { return { firstName: "firstName", secondName: "secondName", }; }, computed: { name() { return this.firstName + " " + this.secondName; }, }, }; </script>
平时开发的时候很多同学的用法是:
- 兼容边界值
return data || [];
- 封装一个长的调用
return res.data.pageData.total;
感觉上面这两种用法都是不准确的, 我想 computed 这个方法一方面其实应该是用来补充 Vue 模版语法的缺陷,在 Vue 的模版语法中只支持单个表达式,computed 封装一个函数正好解决了这个问题(排序或者过滤也是不错的使用场景);另一方面应该是考虑缓存的必要性,当在模版中多个地方使用的时候,应该缓存起来避免重复计算。
你可能会说函数也有抽象复杂逻辑的功能,为什么还要使用 computed,他们的不同点在于缓存结果。computed 只有在依赖更新时才会重新计算。
-
tips:
如果不懂 hook 的同学,强烈推荐在官网的基础上再学习这一篇30 分钟精通 React Hooks,这是我看过讲 hooks 最清楚的文章,和官方文档结合起来读更好
侦听器
-
React
React 中只能使用 useEffect 自己封装一个侦听器, useEffect 本身其实就是一个侦听器function useWatch(target, callback) { const oldValue = useRef(); // 这个地方useRef用来保存旧的值 useEffect(() => { callback(target, oldValue.current); oldValue.current = target; }, [target]); }
-
Vue
Vue 中监听器一般用作监听 props 的变化、route 的变化、state 中的数据变化,当监听的数据变化的时候,执行相应的依赖逻辑:<script> export default { props: ["flag"], data() { return {}; }, watch: { flag(newVal, oldVal) { // 监听到父组件传来的 flag 改变时 执行 initData 函数 this.initData(); }, }, methods: { initData() { // do something }, }, }; </script>
class
界面开发时常需要用到 class 属性来修改样式
-
React
React 绑定 class 使用 className 属性:const Demo = () => { return ( <div className="box"> <i className={isCollapse ? "el-icon-s-unfold" : "el-icon-s-fold"}></i> </div> ); };
-
Vue
Vue 使用 class 属性<template> <div class="box"> <i :class="[isCollapse ? 'el-icon-s-unfold' : 'el-icon-s-fold']"></i> </div> </template>
条件渲染
-
React
React 中直接书写 js 实现条件渲染:const Demo = () => { if (hasToken) { return ( <div> <div style={hasToken ? { display: "block" } : { display: "none" }}></div> 登录成功 </div> ); } else { return <div>未登录</div>; } };
-
Vue
Vue 提供 相关指令实现条件渲染<template> <div> <div v-if="hasToken"> <div v-show="hasToken">hello</div> 登录成功 </div> <div v-else>未登录</div> </div> </template>
-
tips:
- v-if 和 v-show 的区别
v-if 直接控制是否渲染;v-show 控制 display 属性,常用于频繁切换展示的情况,不同业务考虑性能以决定使用合适的指令 - v-for 和 v-if 的优先级
v-for 的优先级高于 v-if,应当使用 v-if 嵌套 v-for
- v-if 和 v-show 的区别
列表渲染
出于性能考虑,Vue 和 React 在列表渲染时都需要为子组件提供 key
-
React
React 中使用数组的 map 方法渲染列表:const Demo = () => { const list = [1, 2, 3].map((item) => { return <div key={item.toString()}>{item}</div>; }); return <div>{list}</div>; };
-
tips:
一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串
开发时传入 number 类型也是可以的
-
-
Vue
Vue 当中使用 v-for 渲染列表:<template> <div> <div v-for="item in list" :key="item">{{ item }}</div> </div> </template> <script> export default { data(){ reutrn { list: [1,2,3] } } } </script>
Vue 中的 key 可以是 string 或者 number 类型
事件处理
-
React
React 中使用类似原生的写法,不过是采用驼峰命名:import { Button } from "antd"; const Demo = () => { const handleClick = () => { console.log("click"); }; return ( <div> <Button onClick={handleClick}></Button> </div> ); };
-
tips:
React 中阻止默认只能显示的执行:e.preventDefault
-
-
Vue
Vue 中使用 v-on 指令绑定事件,v-on 可以缩写为 “:”<template> <div> <el-button :click="handleClick"><el-button> </div> </template> <script> export default { data() { return { } }, methods: { handleClick(){ console.log('click') } } } </script>
- tips:
Vue 中阻止默认可以在事件中处理也可以使用修饰符:.prevent(阻止默认) 、.stop(阻止冒泡)
- tips:
表单输入
-
React
- 受控组件:
import {Input, TextArea, Select} from 'antd'; import {useState} from 'react'; const demo = () => { const [inputValue, setInputValue] = useState(''); const [textAreaValue, setTextAreaValue] = useState(''); const [selectValue, setSelectValue] = useState(''); const handleInputChage = () => {}; const handleTextAreaChange = () => {}; const handleSelectChange = () => {}; return ( <div> <Input value={inputValue} onChange={handleInputChage}> <TextArea value={textAreaValue} onChange={handleTextAreaChange}></TextArea> <Select value={selectValue} onChange={handleSelectChange}> <Option value="white">white</Option> <Option value="red">red</Option> <Option value="pink">pink</Option> </Select> </div> ) }
- 非受控组件:
<Input type="file">
因为 file 的 value 是只读的,所以它是一个非受控组件
-
Vue
value 使用 v-model 在表单元素上实现数据双向绑定, 不用再手动监听 input 或者 change 事件
<template> <div> <el-input v-model="inputValue"></el-input> <el-input type="textarea" v-model="textAreaValue"></el-input> <el-select v-model="selectValue"> <el-option value="whilte">white</el-option> <el-option value="pink">pink</el-option> <el-option value="red">value</el-option> </el-select> </div> </template>
-
tips:
注意 .lazy 、.number 、.trim、等修饰符配合 v-model 的用法
-
获取 DOM
虽然 Vue 和 React 已经封装了虚拟 dom,但是在某些场景下,我们还是需要操作 DOM 元素
-
React
React 提供了操作 DOM 的方法:在 class 组件中使用 createRef,在函数组件中使用 useRef Hook-
React.createRef
将引用自动通过组件传递到子组件,常用于可复用的组件库中class MyComponent extends React.Component { constructor(props) { super(props); this.inputRef = React.createRef(); } render() { return <input type="text" ref={this.inputRef} />; } componentDidMount() { this.inputRef.current.focus(); } }
-
useRef
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。
function TextInputWithFocusButton() { const inputEl = useRef < HTMLInputElement > null; const onButtonClick = () => { // `current` 指向已挂载到 DOM 上的文本输入元素 if (inputEl.current) { inputEl.current.focus(); } }; return ( <div> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </div> ); }
-
-
Vue
Vue 中提供 $refs 方法访问 dom, 我们只需要给 Node 添加 ref 属性即可:<template> <div> <el-table ref="userTable"></el-table> </div> </template> <script> export default { data() { return {}; }, mounted() { const userTable = this.$refs.userTable.$el; // ??? $el是怎么来的??? }, }; </script>
组件
注意:不管在 Vue 还是 React 中,自定义组件名都需要大写
React 会将小写字母开头的组件视为原生的 DOM 标签
Vue 中组件的 name 可以使用短横线分隔法或首字母大写命名
-
React
-
函数组件
import { useState } from "react"; const Demo = (props) => { const [data, setData] = useState([]); // 函数组件中使用 useState 初始化 state useEffect(() => {}, []); // 函数组件中使用 useEffect 模拟部分生命周期钩子函数 return <div>Demo</div>; };
-
class 组件
class Demo extends React.Component { constructor(props) { super(props); this.state = { data: {}, }; } // 生命周期钩子函数 componentDidMount() { console.log("did mount"); } render() { return <div>Demo</div>; } }
-
全局组件
由于全局组件很难推理,React 中没有实现全局组件,需要时 import 即可
- 局部组件
函数组件和 class 组件文件都可以很方便的 import 以 创建局部组件
-
-
Vue
-
全局注册组件
Vue.use 或者 Vue.component<script> import Vue from "Vue"; Vue.component("Demo", { data() {}, template: "<div>Demo compoment</div>", }); new Vue({ // ... }); </script>
import Vue from "Vue"; new Vue({ components: { Demo: { data() {}, template: "<div>template</div>", }, }, });
-
局部注册组件
import .Vue 文件import Demo from "./Demo.vue"; export default { components: { Demo, }, };
-
组件间传参数
-
React
-
父子组件传递状态
使用 props:import { useState } from "react"; import { Button } from "antd"; // 父组件 const Parent = () => { const [msg, setMessage] = useState(""); const changeMsg = () => { setMessage("message"); }; return ( <div> <Child msg={msg} changeMsg={changeMsg}></Child> </div> ); }; // 子组件 const Child = (props) => { const handleClick = () => { this.props.ChangeMsg(); }; return ( <div> <div>{props.msg}</div> <Button onClick={handleclick}></Button> </div> ); };
-
跨级组件传递状态
使用 context// 父组件 const Context = React.createContext(); // 测试这里的入参是不是必须的 const Parent = () => { return ( <div> <Context.Provider value={{ name: "ya", age: 18 }} ></Context.Provider> </div> ); }; // 子组件 const Child = () => { const value = useContext(Context); return <div>This is {value.name}</div>; };
-
EventBus
React 借助 events 库实现事件总线
不推荐使用
-
-
Vue
-
父子组件传参数
使用 props:
父组件<template> <Demo :msg="msg" @confirm="handleConfirm"></Demo> </template> <script> export default { data(){ return { msg: [{id:1,content:'a'}, {id: 2}, content: 'b'] } }, methods: { handleConfirm(param){ console.log('子传父的数据', param) } } }; </script>
子组件
<template> <div> <ul> <li v-for="item in msg" :key="item.id">{{ item.content }}</li> </ul> <el-button @click="clickBtn"></el-button> </div> </template> <script> export default { name: "DEMO", props: ["msg"], data() { return {}; }, methods: { clickBtn() { this.$emit("confirm", { params: {} }); }, }, }; </script>
-
跨级组件传递参数
使用 Provide Inject 选项
父组件中:<template> <Demo :msg="msg"></Demo> </template> <script> export default { provide(){ return { handleClick: () => { // do something } } } };
子组件中:
<template> <div> <el-button @click="handleClick"> </el-button> </div> </template> <script> export default { name: "DEMO", inject: ["handleClick"], data() { return {}; }, }; </script>
-
EventBus
不推荐使用
-
插槽
插槽的作用是扩展组件
-
React
React 中没有插槽的概念,要实现插槽使用 props.children// 父组件 const Parent = () => { return ( <div> <div>我是父组件</div> <Child> <div>我是父组件传给子组件的组件</div> </Child> </div> ); }; // 子组件 const Child = (props) => { return ( <div> <div>我是子组件-header</div> <div>{props.children}</div> <div>我是子组件-footer</div> </div> ); };
-
Vue
Vue 中插槽 使用 slot 标签-
匿名插槽(单个插槽)
父组件将一些组件传入到子组件当中
父组件<template> <div> <ChildDemo> <div>插入的内容</div> </ChildDemo> </div> </template> <script> import ChildDemo from "./components/ChildDemo.vue"; export default { name: "App", components: { ChildDemo, }, }; </script>
子组件
<template> <div> <slot> <p>默认内容</p> </slot> </div> </template> <script> export default { name: "ChildDemo", data() { return { data: [], }; }, }; </script>
-
具名插槽(多个插槽)
具名插槽可以从父组件将 node 插入到指定位置,同时插入多个节点- 父组件
<template> <div id="app"> <child-demo> <template v-slot:demoA> <div>aaaaa</div> </template> <template v-slot:demoB> <div>bbbbb</div> </template> </child-demo> </div> </template> <script> import ChildDemo from "./components/ChildDemo.vue"; export default { components: { ChildDemo, }, }; </script>
- 子组件
<template> <div> <slot name="demoA"> <p>默认内容AAA</p> </slot> <slot name="demoB"> <p>默认内容BBB</p> </slot> </div> </template> <script> export default { name: "ChildDemo", data() { return { data: [], }; }, }; </script>
如果使用 child 组件的时候,只传入了 demoA, 那 demoB 就会渲染默认内容。
-
作用域插槽
作用域插槽是用来传递数据的插槽。子组件将数据传递到父组件
将数据作为 v-slot 的一个特性绑定在子组件上传递给父组件,数据在子组件中更新的时候,父组件接收到的数据也会一同更新。
父组件:<template> <div id="app"> <child-demo> <template v-slot:demoA> <div>aaaaa</div> </template> <template v-slot:demoB="{ dataB }"> <div>bbbbb {{ dataB }}</div> <ul v-for="item in dataB" :key="item"> <li>{{ item }}</li> </ul> </template> </child-demo> </div> </template> <script> import ChildDemo from "./components/ChildDemo.vue"; export default { components: { ChildDemo, }, }; </script>
子组件:
<template> <div> <slot name="demoA"> <p>默认内容AAA</p> </slot> <slot name="demoB" :dataB="data"> <p>默认内容BBB</p> </slot> <button @click="handleClick">add</button> </div> </template> <script> let count = 4; export default { name: "ChildDemo", data() { return { data: [1, 2, 3], }; }, methods: { handleClick() { this.data.push(count++); console.log(this.data); }, }, }; </script>
-
状态管理
-
React
React 的状态管理工具比较多,常用的以下三种: Redux、 Mobx、 Dva、Reduxjs/toolkit
这里使用 reduxjs/toolkit 为例(对另外三种感兴趣的小伙伴自行查看官方 API,按照文档接入很简单):-
安装:
npm i react-redux npm i @reduxjs/toolkit
-
store:
import { configureStore } from "@reduxjs/toolkit"; import { TypedUseSelectorHook, useDispatch, useSelector, } from "react-redux"; import counterSlice from "./features/counterslice"; // 使用redux-persist import storage from "redux-persist/lib/storage"; import { combineReducers } from "redux"; import { persistReducer } from "redux-persist"; // 持久化存储 import thunk from "redux-thunk"; const reducers = combineReducers({ counter: counterSlice, }); const persistConfig = { key: "root", storage, }; const persistedReducer = persistReducer(persistConfig, reducers); // 创建一个redux数据 const store = configureStore({ reducer: persistedReducer, devTools: true, // 注意 调试模式不能在生产环境使用 middleware: [thunk], }); export default store;
-
slice:
import { createSlice } from "@reduxjs/toolkit"; export const counterSlice = createSlice({ name: "counter", initialState: { count: 1, title: "redux toolkit pre", }, reducers: { increment(state, { payload }) { state.count = state.count + payload.step; }, decrement(state) { state.count -= 1; }, }, }); // 导出actions export const { increment, decrement } = counterSlice.actions; //内置了thunk插件可以直接处理异步请求 export const asyncIncrement = (payload: any) => (dispath: any) => { setTimeout(dispath(increment(payload)), 2000); }; export default counterSlice.reducer;
-
访问store中的数据
import { useSelector } from "react-redux"; const {counter} = useSelector(state => state.counter);
-
更新store中的数据
import { useDispatch } from "reactRedux"; import increment from './slice.js' const dispath = useDispath(); dispath(increment({step: newStep}));
-
-
Vue
Vue2 的状态管理工具 Vuex,Vue3 适用的状态管理工具 Pinia( Pinia 也可以在 vue2 中使用)。
这里介绍 Vuex 的使用:-
state 用于保存存储在 store 中的数据
-
getters 用于获取 store 中的数据
-
mutations 用于修改更新 store 中的数据 只能执行同步操作
-
actions Action 函数接受一个和 state 有相同方法和属性的 context 对象 可以执行异步操作
-
modules 将 store 分割成多个模块 避免一个对象过于臃肿
-
引入 vuex
import { createStore } from "vuex"; import Vue from "vue"; const store = createStore({ state: { count: 0, todos: [ { id: 1, done: true }, { id: 2, done: false }, ], }, getters: { donetodos(state) { return state.filter((todo) => todo.done); }, }, mutations: { increment(state) { state.count++; }, }, actions: { increment(context) { context.commit("increment"); }, }, }); Vue.use(store);
-
在组件中访问 store:
console.log(this.$store);
-
在组件中访问 getters:
const doneTodos = this.$store.getters.doneTodos();
-
在组件中提交 mutations:
this.$store.commit("increament");
-
在组件中提交 actions:
Action 通过 store.dispatch 方法触发this.$store.dispatch("increment");
-
多个 modules:
const moduleA = { state: () => ({ ... }), mutations: { ... }, actions: { ... }, getters: { ... } } const moduleB = { state: () => ({ ... }), mutations: { ... }, actions: { ... } } const store = createStore({ modules: { a: moduleA, b: moduleB } }) store.state.a // -> moduleA 的状态 store.state.b // -> moduleB 的状态
-
- tips:
由于刷新后 store 中的数据会重新初始化,所以我们可能需要持久化存储:vuex 搭配使用 vuex-persistedstate,redux 搭配使用 redux-persist。当然我们也可以结合原生的 Storage 来实现。
由于内容太多了,这里不再展开,大家可以自行跳转查看使用方法
逻辑复用
-
React
class 组件中使用 mixin(已被弃用,使用方式类似 Vue 中的 mixin)、render props(渲染属性) 、 高阶组件(HOC)
-
HOC:
高阶组件是一个函数,入参是一个待包装的组件,返回值是一个包装后的组件。-
HOC 的封装:
import React , {Component} from 'react'; export default (WrappedComponent) => { return class extends Component { constructor(props){ super(props); this.state = {count: 0}, } componentDidMount() {} const setCount = (newCount) => { this.setState({count: newCount}); } render(){ return ( <div> <WrappedComponent state={this.state} setCount={this.getCount} {...this.props}></WrappedComponent> </div> ) } } }
-
HOC 的使用:
import HOC from "./HOC.jsx"; import { Button } from "antd"; const SourceComponet = (props) => { return ( <div> <div>count: {props.state.count}</div> <Button onClick={() => props.setCount(props.state.count + 1)} ></Button> </div> ); }; const Demo = HOC(SourceComponent);
我们可以写多个 Demo 组件,其中的 count 都是独立的互不影响
-
tips:
高阶组件的可读性比较差、嵌套深
-
-
render props:
render prop 是指一种在组件之间使用一个值为函数的 prop 共享代码的简单技术(解决横切关注点问题)
将子组件当作 prop 传入,子组件可以获取到参数中的数据。
// Cat组件 展示一只猫咪图片 图片位置随着传人的prop值变化 class Cat extends React.Component { render() { const mouse = this.props.mouse; return ( <img src="/cat.jpg" style={{ position: "absolute", left: mouse.x, top: mouse.y }} /> ); } } // Mouse组件 监听鼠标位置变化 将鼠标位置数据传给 render prop class Mouse extends React.Component { constructor(props) { super(props); this.handleMouseMove = this.handleMouseMove.bind(this); this.state = { x: 0, y: 0 }; } handleMouseMove(event) { this.setState({ x: event.clientX, y: event.clientY, }); } render() { return ( <div style={{ height: "100vh" }} onMouseMove={this.handleMouseMove}> {/* 使用 `render`prop 动态决定要渲染的内容, 而不是给出一个 <Mouse> 渲染结果的静态表示 */} {this.props.render(this.state)} </div> ); } } // MouseTracker组件 包含 Mouse组件,将Cat组件当作prop传入Mounse组件 class MouseTracker extends React.Component { render() { return ( <div> <h1>移动鼠标!</h1> <Mouse render={(mouse) => <Cat mouse={mouse} />} /> </div> ); } }
此例中我们复用的是 Mouse 组件中的逻辑
tips:
- render prop 只是一种模式,不是一定要使用名为 render 的 prop 来实现这种模式
- 日志功能是一个典型的横切关注点(横切代码中做个模块的关注点(系统的部分功能))案例
-
Hooks
-
函数组件中使用 Hook
使用自定义 hook 实现真正的逻辑复用:
-
定义一个通用的 useRequest hook:
import { useEffect, useState } from "react"; import { Get } from "../api/server"; function useRequest(url: string) { const [data, setData] = useState({}); const [err, setErr] = useState({}); const [loading, setLoading] = useState(false); useEffect(() => { const requestData = async () => { setLoading(true); const res: { [key: string]: any } = await Get(url); console.log("resRequest :", res); if (res[1].code === "000000") { setData(res[1].data); } else { setErr(res[1].description); } setLoading(false); }; requestData(); }, []); return [data, loading, err]; } export default useRequest;
-
使用:
import useRequest from "./hooks/useRequset"; const Demo = () => { const [data, loading] = useRequst("/admin-service/api/v1/dictionaries"); const [data2, loading2] = useRequst( "/admin-service/api/v1/factory_menu" ); return ( <div> <Table loading={loading}></Table> <Table loading={loading2}></Table> </div> ); };
- tips:
- 自定义 hook 使用 use 开头
- 自定义 hook 的经典案例就是 request 方法的封装
-
-
-
Vue
Vue 中使用 mixin 实现逻辑复用:-
声明:
export default { methods: { async getUserInfo() { // get info }, }, };
-
使用:
<script> import userinfoMixin from './userinfoMixin'; export default { mixins: ['userinfoMixin'], data(){ return { } }, mounted(){ const userinfo = await this.getUserInfo(); } } </script>
-
css 文件引入
-
React
import "@/static/css/demo.less";
-
Vue
<style> @import url("./static/csss/demo.less"); </style>
@import "./static/csss/demo.less";
图片导入
-
React
通过 import 的方式引入:import avatar from "@/static/img/avatar.png"; import { Avatar } from "antd"; const Demo = () => { return ( <div> <Avatar src={avatar}></Avatar> </div> ); };
-
Vue
通过 import 的方式引入:<template> <div> <img :src="avatar" /> </div> </template> <script> import avatar from "@/static/img/avatar.png"; </script>
scoped css
scoped css 是指只作用于当前组件的 CSS。局部作用域的 CSS 在开发中非常重要,搞清楚 css 的作用域,可以有效避免样式混乱、相互覆盖。
-
React
-
行内样式
不推荐这种方式,会让代码可读性变差const Demo = () => { return ( <div style={{ width: 100, height: 100 }}> <Button style={{ color: "red" }}></Button> </div> ); };
-
styled-components
styled-components 是一个第三方库,可以实现 scoped css:import styled from "styled-components"; export const Demo = styled.div` width: 100px; height: 100px; button: { colof: red; } `;
-
-
Vue
Vue 在 Style 标签中支持 scoped 属性,以实现局部作用域的 css:<style scoped> .box { width: 100px; height: 100px; background-color: #000000; } </style>
style 标签中的 css 只对当前组件的元素起作用,不会污染到外部组件
不会渲染到真实 DOM 的标签
为了性能考虑通常我们不希望渲染过多的 DOM 节点,不会渲染到真实 DOM 的标签帮助我们将子列表分组,而无需向 Dom 中添加额外的节点:
-
React
const Demo = () => { return ( <div> <React.Fragment> <Child1></Child1> <Child2></Child2> <Child3></Child3> </React.Fragment> </div> ); };
-
Vue
<template> <div>node</div> </template>
template 标签 上可以使用插槽; 也可以用于循环
router
-
React
React 中我们使用 react-router 、react-router-dom 实现
在 index.tsx 中引入 BrowserRouter:import React from "react"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; const root = ReactDOM.createRoot( document.querySelector("#root") as HTMLElement ); root.render( <BrowserRouter> <App /> </BrowserRouter> );
在 App.jsx 文件中声明可用的 routes:
import { Routes, Route } from "react-router-dom"; import M1 from "./M1"; import M2 from "./M2"; import M3 from "./M3"; const App = () => { return ( <div> <Routes> <Route path="/" element={<Home />}></Route> <Route path="/a" element={<M1 />}></Route> <Route path="/b" element={<M2 />}></Route> <Route path="/c" element={<M3 />}></Route> </Routes> </div> ); };
使用 menu 组件实现点击 menu 切换路由:
import { useNavigate, useLocation } from "react-router-dom"; // 路由跳转方法 const navigate = useNavigate(); // 当前 path 用于绑定到 Menu 组件的 defaultOenKey const { pathname } = useLocation(); // 点击 menuItem const clickItem = (param: ItemObj) => { navigate(param.key); };
- tips:
react-router-dom 是对 react-router 的扩展,新增了 DOM 操作的 API,提供了更多好用的 API
- tips:
-
vue
Vue 中我们使用 vue-router 实现-
在 index.js 中引入
import VueRouter from "vue-router"; let routes = [ { path: "/home", name: "", }, ]; let Routes = new VueRouter({ mode: "history", routes: routes, }); Vue.use(Routes); // use方法的作用全局注入插件 new Vue({ router: routes, });
-
实现路由跳转:
this.$router.push({ path: "/a" });
-
组件状态缓存 keep-alive
-
React
React 官方没有提供 keep-alive 的组件,不过提供了 Portal Api 我们可以自己封装,这里使用第三方的库 [reat-activation] 来实现import React, { useState } from "react"; import { Input } from "antd"; import KeepAlive from "react-activation"; const Keep = () => { const [inputValue, setInputValue] = useState(""); const changeInput = (e) => { setInputValue(e.target.value); }; return ( <div> <div>{inputValue}</div> <Input placeholder="keepalive this value" value={inputValue} onChange={changeInput} ></Input> </div> ); }; const KP = () => { return ( <div> <KeepAlive> <Keep></Keep> </KeepAlive> </div> ); }; export default KP;
AliveScope 放在应用入口处,一般放在 Router 和 Provider 内部
import { AliveScope } from "react-activation"; const root = ReactDOM.createRoot( document.querySelector("#root") as HTMLElement ); root.render( <AliveScope> <App /> </AliveScope> );
-
Vue
Vue 提供 keep-alive 组件实现组件状体缓存<template> <div> <keep-alive :max="10" :include="[]" > <router-view> </keep-alive> </div> </template>
总结
鉴于这一篇已经过 2 万字了,篇幅过长会导致阅读体验不佳,原理对比的部分放在下一篇再写吧(如果有人看的话)。有兴趣的小伙伴可以关注一下!
希望每一位同学都能顺利的学会 React 和 Vue,Give me five!
关注小姐姐,一起学一学,欢迎大家批评指正,共同进步!