Vue和React差异性

284 阅读11分钟

前言

Vue和React都是目前最流行、生态最好的前端框架之一。框架本身没有优劣之分,只有适用之别,选择符合自身业务场景、团队基础的技术才是我们最主要的目的。

正文

设计思想

vue

vue的官网中说它是一款渐进式框架,采用自底向上增量开发的设计。这里我们需要明确一个概念,什么是渐进式框架。在声明式渲染(视图模板引擎)的基础上,我们可以通过添加组件系统(components)、客户端路由(vue-router)、大规模状态管理(vuex)来构建一个完整的框架。Vue从设计角度来讲,虽然能够涵盖所有这些内容,但是你并不需要一上手就把所有东西全用上,因为没有必要。无论从学习角度,还是实际情况,这都是可选的。声明式渲染和组件系统是Vue的核心库所包含内容,而客户端路由、状态管理、构建工具都有专门解决方案。这些解决方案相互独立,你可以在核心的基础上任意选用其他的部件,不一定要全部整合在一起。可以看到,所说的“渐进式”,其实就是Vue的使用方式,同时也体现了Vue的设计的理念。

react

react主张函数式编程,所以推崇纯组件,数据不可变,单向数据流,当然需要双向的地方也可以手动实现,比如借助 onChange 和 setState 来实现一个双向的数据流。React的三个最大特点是声明式、组件化和多端(native、node)渲染。

  1. React专注于View层,并可配合其它库(如Redux, Mobx等)行程MV*架构。
  2. React提出了虚拟dom的概念,极大的提高了页面渲染的性能,并能在Native或服务端进行渲染。
  3. 单向数据流,React的数据会从上到下的通过props传递,用户只用关心数据状态的更新。
  4. React采用声明式渲染,提高代码可读性和可维护性。
  5. 组件化,React中组件是一等公民。

编写语法

vue

vue推荐的做法是webpack+vue-loader的单文件组件格式,vue保留了html、css、js分离的写法,使得现有的前端开发者在开发的时候能保持原有的习惯,更接近常用的web开发方式,模板就是普通的html,样式直接使用css.

react

react是没有模板的,直接就是一个渲染函数,React推荐的做法是 JSX + inline style, 也就是把HTML和CSS全都写进JavaScript了,即'all in js'。JSX实际就是一套使用XML语法,用于让我们更简单地去描述树状结构的语法糖。在react中,所有的组件的渲染功能都依靠JSX。你可以在render()中编写类似XML的语法,它最终会被编译成原生JavaScript。

组件

vue

vue组件定义使用xx.vue文件来表示,vue组件将html、css、js组合到一起,形式如下:

// 模板(html)
<template>
  <div>{{name}}</div>
</template>

// 数据管理(js)
<script>
export default {
  name: 'CompoenentName',
  data() {
    return {
      name: 'xx'
    }
  }
}
</script>

// 样式(css)
<style scoped>

</style>

react

react推荐使用jsx或者js文件来表示组件,react支持类组件和函数式组件2种形式,react使用{}包裹变量。

注意: 组件名称必须以大写字母开头。React 会将以小写字母开头的组件视为原生 DOM 标签。例如,<div />代表HTML的div标签,而<App />则代表一个组件。

以下定义了react的类组件和函数式组件

// 类组件
import React from 'react';

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      msg: 'hello world'
    };
  }
  render() {
    rerurn(<div>{msg}</div>);
  }
}

export default App;
// 函数式组件
import React from 'react';

function App() {
  return (<div>hello world</div>);
}

export default App;

组件数据交互

组件数据交互是指父子组件、兄弟组件、跨层组件之间传递数据。 兄弟组件之间可以通过事件总线或者通过父组件传递数

vue

父子组件数据交互

vue中父组件通过props传递数据给子组件,子组件使用$emit触发自定义事件,父组件中监听子组件的自定义事件获取子组件传递来的数据。

// 子组件使用$emit传递自定义事件myEvent:
<template>
  <div @click="changeName">{{name}}</div>
</template>

<script>
export default {
  name: 'Child',
  data() {
    return {
      name: 'xxx'
    }
  },
  methods: {
    changeName() {
      this.name = 'new Name';
      this.$emit('myEvent', this.name);
    }
  }
}
</script>
// 父组件使用@myEvent监听自定义事件,回调函数参数是子组件传回的数据:
<template>
  <div>
    <Child @myEvent="getName"></Child>
  </div>
