前端两大主流框架对比之-(vue - react)

858 阅读12分钟

类同

对比.png 注:以上括号内是本文示例中用到的版本号

简介

  • vue Vue 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
  • react React 是一个声明式,高效且灵活的用于构建用户界面的 JavaScript 库。使用 React 可以将一些简短、独立的代码片段组合成复杂的 UI 界面,这些代码片段被称作“组件”。

生命周期

生命周期2.webp

  • vue

vue生命周期.jpg

  • react

react生命周期2.png v16.3引入了两个新的生命周期函数:

  1. static getDerivedStateFromProps(props, state) 在组件创建时和更新时的render⽅法之前调⽤,它应该返回⼀个对象来更新状态,或者返回null来不更新任何内容。
  2. getSnapshotBeforeUpdate(prevProps, prevState) 在最近⼀次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发⽣更改之前从 DOM中捕获⼀些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()。 可能会废弃的三个生命周期,现在使用需要加前缀“UNSAFE_”
  3. componentWillMount
  4. componentWillReceiveProps
  5. componentWillUpdate 用一个静态函数getDerivedStateFromProps来取代被deprecate的几个生命周期函数,就是强制开发者在render之前只做⽆副作用的操作,⽽且能做的操作局限在根据props和state决定新的state

响应式

  1. 数据 -> 视图 响应式.png vue直接修改data即可。还可以通过Object.freeze()阻止响应
    react需要通过setState来修改。
    vue更简洁

原理:

  • vue:

vue响应式原理.png

初始化的时候通过Object.defineProperty递归地把 data 的 property 转换为 getter/setter ,获取数据时收集依赖,修改数据时通知依赖更新。
Vue在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。需要操作更新后的dom必须在nextTick中执行。
注意:对象属性的新增和删除、数组的部分修改无法劫持到,需要使用特定的方法。

  • react:

react setState原理.png

在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新 this.state 还是放到队列中回头再说,而 isBatchingUpdates 默认是 false,也就表示 setState 会同步更新 this.state,但是,有一个函数batchedUpdates,这个函数会把 isBatchingUpdates 修改为 true,而当 React 在调用事件处理函数之前就会调用这个 batchedUpdates,造成的后果,就是由 React 控制的事件处理过程 setState 不会同步更新 this.state。
setState只有在合成事件和钩⼦子函数中是异步的,在原⽣事件和setTimeout、setInterval中都是同步的。
注意:如上图,引用类型不能直接修改,要换一个引用,这样才会触发更新

  1. 视图 -> 数据
  • vue 提供语法糖v-model
    而且提供修饰符lazy、number、trim
  • react 需绑定事件处理后通过setState更新相应的state

语法、API

以下通过具体的demo来比较他们在语法、API、风格等方面的差别
注:以下代码在项目中的配置都是:webpack5 + babel7

项目结构

项目结构对比.png

入口js indexjs.png

根组件 App.png

首页 Home.png

路由,vue搭配vue-router,react搭配react-router-dom router.png

全局状态,vue搭配vuex,react搭配redux,通过react-redux连接 store.png

vue需要一个特定的vue-loader webpack.png

组件

组件.png

调用方(父组件)

组件调用.png vue自定义组件上的v-model相当于:

<my-input :value="theName" @change="changeTheName"></my-input>

methods: {
    changeTheName(val) {
        this.theName = val
    },
}

组件通信

组件通信.png 其他:

  • vue $attrs + $listeners、 new Vue()、 vuex
  • react redux

props写法

  • vue 传递变量值需使用 v-bind: 或 : ,事件绑定或自定义事件使用 v-on: 或 @
<Child name="jeni" :count="count" @change={handleChange} />

支持动态参数

<a v-bind:[attributeName]="url"> ... </a>
  • react
<Child count={3} onChange={handleChange}/>

获取更新后数据

  • vue
data() {
    return {
        count: 0,
    };
},
methods: {
    handleClick() {
        this.count += 3;
        console.log('count: ', this.count); // 3
    },
},
  • react
const [count, setCount] = useState(0);

const handleClick = () => {
    setCount(count + 3);
    console.log('count: ', count); // 0
}

useEffect(() => {
    console.log('count2: ', count); // 3
}, [count]);

因为setState在react事件中是异步的,所以同步去获取还是旧的,需要在useEffect中取最新值
setCount直接传值,其中用到count,可能不是最新的。需要改成传递函数,该函数的参数就是最新的count

setCount(c => c + 3);

vue指令

  1. 条件
  • vue
<div v-if="theName.length === 0">1</div>
<div v-else-if="theName.length <= 5">2</div>
<div v-else>3</div>
  • react
{theName.length === 0 && <div>1</div>}
{theName.length !== 0 && theName.length <= 5 && <div>2</div>}
{theName.length !== 0 && theName.length > 5 && <div>3</div>}

或者

{theName.length === 0 ? 
(<div>1</div>) 
: 
(theName.length <= 5 ? (<div>2</div>) : (<div>3</div>))}
  1. 遍历
  • vue 当 Vue 正在更新使用 v-for 渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。

