「Vue」与「React」--使用上的区别

10,423 阅读13分钟

作者: 落尘

Vue和React都是目前最流行、生态最好的前端框架之一,之所以用“与”字来做标题,也是为了避免把他们放在对立面。毕竟框架本身没有优劣之分,只有适用之别,选择符合自身业务场景、团队基础的技术才是我们最主要的目的。

本文希望通过对比两个框架在使用上的区别,能使只用其中一个框架进行开发的开发者快速了解和运用另一个框架,已应对不同技术栈的需求,无论你是为了维护老系统还是为了适应新的生态(React-Native)。

那我们现在就从下面这几个步骤来分别聊聊Vue和React的区别:

1. 如何开始第一步?

Vue和React分别都有自己的脚手架工具:vue-cli和create-react-app,两者都能帮助我们快速建立本地的开发环境,具体步骤如下:

vue-cli 3.*

npm install -g @vue/cli
vue create my-project  //node版本推荐8.11.0+

// 如果你还是习惯之前2.*版本的话,再安装一个工具即可
npm install -g @vue/cli-init
vue init webpack my-project

生成的项目结构大同小异,只不过3.*版本将原先直接暴露出来的webpack配置封装到了自己的vue.config.js配置文件中,包括常见baseUrl、outputDir、devServer.proxy等。

Vue-2:

vue-2图

Vue-3:

vue-3图

create-react-app

npm install -g create-react-app
npx create-react-app my-project

npm run eject前图

可以看出create-react-app的封装做的更为彻底一些,基本将所有的webpack配置都隐藏了起来,仅由package.json来控制。但对于需要灵活配置的场景,这样的封装显然是不合适的,你可以运行npm run eject将封装的配置再次释放出来。

npm run eject后图

2. 如何解决最基本的开发需求?

使用脚手架我们可以非常顺利的搭建起本地的开发环境,那下一步就是我们将使用它们来进行业务开发。在此之前,我们可以先问一个问题,什么是最基本的开发需求?

我们可以简单总结为三个方面,而面对新框架你也可以从这三个方面入手,看框架是如何处理的,从而达到快速上手的目的。

  • 模板渲染:将从后端获取的数据(或者是已经在JS中的数据)根据一定的规则渲染到DOM中。
  • 事件监听:监听用户的操作行为,例如点击、移动、输入等。
  • 处理表单:将用户操作行为所产生的数据影响保存下来,并发送给后端,处理返回结果。

我们先来看Vue是如何处理这三方面的

// 模板渲染,可以直接使用data对象中的数据,利用指令来处理渲染逻辑
<template>
  <div class="hello">
    <div v-for="(item, index) in list" :key="index">
      {{ item.title }}
    </div>
    // 处理表单,将用户输入的内容赋值到title上
    <input v-model="title" />	 
    // 事件监听,用户点击后触发methods中的方法
    <button @click="submit">提交</button>  
  </div>
</template>

<script>
export default {
  data () {
    return {
      list: [
        { title: 'first' },
        { title: 'second' }
      ],
      title: ''
    }
  },
  methods: {
    submit () {
      this.list.push({ title: this.title })
      this.title = ''
    }
  }
}
</script>

而React的书写方式为:

import React, { Component } from 'react';

export default class HelloWorld extends Component {
  state = {
    list: [
      { title: 'first' },
      { title: 'second' },
    ],
    title: '',
  };

  setTitle = (e) => {
    // 需要手动调用setState来进行重绘,否则input的value不会改变
    this.setState({ title: e.target.value })
  }

  submit = (e) => {
    const { title, list } = this.state;
    list.push({ title });
    this.setState({ list, title: '' });
  }

  render() {
    const { title, list } = this.state;
    return (
      <div className="App">
        // react会使用jsx的方式来进行模板的渲染,可混合使用js和html标签
		//  {}解析js,()解析html
        { list.map((item, index) => (
            <div key={index}>{ item.title }</div>
          )
        )}
        // 事件监听 + 表单处理
        <input value={title} onChange={this.setTitle} />
        <button onClick={this.submit}>增加</button>
      </div>
    );
  }
}

从上面两个例子中我们可以看出,Vue的写法稍微简单一下,不需要手动利用setState这样类似的特定函数来触发数据与模板的同步。但也正由于手动触发,React显得更加自由一些,我们可以根据实际情况去处理,适当减少不必要的重绘,从而对性能进行优化。