</template>

<script>
import Child from './Child';

export default {
  components: {
    Child
  },
  methods: {
    getName(name) {
      console.log(name)
    }
  }
}
</script>

跨组件数据交互

通过provide / inject在祖先组件向所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。

// 上层组件设置provide
<script>
export default {
  provide: { // 定义provide选项
    message: 'This is a big news'
  }
}
</script>
// 下层组件定义inject
<template>
  <div>{{message}}</div>
</template>

<script>
export default {
  name: 'Children',
  inject: ['message']
}
</script>

react

父子组件数据交互

react中父组件使用props传递数据和回调函数给子组件,子组件通过props传下来的回调函数返回数据,父组件通过回调函数获取子组件传递上来的数据。

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

function Children(props) {
  const { myEvent } = props;

  const changeName = () => {
    myEvent('new name');
  };
  return <div onClick={changeName}>修改name</div>;
}
// 父组件
function Parent() {
  const changeName = name => {
    console.log(name);
  };
  return <Children myEvent={changeName}></Children>;
}

跨组件数据交互

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树
import React from 'react'
const ThemeContext = React.createContext('light');
class App extends React.Component {
  render() {
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

状态集管理工具

vue

vue使用vuex来进行状态管理。vuex引入State、Getter的概念对状态进行定义;使用Mutation和Action对状态进行变更;引入Module对状态进行模块化分割;引入插件对状态进行快照、记录、以及追踪等;提供了mapState、mapGetters、 mapActions、 mapMutations 辅助函数方便开发者在vm中处理store。具体构成关系如下:

1.png

  1. 将需要共享的状态挂载到state上:this.$store.state或者mapState辅助函数来调用。
  2. 通过getters来创建状态:通过this.$store.getters来调用。可以根据某一个状态派生出一个新状态,vuex也提供了mapGetters辅助函数来帮助我们在组件中使用getters里的状态。
  3. 使用mutations来更改state:通过this.$store.commit来调用。我们不能直接在组件中更改state,而是需要使用mutations来更改,mutations也是一个纯对象,里面包含很多更改state的方法,这些方法的形参接收到state,在函数体里更改,这时,组件用到的数据也会更改,实现响应式。vuex提供了mapMutations方法来帮助我们在组件中调用mutations 的方法。
  4. 使用actions来处理异步操作:this.$store.dispatch来调用。Actions类似于mutations,不同在于:Actions提交的是mutations,而不是直接变更状态。Actions可以包含任意异步操作。也就是说,如果有这样的需求:在一个异步操作处理之后,更改状态,我们在组件中应该先调用actions,来进行异步动作,然后由actions调用mutations来更改数据。在组件中通过this.$store.dispatch方法调用actions的方法,当然也可以使用mapMutations来辅助使用。

react

react使用Redux进行状态管理,它的出现主要是为解决react中组件之间的通信问题。建议把数据放入到redux中管理,目的就是方便数据统一,好管理。

redux的流程:

2.png

  1. 创建store: 从redux工具中取出createStore去生成一个store。
  2. 创建一个reducer,然后将其传入到createStore中辅助store的创建。 reducer是一个纯函数,接收当前状态和action,返回一个状态,返回什么,store的状态就是什么,需要注意的是,不能直接操作当前状态,而是需要返回一个新的状态。 想要给store创建默认状态其实就是给reducer一个参数创建默认值。
  3. 组件通过调用store.getState方法来使用store中的state,挂载在了自己的状态上。
  4. 组件产生用户操作,调用actionCreator的方法创建一个action,利用store.dispatch方法传递给reducer。
  5. reducer对action上的标示性信息做出判断后对新状态进行处理,然后返回新状态,这个时候store的数据就会发生改变, reducer返回什么状态,store.getState就可以获取什么状态。
  6. 我们可以在组件中,利用store.subscribe方法去订阅数据的变化,也就是可以传入一个函数,当数据变化的时候,传入的函数会执行,在这个函数中让组件去获取最新的状态。

React Hooks

Hooks作为react所特有的,vue不存在hooks这一概念。是因为react创建组件有2种方式:类组件和函数式组件。类组件可以定义状态的,也有生命周期等等,但是在函数式组件里面没有,于是react就推出了hooks。

useState

useState 是最基本的 API,它传入一个初始值,每次函数执行都能拿到新值。

import React, { useState } from 'react';
import ReactDOM from 'react-dom';

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+</button>
      <h1>{count}</h1>
      <button onClick={() => setCount(count - 1)}>-</button>
    </div>
  )
}