建议尽可能在使用 v-for 时提供 key attribute,因为它是 Vue 识别节点的一个通用机制。

<div v-for="(item, index) in users" :key="item.id" class="list-item">
    <div>序号:{{index}}</div>
    <div>姓名:{{item.name}}</div>
    <div>年龄:{{item.age}}</div>
</div>

<span v-for="n in 10">{{ n }} </span>
  • react
{users.map((item, index) => (
    <div key={item.id} className={styles.listItem}>
        <div>序号:{index}</div>
        <div>姓名:{item.name}</div>
        <div>年龄:{item.age}</div>
    </div>
))}

注意都要加key,方便做diff运算时快速得知两个节点是否是同一个节点,并保证key的唯一性!

  1. v-show
  • vue
<div v-show="theName.length > 10">theName的值长度超过10才显示</div>
  • react
<div style={{display: theName.length > 10 ? 'block' : 'none'}}>
    theName的值长度超过10才显示
</div>
  1. v-html
  • vue
<div v-html="htmlContent"></div>
<div>{{htmlContent}}</div>

htmlContent: '<h1>我是h1</h1>'
  • react
<div dangerouslySetInnerHTML={{__html: htmlContent}} />
<div>{htmlContent}</div>

const [htmlContent] = useState('<h1>我是h1</h1>')
  1. 其他内置指令
  • v-model
  • v-text
  • v-bind
  • v-on
  • v-cloak
  • v-pre
  • v-once
  1. 自定义指令
  • vue
<input value="页面初始化时自动聚焦"  v-focus />

directives: {
    focus: {
        bind: function() {
            // 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
        },
        /**
         * el: 指令所绑定的元素
         * vnode: Vue 编译生成的虚拟节点
         */
        inserted: function (el, binding, vnode, oldVnode) {
            // 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
            el.focus();
            const {
                name, // 指令名,不包括 v- 前缀
                value, // 指令的绑定值
                oldValue, // 指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用
                expression, // 字符串形式的指令表达式
                arg, // 传给指令的参数
                modifiers, // 一个包含修饰符的对象
            } = binding;
        },
        update: function() {
            // 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。
            // 指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。
        },
        componentUpdated: function() {
            // 指令所在组件的 VNode 及其子 VNode 全部更新后调用。
        },
        unbind: function() {
            // 调用一次,指令与元素解绑时调用。
        },
    }
},

以上代码是局部指令,只可以在当前组件用,还可以定义全局指令

  • react 模拟实现
<input value="页面初始化时自动聚焦" ref={inputRef} />

const inputRef = useRef(null);

useEffect(() => {
    //类似vue directive中的inserted
    inputRef.current.focus();
    return () => {
        // 类似vue directive中的unbind
    };
}, []);
useEffect(() => {
    // 类似vue directive中的componentUpdated
});

事件

  • vue 在模板中通过指令 v-on: 或简写 @ 绑定事件
    支持修饰符
<form v-on:submit.prevent="onSubmit">...</form>

修饰符还有stop、capture、self、once、passive(不想阻止事件的默认行为),可以串联。
还有按键修饰符:enter、page-down、tab、delete (捕获“删除”和“退格”键)、esc、space、up、down、left、right
还有系统修饰符:ctrl、alt、shift、meta
其他: .exact 修饰符允许你控制由精确的系统修饰符组合触发的事件。

  • react 在JSX中通过 onClick={handleClick} 绑定事件
    参数e是合成事件SyntheticEvent,没有修饰符,要阻止默认行为或冒泡等等要在绑定的回调函数中具体处理

是否立即执行?

  • vue
<div @click="add()"></div>

初始化时不会执行一次add函数

  • react
<div onClick={add()}></div>

初始化时会执行一次add函数
所以如果需要传递参数,需要改写成

<div onClick={e => {add(e, name)}}></div>

计算属性

  • Vue 基于响应式依赖进行缓存。只在相关响应式依赖发生改变时它们才会重新求值。
computed: {
    theName2() {
        return this.theName + '2';
    },
},
  • React
const theName2 = useMemo(() => {
    return theName + '2';
}, [theName])

侦听属性

  • Vue 在数据变化时执行异步或开销较大的操作时
watch: {
    theName: function(newValue, oldValue) {
        if(newValue - oldValue > 8) {
            alert('theName新值比旧值大8');
        }
    },
},
  • React
const theNameRef = useRef(theName);
useEffect(() => {
    if(theName - theNameRef.current > 8) {
        alert('theName新值比旧值大8');
    }
    theNameRef.current = theName;
}, [theName])

class

  • vue
<div class="container" :class="{active: isActive}">
  • react
<div className={styles.container}>

style

  • vue
<div style="width: 20px;height: 20px;">diff2</div>
<div :style="{color: textColor}">diff3</div>

当 v-bind:style 使用需要添加浏览器引擎前缀的 CSS property 时,如 transform,Vue.js 会自动侦测并添加相应的前缀。

  • react
<div style={{width: '20px', height: '20px'}}>diff2</div>
<div style={{color: textColor}}>diff3</div>

prop验证