3. 生命周期有什么不一样?

生命周期一直是Vue和React中非常重要的概念,明确的表达了组件从创建、挂载、更新、销毁的各个过程。那两者在细节上有什么区别呢,我们可以通过它们各自官网的图来看一下。

Vue的生命周期如下图:

vue生命周期

React的生命周期如下图:

react生命周期

主体流程大致都可以分为:

  • 创建组件:建立组件状态等一些前置工作,如果需要请求数据的话,大部分在此时进行。
  • 挂载组件:将组件真正挂载到Document中,可以获取真实的DOM进行操作。
  • 更新组件: 组件自身的状态,外部接受的参数发生变化时。
  • 卸载组件: 当组件要被移除当前Document时,通常会销毁一些定时器等脱离于组件之外的事件。

4. Vue比React多了哪些常见属性

Vue之所以给人上手简单、开发便捷的感觉,有一部分原因就在于封装了很多常见的操作,你只需要简单的使用一些属性就可以达到目的。便捷和自由有的时候就是一对反义词,你在获得一些特性的时候就会牺牲一些特性,就像算法上常见的空间换时间这样的策略。

那我们就来看看Vue的哪些常见属性是React中没有的:

computed

计算属性经常用于集中管理多个状态的结果,避免将过多的计算逻辑暴露在模板或其他引用的地方。我们也无需知道该计算属性依赖的属性何时、如何变化,Vue能自动监听变化结果并重新计算。例如:

computed: {
  fullName () {
    return this.firstName + ' ' + this.lastName;
  }
}

####watch: 和计算属性类似,可以指定监听某个状态变化,并获得该属性变化前后的值。例如:

watch: {
  name (val, oldVal) {	// 监听某个属性发生变化
  ....
  }
}

而在React中,你需要实现这一功能,你可能需要在componentWillReceiveProps时自己去判断属性是否发生了变化,或者在setState的同时触发变化后的业务逻辑。例如:

componentWillReceiveProps(nextProps) {
  if (nextProps.name != this.props.name) {  // props中的某个属性发生了变化
    ....
  }
}

####指令 Vue中的指令主要用于封装对DOM的操作,例如模板中的v-if/v-else/v-for等;React本身利用jsx来操作DOM,所以也就没有这一概念。我们举一个自定义指令的例子,来提现下指令的作用:

<img v-lazy="img_url" />
directives: {
  'lazy': {
	inserted: function (el, binding) {
	  var body = document.body;
	  var offsetTop = el.offsetTop;
	  var parent = el.offsetParent;
	  // 获取绑定元素对于body顶部的距离
	  while (parent && parent.tagName != 'body') {
	    offsetTop += parent.offsetTop;
	    parent = parent.offsetParent;
	  }
	  // 若出现在可视区域内,则直接赋值src
	  if (body.scrollTop + body.clientHeight > offsetTop && body.scrollTop < offsetTop) {
	    el.src = binding.value;
	  } else {
	  	 // 若暂未出现,则监听window的scroll事件 
	    var scrollFn = function () {
	      // 出现在区域内才赋值src,并取消事件监听
	      if (body.scrollTop + body.clientHeight > offsetTop && body.scrollTop < offsetTop) {
	        el.src = binding.value;
	        window.removeEventListener('scroll', scrollFn)
	      }
	    }
	    window.addEventListener('scroll', scrollFn)
	  }
	}
  }
}

这里其实我们也可以试想一下,如果使用React的话,我们可以如何实现图片懒加载这个功能?

5. 组件的区别

组件目前可以说是两个框架开发中最基本的单位,每个项目都包含一个根组件,然后再以路由进行细分,从页面直到功能更为单一的组件。而如何处理项目中多个组件的关系、如何高效的复用组件也就成了框架所需要考虑的事情,下面就和大家对比下两者在处理这种情况下的异同。

组件通信

组件通信是组件关系中最常见的一种,主要表现在父组件向子组件传递数据,子组件调用父组件方法影响父组件。我们分别举例来说明两者在写法上的区别:

Vue的写法:

// 父组件
<template>
  <div class="parent">
   <div v-for="(msg, index) in msgs" :key="index">
      {{ msg.content }}
    </div>
    // 通过v-bind可以绑定父组件状态传递给子组件
    // 并且可以自定义事件将方法传递给子组件
    <child :last="last" @add="add"></child>
  </div>
</template>

<script>
import Child from '@/components/PropsEventChild'