ReactDOM.render(<Counter />, document.getElementById('root'));

useEffect

一个至关重要的 Hooks API,顾名思义,useEffect 是用于处理各种状态变化造成的副作用,也就是说只有在特定的时刻,才会执行的逻辑,可用于生命周期:componentDidMount/componentDidUpdate/componentWillUnMount。

import { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

function Example() {
  const [count, setCount] = useState(0);

  // => componentDidMount/componentDidUpdate
  useEffect(() => {
    // update 
    document.title = `You clicked ${count} times`;
    // => componentWillUnMount
    return function cleanup() {
    	document.title = 'app';
    }
  }, [count]);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

ReactDOM.render(<Example />, document.getElementById('root'));

useRef

创建ref对象:类组件、React 元素用 React.createRef,函数组件使用 useRef。

useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue)。

const refContainer = useRef(initialValue);

forwardRef:因为函数组件没有实例,所以函数组件无法像类组件一样可以接收 ref 属性。forwardRef 可以在父组件中操作子组件的 ref 对象。forwardRef 可以将父组件中的 ref 对象转发到子组件中的 dom 元素上。子组件接受 props 和 ref 作为参数。

function Child(props,ref){
  return (
    <input type="text" ref={ref}/>
  )
}
Child = React.forwardRef(Child);
function Parent(){
  let [number,setNumber] = useState(0); 
  // 在使用类组件的时候,创建 ref 返回一个对象,该对象的 current 属性值为空
  // 只有当它被赋给某个元素的 ref 属性时,才会有值
  // 所以父组件(类组件)创建一个 ref 对象,然后传递给子组件(类组件),子组件内部有元素使用了
  // 那么父组件就可以操作子组件中的某个元素
  // 但是函数组件无法接收 ref 属性 <Child ref={xxx} /> 这样是不行的
  // 所以就需要用到 forwardRef 进行转发
  const inputRef = useRef();//{current:''}
  function getFocus(){
    inputRef.current.value = 'focus';
    inputRef.current.focus();
  }
  return (
      <>
        <Child ref={inputRef}/>
        <button onClick={()=>setNumber({number:number+1})}>+</button>
        <button onClick={getFocus}>获得焦点</button>
      </>
  )
}

useContext

  • 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
  • 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
  • useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
import { useState, useContext, createContext } from 'react';
import ReactDOM from 'react-dom';

// 1. 使用 createContext 创建上下文
const UserContext = new createContext();

// 2. 创建 Provider
const UserProvider = props => {
  let [username, handleChangeUsername] = useState('');
  return (
    <UserContext.Provider value={{ username, handleChangeUsername }}>
      {props.children}
    </UserContext.Provider>
  );
};

const Pannel = () => {
  const { username, handleChangeUsername } = useContext(UserContext); // 3. 使用 Context
  return (
    <div>
      <div>user: {username}</div>
      <input onChange={e => handleChangeUsername(e.target.value)} />
    </div>
  );
};

const App = () => (
  <div>
    <UserProvider>
      <Pannel />
    </UserProvider>
  </div>
);

ReactDOM.render(<App />, document.getElementById('root'));

useCallback

语法:

const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);

返回值 memoizedCallback 是一个 memoized 回调。传递内联回调和一系列依赖项。useCallback 将返回一个回忆的memoized版本,该版本仅在其中一个依赖项发生更改时才会更改。

Foo.js

import React from 'react';

const Foo = ({ onClick }) => {

    console.log('Foo:', 'render');
    return <button onClick={onClick}>Foo 组件按钮</button>;

};

export default React.memo(Foo);

App.js

import React, { useCallback, useState } from 'react';
import Foo from './Foo';

function App() {
    const [count, setCount] = useState(0);

    const fooClick = useCallback(() => {
        console.log('点击了 Foo 组件的按钮');
    }, []);

    return (
        <div>
            <p>{count}</p>
            <Foo onClick={fooClick} />
            <br />
            <br />
            <button onClick={() => setCount(count + 1)}>count increment</button>
        </div>
    );
}

