Vue2 到 React 一遍过

很多同学说自己只会 Vue 不会 React,相信我,React 没那么难,只需要花 2 天时间看完这篇对比教程,马上你就能上手开发啦。
由于近两年工作中一直使用 Vue2,React 知识快忘了。现在市场行情不好,为了减少焦虑,准备捡起来用用。

注意:

  1. 文章对比的是 Vue2 和 React18
  2. 多数 React 案例使用 Hooks
  3. 默认 Vue 项目使用的 UI 框架是 elementUI,默认 React 使用的 UI 框架是 antDesign
  4. 文章适用于 Vue 转学 React 或者 React 想学 Vue2 的同学(最好有其中一种框架的开发经验)

前置工作

在转换之前建议大家先复习一遍 Vue 中的基础知识,然后过一遍 React 中的概念(官网是是最好的学习资料),学习官方文档让我们更容易实现无痛转换。

Vue 官网

React 官网

Let`s go !

对比目录

  • 安装
  • 脚手架的使用
  • 实例
  • 常见的 UI 库组合
  • 生命周期
  • 模版语法
  • 计算属性
  • 侦听器
  • class
  • 条件渲染
  • 列表渲染
  • 事件处理
  • 表单输入
  • 获取 DOM
  • 组件
  • 组件间传参数
  • 状态管理器
  • 逻辑复用
  • scoped css
  • 不会渲染到真实 DOM 的标签
  • 虚拟 DOM 对比算法
  • router
  • keep-alive

基础知识对比

安装

  • React

    1. 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>
    
    1. npm 方式引入
    npm i react react-dom
    
  • Vue

    1. script 引入
    <!-- 适用于开发环境 -->
    <script src="https://cdn.jsdelivr.net/npm/Vue@2.7.8/dist/Vue.js"></script>
    
    1. npm 方式引入
    npm install vue
    

脚手架

  • React

    1. 安装脚手架:
    npm install create-react-app
    
    1. 使用脚手架创建一个 demo 项目:
    npx create-react-app myApp
    
  • Vue

    1. 安装脚手架:
    npm install -g @vue/cli
    # OR
    yarn global add @vue/cli
    
    1. 使用脚手架创建一个 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。

      1. useEffect 相当于 componentDidMount、componentDidUpdate、componentWillUnMount 三个生命周期钩子的合并方法
      2. 模拟 componentDidMount
        useEffect(fn,[])的第二个参数传空数组,相当于 componentDidMount
      3. 模拟 componentDidUpdate
        useEffect(fn,[dep])的第二个参数数组中传入依赖参数,相当于 componentDidUpdate
      4. 模拟 componentWillUnMount
        useEffect(() => { return callback}, [])的第一个参数函数返回的 callback 函数会在组件卸载前被调用,相当于 componentWillUnMount
  • Vue
    分为:创建阶段、挂载阶段、更新阶段、卸载阶段
    常见的生命周期钩子函数:

    • 创建阶段:
      • beforeCreate
      • created
    • 挂载阶段:
      • beforeMount
      • Mounted (初始页面依赖的数据 ajax 请求放在这里)
    • 更新阶段:
      • beforeUpdate
      • updated
    • 销毁阶段:
      • beforeDestroy (在这里取消事件监听)
      • destroied

模版语法

  • React

    jsx 语法

    1. 文本
    const Demo = () => {
      return <div>{message}</div>;
    };
    
    1. html-innerHTML

    使用富文本编辑器产生的数据,回到页面时也要按 html 来展示,React 中没有像 Vue 一样的 v-html 指令,但是提供了 dangerouslySetInnerHTML 属性

    function MyComponent() {
      const html = "<div>innerHTML</div>";
      return <div dangerouslySetInnerHTML={{ __html: html }} />;
    }
    

    注意这里的用法比较特殊

    1. style

    style 接收一个对象,width 和 height 的单位 px 可以省略
    style 多用于添加动态样式 不推荐直接将 css 属性写在里面 可读性差
    style 中的 key 使用小驼峰命名法

    const Demo = () => {
      return <div style={{ width: 100, height: 100, fontSize: "16px" }}></div>;
    };
    
    1. 属性
    const Demo = () => {
      return <div loading={loading}></div>;
    };
    
    1. js 表达式

    jsx 中可以书写任意表达式,甚至可以使用 map:

    const Demo = () => {
      let flag = true;
      return <div>{flag ? message : "暂无数据"}</div>;
    };
    
  • Vue

    基于 HTML 的模版语法

    1. 文本

    Mustache 双大括号语法

    数据绑定放在 双大括号 里面

    <template>
      <div>{{ message }}</div>
    </template>
    
    1. html
    <template>
      <div v-html="html"></div>
    </template>
    <script>
      export default {
        data() {
          return {
            html: `<div>v-html directive</div>`,
          };
        },
      };
    </script>
    
    1. style
    <template>
      <div style="width: 100px;height:100px">{{ message }}</div>
      <div :style={width: 100px;heihgt: 200px}></div>
    </template>
    
    1. 属性

    v-bind

    <template>
      <div v-bind:loading="loading"></div>
    </template>
    

    Vue 中 v-bind 指令 可以缩写为冒号 “:”

    1. js 表达式

    Vue 的模版语法中,只能包含单个表达式:

    <template>
      <div>{{ flag ? message : "暂无数据" }}</div>
    </template>
    <script>
    export default {
        data(){
          return {
              flag: false,
          }
        }
    }
    </script>
    
    1. 动态参数
    <template>
      <div v-bind[attributeName]="url">动态参数</div>
    </template>
    
    1. 修饰符
      • .stop 阻止冒泡 相当于 event.stopPropagation

      • .prevent 阻止默认 相当于event.preventDefault()

      • .lazy 数据绑定放在 change 事件之后

      • .number 自动转换用户输入为 number 类型

      • .trim 自动去除用户输入的头尾空白字符

      • .native 将原生事件绑定到组件

      • .sync 对一个 prop 进行 “双向绑定”

    • tips:
      Vue 也支持 jsx 语法,这里仅用模版语法做对比
      Vue 中 template 标签内只能有一个根节点,每个组件必须只有一个根元素

计算属性

计算属性的常见使用场景是:

  1. 对复杂逻辑的抽象
  2. 缓存计算结果
  • 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 可以缓存计算结果或缓存组件
  • 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>
    

    平时开发的时候很多同学的用法是:

    1. 兼容边界值
    return data || [];
    
    1. 封装一个长的调用
    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:

    1. v-if 和 v-show 的区别
      v-if 直接控制是否渲染;v-show 控制 display 属性,常用于频繁切换展示的情况,不同业务考虑性能以决定使用合适的指令
    2. v-for 和 v-if 的优先级
      v-for 的优先级高于 v-if,应当使用 v-if 嵌套 v-for

列表渲染

出于性能考虑,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(阻止冒泡)

表单输入

  • 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

    1. 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();
        }
      }
      
    2. 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

    1. 函数组件

      import { useState } from "react";
      const Demo = (props) => {
        const [data, setData] = useState([]); // 函数组件中使用 useState 初始化 state
        useEffect(() => {}, []); // 函数组件中使用 useEffect 模拟部分生命周期钩子函数
        return <div>Demo</div>;
      };
      
    2. class 组件

      class Demo extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            data: {},
          };
        }
        // 生命周期钩子函数
        componentDidMount() {
          console.log("did mount");
        }
        render() {
          return <div>Demo</div>;
        }
      }
      
    3. 全局组件

    由于全局组件很难推理,React 中没有实现全局组件,需要时 import 即可

    1. 局部组件

    函数组件和 class 组件文件都可以很方便的 import 以 创建局部组件

  • Vue

    1. 全局注册组件
      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>",
          },
        },
      });
      
    2. 局部注册组件
      import .Vue 文件

      import Demo from "./Demo.vue";
      export default {
        components: {
          Demo,
        },
      };
      

组件间传参数

  • React

    1. 父子组件传递状态
      使用 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>
        );
      };
      
    2. 跨级组件传递状态
      使用 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>;
      };
      
    3. EventBus
      React 借助 events 库实现事件总线
      不推荐使用

  • Vue

    1. 父子组件传参数
      使用 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>
      
    2. 跨级组件传递参数
      使用 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>
      
    3. 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 标签

    1. 匿名插槽(单个插槽)
      父组件将一些组件传入到子组件当中
      父组件

      <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>
      
    2. 具名插槽(多个插槽)
      具名插槽可以从父组件将 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 就会渲染默认内容。

    3. 作用域插槽
      作用域插槽是用来传递数据的插槽。子组件将数据传递到父组件
      将数据作为 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 的使用:

    1. state 用于保存存储在 store 中的数据

    2. getters 用于获取 store 中的数据

    3. mutations 用于修改更新 store 中的数据 只能执行同步操作

    4. actions Action 函数接受一个和 state 有相同方法和属性的 context 对象 可以执行异步操作

    5. 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:

      1. render prop 只是一种模式,不是一定要使用名为 render 的 prop 来实现这种模式
      2. 日志功能是一个典型的横切关注点(横切代码中做个模块的关注点(系统的部分功能))案例
    • Hooks

      1. 函数组件中使用 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:
        1. 自定义 hook 使用 use 开头
        2. 自定义 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

    1. 行内样式
      不推荐这种方式,会让代码可读性变差

      const Demo = () => {
        return (
          <div style={{ width: 100, height: 100 }}>
            <Button style={{ color: "red" }}></Button>
          </div>
        );
      };
      
    2. 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
  • 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!
关注小姐姐,一起学一学,欢迎大家批评指正,共同进步!