export default {
  components: {
    Child
  },

  data () {
    return {
      name: 'parent',
      msgs: []
    }
  },

  computed: {
    last () {
      const { msgs } = this
      return msgs.length ? msgs[msgs.length - 1] : undefined
    }
  },

  methods: {
    add (msg) {
      this.msgs.push(msg)
    }
  }
}
</script>

// 子组件
<template>
  <div class="child">
    <input v-model="content" placeholder="请输入" />
    <button @click="submit">提交</button>
  </div>
</template>

<script>
export default {
  // 此处需要定义接受参数的名称
  props: ['last'],

  data () {
    return {
      content: ''
    }
  },

  methods: {
    submit () {
      const time = new Date().getTime()
      const { last } = this
      if (last && (time - last.time < 10 * 1000)) {
        alert('你发言太快了')
        return
      }
      // 通过$emit的方式可以调用父组件传递过来的自定义事件,从而修改父组件的状态
      this.$emit('add', { content: this.content, time })
      this.content = ''
    }
  }
}
</script>

React写法:

// 父组件
import React, { Component } from 'react';
import Child from './Child'

export default class Parent extends Component {
  state = {
    msgs: []
  };

  get last () {
    const { msgs } = this.state;
    return msgs.length ? msgs[msgs.length - 1] : undefined;
  }

  add = (msg) => {
    const { msgs } = this.state;
    msgs.push(msg);
    this.setState({ msgs });
  };

  render() {
    const { msgs } = this.state;
    return (
      <div className="Parent">
        { msgs.map((item, index) => (
            <div key={index}>{ item.content }</div>
          )
        )}
        // 直接传递参数和方法
        <Child last={this.last} onClick={this.add}/>
      </div>
    );
  }
}

// 子组件
import React, { Component } from 'react';

export default class Child extends Component {
  state = {
    content: ''
  };

  setContent = (e) => {
    this.setState({ content: e.target.value })
  }

  submit = (e) => {
    const { props: { last }, state: { content } } = this
    const time = new Date().getTime()
    if (last && (time - last.time < 10 * 1000)) {
      alert('你发言太快了')
      return
    }
    // 直接调用传递过来的方法
    this.props.onClick({ content, time })
    this.setState({ content: '' })
  }

  render() {
    const { content } = this.state;
    return (
      <div className="Child">
        <input value={content} onChange={this.setContent} />
        <button onClick={this.submit}>增加</button>
      </div>
    );
  }
}

组件嵌套

实际开发当中经常会使用到一些提供单纯的UI功能,但内容需要业务自行定义的组件,例如Modal、Table等。这种类型的组件往往和会业务组件进行嵌套,但不影响业务组件的状态。Vue提供了这样写法,可以将业务组件替换掉UI组件中特定的一部分,这里就不赘述代码了,直接上一下例子:

Vue Slot组件

Vue Slot使用方式

React中没有<slot>这种语法,但组件本身可以将标签内的部分以props.children的方式传递给子组件,例如:

import React, { Component } from 'react';
import Wrapper from './Wrapper'

export default class Demo extends Component {

  state = {
    content: '我是Demo组件中的内容'
  };

  render() {
    return (
      <div>
        <Wrapper>
          <div>{ this.state.content }</div>
        </Wrapper>
      </div>
    );
  }
}

import React, { Component } from 'react';

export default class Wrapper extends Component {
  render() {
    return (
      <section>
        <header>我是Wrapper头部</header>
        { this.props.children }
        <footer>我是Wrapper尾部</footer>
      </section>
    );
  }
}

可以看出this.props.children代表的就是父组件(class Demo)中的被包裹的标签<div>{ this.state.content }</div>

无状态组件

有的时候我们可能希望组件只是起一个渲染的作用,并不需要状态变化或者生命周期这种监听。为了节约性能,React可以只使用函数来接受和使用props:

const Wrapper = (props) => (
  <section>
    <header>我是Wrapper头部</header>
    { props.children }
    <footer>我是Wrapper尾部</footer>
  </section>
)

Vue当中有种类似的用法,在template标签中加上functional,模板渲染后不会对数据进行监听:

<template functinal>
  <section>
    <header>我是Wrapper头部</header>
    { props.children }
    <footer>我是Wrapper尾部</footer>
  </section>
 </template>
 // 除了template标签之外,该组件已无法使用Vue实例,也就是不能在该.vue文件中使用<script></script>标签。