vue:不仅为你的组件提供了文档,还会在它们遇到错误的类型时从浏览器的JavaScript 控制台提示用户。
react:还需要单独引入一个包:prop-types

prop类型检查.png

插槽

  • vue
// 父组件
<slot-test>
    I am slot content
</slot-test>

// 子组件
<template>
  <div>
      I am SlotTest.vue
      <div>
          <slot></slot>
      </div>
  </div>
</template>

这是最简单的,还有具名插槽、作用域插槽

  • react
// 父组件
<SlotTest>
    I am slot content
</SlotTest>

// 子组件
<div>
    I am SlotTest.jsx
    <div>{props.children}</div>
</div>

动态组件

  • vue
<component :is="currentComponent"></component>

适用场景:

  1. tab切换
  2. 有些 HTML 元素,诸如 <ul>、<ol>、<table> 和 <select>,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li>、<tr> 和 <option>,只能出现在其它某些特定的元素内部。
  • react
const components = [Parent, SlotTest];

const CurrentComponent = components[0];

<CurrentComponent />

异步组件

  • vue 路由配置中
{
    path: '/list',
    component: () => import(/* webpackChunkName: "list" */ '../pages/list'),
},
{
    path: '/diff',
    component: r => require(['../pages/diff'], r),
},
  • react
{
    path: '/list',
    component: lazy(() => import('../pages/list')),
},
// 必须在组件外面包一层 <Suspense fallback="<div>...</div>">

组件缓存

  • vue <keep-alive> 组件实例能够被在它们第一次被创建的时候缓存下来
  • react 无

混入

mixins
react基本废弃,推荐使用 HOC(高阶组件)或 Render Props

插件

  • vue 插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:
  1. 添加全局方法或者 property。如:vue-custom-element
  2. 添加全局资源:指令/过滤器/过渡等。如 vue-touch
  3. 通过全局混入来添加一些组件选项。如 vue-router
  4. 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。
  5. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router 使用:
    Vue.use(插件名,选项) 开发:
MyPlugin.install = function (Vue, options) {...}

vue-router和vuex都是插件

  • react 无

过滤器

  • vue 可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示
{{ message | filterA | filterB }}

先调用filterA,再将结果传入filterB作为参数,如果filterA传递了参数,那么message将作为第一个参数

  • react 方法调用

组件之间相互访问

  • vue
  1. $root
  2. $parent
  3. $children
  4. $refs
  • react forwardRef

单元素/组件的过渡

  • vue
<transition name="fade">
    <p v-if="show">hello</p>
</transition>
  • react 无

模板根节点

必要要有根节点

  • vue 只能是实际的html节点,3版本有提供虚拟节点
  • react 可以是虚拟节点React.Fragment或<></>减少页面上不必要的标签

其他

  1. vue不支持模板部分注释

vue坑

  1. 接入ts成本高 代码改造量大。

  2. 很多全局的东西(如Vue.extend、Vue.mixin、Vue.prototype)容易造成混乱,不易维护

vue亮点

  1. 提供keep-alive,多组件切换非常实用

  2. 内在优化及众多语法糖和API,对开发者更友好

react坑

  1. 条件渲染:当条件不是boolean类型且值转为boolean为false时,会显示相应的值
const isShow = 0

{isShow && <div>1234567890</div>}

以上会显示 ‘0’

  1. 异步请求返回后setState,可能报提示:组件已卸载,不能setState。因为可能其他state变化导致组件重渲染

  2. 很多重复无用的diff运算开销,需要手动优化 当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树。
    如要避免不必要的子组件的重渲染,你需要在所有可能的地方使用 PureComponent,或是手动实现 shouldComponentUpdate 方法,函数式组件使用memo包裹。同时你可能会需要使用不可变的数据结构来使得你的组件更容易被优化。

vue组件的依赖是在渲染过程中自动追踪的,所以系统能精确知晓哪个组件确实需要被重渲染。

  1. 引用类型只改了值没改引用,不会触发更新

react亮点

  1. fiber Fiber 是 React 16 中新的协调引擎。它的主要目的是使 Virtual DOM 可以进行增量式渲染。
    为解决主线程长时间被 JS 运算占用而存在,是将运算切割为多个步骤,分批完成。
    旧版 React 通过递归的方式进行渲染,使用的是 JS 引擎自身的函数调用栈,它会一直执行到栈空为止。而Fiber实现了自己的组件调用栈,它以链表的形式遍历组件树,可以灵活的暂停、继续和丢弃执行的任务。实现方式是使用了浏览器的requestIdleCallback这一 API。

window.requestIdleCallback()会在浏览器空闲时期依次调用函数,这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这些延迟触发但关键的事件产生影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间。

  1. React Native用于开发原生应用技术成熟且应用广泛

  2. 复用技术在不断进步,mixins -> HOC -> Render Props -> hooks

参考:

lq782655835.github.io/blogs/vue/d…
jishuin.proginn.com/p/763bfbd54…
www.html.cn/qa/react/15…
www.cnblogs.com/xzsj/p/1351…
blog.csdn.net/huazai96318…
jishuin.proginn.com/p/763bfbd4f…