export default App;

如果点击count increment按钮,Foo组件是不会重新渲染的。如果将useCallback或者React.memo,只要改变父组件的任何变量,Foo组件还是会重新渲染的。

useMemo

语法:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

返回一个memoized值。 传递“创建”函数和依赖项数组。useMemo 只会在其中一个依赖项发生更改时重新计算 memoized 值。此优化有助于避免在每个渲染上进行昂贵的计算。

  • 可以进行数据的缓存,类似于 Vue 的 computed,可以根据依赖变化自动重新计算
  • 可以帮助我们优化子组件的渲染,比如这种场景:在 App 组件中有两个子组件 Foo 和 Bar,当 App 组件中传给 Foo 组件的 props 发生变化时,App 组件状态会改变,重新渲染。此时 Foo 组件 和 Bar 组件 也都会重新渲染。其实这种情况是比较浪费资源的,现在我们就可以使用 useMemo 进行优化,Foo 组件用到的 props 变化时,只有 Foo 组件进行 render,而 Bar 却不会重新渲染。

Foo.js

import React from 'react';

export default ({ text }) => {
    console.log('Foo:', 'render');
    return <div>Foo 组件:{ text }</div>
}

Bar.js

import React from 'react';

export default ({ text }) => {
    console.log('Bar:', 'render');
    return <div>Bar 组件:{ text }</div>
}

App.js

import React, { useState, useMemo } from 'react';
import Foo from './Foo';
import Bar from './Bar';

export default () => {
    const [a, setA] = useState('foo');
    const [b, setB] = useState('bar');

    const foo = useMemo(() => <Foo text={ a } />, [a]);
    const bar = useMemo(() => <Bar text={ b } />, [b]);

    return (
        <div>
            { foo }
            { bar }
            <br />
            <button onClick={ () => setA('修改后的 Foo') }>修改传给 Foo 的属性</button>
            &nbsp;&nbsp;&nbsp;&nbsp;
            <button onClick={ () => setB('修改后的 Bar') }>修改传给 Bar 的属性</button>
        </div>
    )
}

此时我们点击不同的按钮,控制台都只会打印一条输出,改变 a 或者 b,A 和 B 组件都只有一个会重新渲染。useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

自定义 Hook

自定义 Hook 更像是一种约定,而不是一种功能。如果函数的名字以 use 开头,并且调用了其他的 Hook,则就称其为一个自定义 Hook。

import React, { useEffect, useState } from 'react';

function useNumber(){
  let [number,setNumber] = useState(0);
  useEffect(()=>{
    setInterval(()=>{
        setNumber(number=>number+1);
    },1000);
  },[]);
  return [number,setNumber];
}

HOC

HOC(全称Higher-order component高阶组件)是一种React的进阶使用方法,主要还是为了便于组件的复用。HOC就是一个方法,获取一个组件,返回一个更高级的组件。在React开发过程中,发现有很多情况下,组件需要被"增强",比如说给组件添加或者修改一些特定的props,一些权限的管理,或者一些其他的优化之类的。而如果这个功能是针对多个组件的,同时每一个组件都写一套相同的代码,明显显得不是很明智,所以就可以考虑使用HOC。

  • 代码复用,代码模块化
  • 增删改props
  • 渲染劫持

1.可以通过对传入的props进行修改,或者添加新的props来达到增删改props的效果。比如你想要给wrappedComponent增加一个props,可以这么搞:

function control(wrappedComponent) {
  return class Control extends React.Component {
    render(){
      let props = {
        ...this.props,
        message: "You are under control"
      };
      return <wrappedComponent {...props} />
    }
  }
}

这样,你就可以在你的组件中使用message这个props

2.这里的渲染劫持并不是你能控制它渲染的细节,而是控制是否去渲染。由于细节属于组件内部的render方法控制,所以你无法控制渲染细节。比如,组件要在data没有加载完的时候,实现loading...,就可以这么写:

function loading(wrappedComponent) {
  return class Loading extends React.Component {
    render(){
      if(!this.props.data) {
        return <div>loading...</div>
      }
      return <wrappedComponent {...props} />
    }
  }
}

这个样子,在父级没有传入data的时候,这一块儿就只会显示loading...,不会显示组件的具体内容