跨组件通信

所谓的跨组件通信只指不通过props传递获取父组件中定义的数据,这样深层的子组件就能直接访问到顶层的数据。

React中提供了Context这样的机制来实现,具体代码:

定义Context

设定Provider,定义数据

设定Consumer,获取数据

Vue中也有类似的机制,但官方文档中表示这个特性本意在于给高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。,不过我们还是可以简单了解下它的使用方法:

// 祖先级组件
export default {
	name: 'App',

    provide: {
      theme: 'meicai'
    }
}

// 子组件,获取provide中定义的数据
export default {
  inject: ['theme']
}

React高阶组件

高阶组件是React中的一个概念,简称HOC(Higher Order Component),定义是:高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。例如下面这个例子:

const withHeader = (WrappedComponent) =>
  class WrapperComponent extends Component {
    render() {
      return <section>
        <header><h1>顶部信息</h1></header>
        <WrappedComponent {...this.props} />
      </section>
    }
}

其中参数WrappedComponent为React组件,我们可以在其他组件中使用装饰器的语法来使用这个高阶组件:

import React, { Component } from 'react';
import withHeader from './withHeader'

@withHeader  // 装饰器写法
class Section extends Component {

  state = {
    content: '我是SectionOne'
  };

  render() {
    return (
      <div>
        { this.state.content }
      </div>
    );
  }
}

这样,这个Section组件最后render的效果即包含了withHeader中添加的顶部信息等元素,也包括了自己本身的DOM结构。

除了从结构上对子组件进行增强外,高阶组件也可以通过注入props的方式增强子组件的功能,在ant-design的Form组件中,就有类似的用法,以下我们举个简化的例子,实现给受控组件绑定值并且设定onChange:

import React, { Component } from 'react';
// 表单高阶组件
const Form = (Wrapped) =>
  class WrapperComponent extends Component {

    state = {
      fields: {}
    };

    getFieldValue = (name) => {
      return this.state.fields[name];
    };

    setFieldValue = (name, value) => {
      const fields = this.state.fields;
      fields[name] = value;
      this.setState({ fields });
    };

    getFieldDecorator = (name, value) => {
      const { fields } = this.state;
      if (fields[name] === undefined) {
        fields[name] = value || '';
        this.setState({ fields })
      }
      return (WrappedInput) =>
        React.cloneElement(WrappedInput,
          Object.assign({}, WrappedInput.props, { value: fields[name], onChange: (e) => this.setFieldValue(name, e.target.value) }));
    };

    render () {
      const { getFieldValue, getFieldDecorator } = this;
      // 注入新的props对象,将高阶组件的方法传入到子组件中
      const form = {
        state: this.state,
        getFieldValue,
        getFieldDecorator,
      };
      return (<Wrapped {...this.props} form={form}></Wrapped>);
    }
  };

export default Form

// 使用方式
import React, { Component } from 'react';
import Form from './form'

class Demo extends Component {
  checkValue = (e) => {
    const { getFieldValue } = this.props.form;
    console.log(getFieldValue('title'));
  }

  render() {
    const { getFieldDecorator } = this.props.form;
    return (
      <div>
        { getFieldDecorator('title')(<input />) }
        <button onClick={this.checkValue}>获取输入框值</button>
      </div>
    );
  }
}

export default Form(Demo)

##6. 状态管理 状态管理是在处理大型项目中组件通信常用的设计模式,Vue和React都采取了这种模式并拥有自己的实现方式。两者在大概念上没有什么的不同,从两者的流程图上也能看出来:

Vue状态管理流程图:

vuex

React状态管理流程图:

mobx

两张图都标明了整个过程为一个单项数据流,从状态映射视图,视图(组件)操作动作,动作影响状态。

所以这节就简单对比下两者在使用的区别:

Vue:

定义状态及动作

使用状态并含有动作的组件

React:

定义状态及动作

使用状态并含有动作的组件

7. 小结

最后我们回顾下,本文主要从以下这几个方面对比了Vue和React的不同:

  • 处理渲染、事件和表单的不同
  • 常见属性的不同
  • 组件及组件间关系的不同
  • 状态管理使用的不同

希望从这几个方面切入可以帮大家快速的了解另一门框架,本文所包含案例


原文链接: tech.meicai.cn/detail/78, 也可微信搜索小程序「美菜产品技术团队」,干货满满且每周更新,想学习技术的你不要错过哦。