vue与react比较

280 阅读15分钟

一、相同点

1.都有组件化开发和Virtual DOM
2.都支持props进行父子组件间数据通信
3.都支持数据驱动视图,不直接操作真实的DOM,更新状态数据界面就自动更新
4.都支持服务器端渲染
5.都有支持native的方案,react的react native vue的weex

二、不同点

1.数据绑定:vue实现了数据的双向绑定,react数据流动是单项的。

2.组件写法不一样,react推荐的是JSX,也就是把HTML和css全部都写进JavaScript, vue推荐的做
法是 webpack+vue-loader的单文件组件格式,即html,css,js写在同一个文件。

3.state对象在react应用中不可变的,需要使用setState方法更新状态,在vue中,state对象不是必
须的,数据由data属性在vue对象中管理

4.virtual Dom不一样,vue会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树,而对于react
而言,每当应用的状态被改变时,全部组件都会被重新渲染,所以react中需要
shouldComponentUpdate,这个生命周期函数方法来进行控制

5.react严格上只针对MVC的view层,vue则是MVVM模式

6.HoC和mixins

Vue组合不同功能的方式是通过mixin
React组合不同功能的方式是通过HoC(高阶组件)

7.组件通信间的区别

Vue中有三种方式可以实现组件通信:
父组件通过props向子组件传递数据或者回调,父给子:子接收props
子组件通过事件向父组件发送消息;子给父$emit,父接收@方法名

新增的provide/inject来实现父组件向子组件注入数据,可以跨越多个层级。
eventbus https://juejin.cn/post/6974184935804534815

React中也有对应的三种方式:
父组件通过props可以向子组件传递数据或者回调;
可以通过 context 进行跨层级的通信,

8.Vuex和Redux区别

从表面上来说,store注入和使用方式有一些区别。在Vuex中,$store被直接注入到了组件实例中,因此
可以比较灵活的使用:使用dispatch、commit提交更新,通过mapState或者直接通过this.$store来
读取数据。在Redux中,我们每一个组件都需要显示的用connect把需要的props和dispatch连接起来。
另外,Vuex更加灵活一些,组件中既可以dispatch action,也可以commit updates,而Redux中只
能进行dispatch,不能直接调用reducer进行修改。

从实现原理上来说,最大的区别是两点:Redux使用的是不可变数据,而Vuex的数据是可变的,因此,
Redux每次都是用新state替换旧state,而Vuex是直接修改。Redux在检测数据变化的时候,是通过
diff的方式比较差异的,而Vuex其实和Vue的原理一样,是通过getter/setter来比较的,这两点的区
别,也是因为React和Vue的设计理念不同。React更偏向于构建稳定大型的应用,非常的科班化。相比之
下,Vue更偏向于简单迅速的解决问题,更灵活,不那么严格遵循条条框框。因此也会给人一种大型项目用
React,小型项目用Vue的感觉。

9.监听数据变化的实现原理不同

Vue通过 getter/setter以及一些函数的劫持,能精确知道数据变化。

React默认是通过比较引用的方式(diff)进行的,如果不优化可能导致大量不必要的VDOM的重新渲染。
为什么React不精确监听数据变化呢?这是因为Vue和React设计理念上的区别,Vue使用的是可变数据,
而React更强调数据的不可变,两者没有好坏之分,Vue更加简单,而React构建大型应用的时候更加鲁棒。

三、vue 生命周期

1>beforeCreate( 创建前 )
在实例初始化之后,el 和 data 并未初始化,因此无法访问methods, data, computed等上的方法和数据。

2>created ( 创建后 )
实例已经创建完成之后被调用,在这一步,实例已完成以下配置:数据观测、属性和方法的运算,
watch/event事件回调,完成了data 数据的初始化,el没有。 然而,挂在阶段还没有开始, $el属性
目前不可见,这是一个常用的生命周期,因为你可以调用methods中的方法,改变data中的数据,并且修
改可以通过vue的响应式绑定体现在页面上,,获取computed中的计算属性等等,通常我们可以在这里对
实例进行预处理,也有一些童鞋喜欢在这里发ajax请求,值得注意的是,这个周期中是没有什么方法来对
实例化过程进行拦截的,因此假如有某些数据必须获取才允许进入页面的话,并不适合在这个方法发请求,
建议在组件路由钩子beforeRouteEnter中完成

3>beforeMount
挂在开始之前被调用,相关的render函数首次被调用(虚拟DOM),实例已完成以下的配置: 编译模板,
把data里面的数据和模板生成html,完成了el和data 初始化,注意此时还没有挂在html到页面上。

4>mounted
挂在完成,也就是模板中的HTML渲染到HTML页面中,此时一般可以做一些ajax操作,mounted只会执行一次。

5>beforeUpdate
在数据更新之前被调用,发生在虚拟DOM重新渲染和打补丁之前,可以在该钩子中进一步地更改状态,不会
触发附加地重渲染过程

6>updated(更新后)
在由于数据更改导致地虚拟DOM重新渲染和打补丁只会调用,调用时,组件DOM已经更新,所以可以执行依
赖于DOM的操作,然后在大多是情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环,该
钩子在服务器端渲染期间不被调用

7>beforeDestroy(销毁前)
一般在这一步做一些重置的操作,比如清除掉组件中的定时器 和 监听的dom事件

8>destroyed(销毁后)
所以的事件监听器会被移出,所有的子实例也会被销毁,该钩子在服务器端
渲染期间不被调用


四、react生命周期

1>初始化阶段:

getDefaultProps: 获取实例的默认属性

getInitialState:获取每个实例的初始化状态

componentWillMount:组件即将被装载、渲染到页面上(在整个生命周期中只会触发一次)

render:组件在这里生成虚拟的 DOM 节点

componentDidMount:组件真正在被装载之后(在整个生命周期中只会触发一次)

2>运行中状态:

componentWillReceiveProps:组件将要接收到属性的时候调用

shouldComponentUpdate:组件接受到新属性或者新状态的时候(可以返回 false,接收数据后不更
新,阻止 render 调用,后面的函数不会被继续执行了)

componentWillUpdate:组件即将更新不能修改属性和状态

render:组件重新描绘

componentDidUpdate:组件已经更新

3>销毁阶段:

componentWillUnmount:组件即将销毁

新增俩个状态

getDerivedStateFromProps(nextProps, prevState) 
用于替换
componentWillReceiveProps

getSnapshotBeforeUpdate(prevProps, prevState)
用于替换 
componentWillUpdate

5.为啥react 使用了 hook?hook 优于class的方面?

1.在组件之间复用状态逻辑很难

React 没有提供将可复用性行为“附加”到组件的途径,render props 和 高阶组件来解决此类问题,但是重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解【嵌套地狱】,所以说React 需要为共享状态逻辑提供更好的原生途径,所以 就用了hook。

2.复杂组件变得难以理解

组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug。

Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据) ,而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

3.难以理解的class

class 是学习 React 的一大屏障。你必须去理解 JavaScript 中 this 的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。

为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的 React 特性

6.HOC 与 mixin 的区别?

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

hoc:高阶组件是参数为组件,返回值为新组件的函数

HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用

mixin是:混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项

HOC可用于许多任务,例如:

  • 代码重用,逻辑和引导抽象

  • 渲染劫持

  • 状态抽象和控制

  • Props 控制

  • data合并策略:数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先;

  • 钩子函数合并策略:同名钩子函数将合并为一个数组,因此都将被调用。混入对象的钩子将在组件自身钩子之前调用;

  • 值为对象的选项合并策略: 例如 methodscomponents 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对

总结: vue 的混入就是给组件添加功能,冲突就用合并不一样的合并策略解决;但是组件还是原来的组件,不能对template模板复用;

react 的 HOC, 其实就是通过函数,返回一个基本一样的组件,可以对组件的所有功能扩展,灵活性比较高;

react为什么放弃mixin,选用了hoc?

  • 1.隐含了一些依赖,比如我在组件中写了某个 state 并且在 mixin 中使用了,就这存在了一个依赖关系。万一下次别人要移除它,就得去 mixin 中查找依赖

  • 2.多个 mixin 中可能存在相同命名的函数,同时代码组件中也不能出现相同命名的函数,否则就是重写了,其实我一直觉得命名真的是一件麻烦事。。

  • 3.雪球效应,虽然我一个组件还是使用着同一个 mixin,但是一个 mixin 会被多个组件使用,可能会存在需求使得 mixin 修改原本的函数或者新增更多的函数,这样可能就会产生一个维护成本

7.redux的工作原理:

redux 是将整个应用的state存储在一个公共的store文件当中,组件可以通过分发(dispatch)一个动作或者是行为(action)给这个公用的store,而不是直接去通知其他组件,组件内部通过订阅store中的状态state来刷新自己的视图。这里我个人对的理解是,在我们的组件内部有个类似于监听器的东西,一旦监听到store中的值发生了改变就会刷新我们的页面。

redux 三大原则

  • 唯一数据源

整个应用的数据存储在一个统一的状态树中,也就是我们前面所说的公共的store 文件。在组件都会从这个store中获取数据。

  • 保持只读状态

state是只读的,唯一改变state的方法就是触发action,action是一个用于描述以发生时间的普通对象。

  • 数据改变只能通过纯函数来执行

使用纯函数来执行修改,为了描述action如何改变state的,你需要编写reducers。

4、redux原理详细解析

下面详细介绍一下redux的工作原理

4.1 Store

redux中的数据是存储在一个状态树store中,所以说:

  • store就是保存数据的地方,你可以把它看成一个数据,而且整个应用能有一个store。
  • redux提供了createStore这个函数,用来生成store。
    import {createStore} from 'redux'
    const store=createStore(fn);
复制代码

4.2 State

state就是store中存储的数据,store里面可以拥有多个state,Redux规定一个state对应一个View,只要state相同,view就是一样的,反过来也是一样的,可以通过store.getState( )获取。

    import {createStore} from 'redux'
    const store=createStore(fn);
    const state=store.getState()
复制代码

4.3 Action

state的改变会导致View的变化,上面我们说过redux中不能直接通过this.setState()修改state,为了使state发生改变,在redux中提供了一个对象Action,我们可以理解为一个行为或者是动作,也就是说这个action可以改变state,而且也是改变state的唯一方法。

    const action={
      type:'ADD_TODO', //action名称,必须存在
      payload:'redux'
    }
复制代码

4.4 store.dispatch( )

store.dispatch( )是view发出Action的唯一办法,这里解释一下,在view中,用户触发一个行为或者简单理解点击某一个按钮,这时候如果需要修改state值,就需要触发action,而store.dispatch接收一个Action作为参数,将它发送给store通知store来改变state。

    const action = {
        type:'ADD_TODO',
        payload:'redux'
    };
    store.dispatch(action);
复制代码

4.5 Reducer

Store收到Action以后,必须给出一个新的state,这样view才会发生变化。这种state的计算过程就叫做Reducer。 Reducer是一个纯函数,他接收Action和当前state作为参数,返回一个新的state。

    const reducer =(state,action)=>{
      switch(action.type){
        case ADD_TODO:
            return newstate;
        default return state
      }
    }

5.5 redux实现原理

const createStore = (reducer, initialState) => {
  let state = initialState;
  let listeners = [];
  const getState = () => state;
  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach(listener => listener());
    return action
  };
  const subscribe = (listener) => {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter(l => l !== listener);
    }
  };
  // 在创建仓库的时候,内部会先派发一个初始动作,目的是初始化state
  dispatch({type:'@@REDUX_INIT'})
  return { getState, dispatch, subscribe };
};

11.React Fiber

React Fiber可以理解为:

React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态 React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态

React15及以前,Reconciler采用递归的方式创建虚拟DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,造成卡顿。

为了解决这个问题,React16递归的无法中断的更新重构为异步的可中断更新,由于曾经用于递归的虚拟DOM数据结构已经无法满足需要。于是,全新的Fiber架构应运而生

Fiber包含三层含义:

  1. 作为架构来说,之前React15Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack ReconcilerReact16Reconciler基于Fiber节点实现,被称为Fiber Reconciler
  2. 作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息。
  3. 作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)

这种将长任务分拆到每一帧中,像蚂蚁搬家一样一次执行一小段任务的操作,被称为时间切片(time slice)

cpu的瓶颈

解决CPU瓶颈的关键是实现时间切片,而时间切片的关键是:将同步的更新变为可中断的异步更新

IO的瓶颈

网络延迟是前端开发者无法解决的。如何在网络延迟客观存在的情况下,减少用户对网络延迟的感知?

React给出的答案是将人机交互研究的结果整合到真实的 UI 中

react15架构中递归更新的缺点

由于递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿。

在上一节中,我们已经提出了解决办法——用可中断的异步更新代替同步的更新

React16架构

React16架构可以分为三层:

  • Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
  • Reconciler(协调器)—— 负责找出变化的组件
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上

可以看到,相较于React15,React16中新增了Scheduler(调度器) ,让我们来了解下他。

Scheduler(调度器)

既然我们以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器有剩余时间时通知我们。

其实部分浏览器已经实现了这个API,这就是requestIdleCallback (opens new window)。但是由于以下因素,React放弃使用:

  • 浏览器兼容性
  • 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的requestIdleCallback触发的频率会变得很低

基于以上原因,React实现了功能更完备的requestIdleCallbackpolyfill,这就是Scheduler。除了在空闲时触发回调的功能外,Scheduler还提供了多种调度优先级供任务设置。

Reconciler(协调器)

我们知道,在React15中Reconciler是递归处理虚拟DOM的。让我们看看React16的Reconciler (opens new window)

我们可以看见,更新工作从递归变成了可以中断的循环过程。每次循环都会调用shouldYield判断当前是否有剩余时间。

/** @noinline */
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

那么React16是如何解决中断更新时DOM渲染不完全的问题呢?

在React16中,ReconcilerRenderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记,类似这样:

export const Placement = /*             */ 0b0000000000010;
export const Update = /*                */ 0b0000000000100;
export const PlacementAndUpdate = /*    */ 0b0000000000110;
export const Deletion = /*              */ 0b0000000001000;

整个SchedulerReconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer

Renderer(渲染器)

Renderer根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作。

优先级调度

    case ImmediatePriority:
    case UserBlockingPriority:
    case NormalPriority:
    case LowPriority:
    case IdlePriority:

不同优先级意味着什么?不同优先级意味着不同时长的任务过期时间

var timeout;
switch (priorityLevel) {
  case ImmediatePriority:
    timeout = IMMEDIATE_PRIORITY_TIMEOUT;
    break;
  case UserBlockingPriority:
    timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
    break;
  case IdlePriority:
    timeout = IDLE_PRIORITY_TIMEOUT;
    break;
  case LowPriority:
    timeout = LOW_PRIORITY_TIMEOUT;
    break;
  case NormalPriority:
  default:
    timeout = NORMAL_PRIORITY_TIMEOUT;
    break;
}

var expirationTime = startTime + timeout;

其中:

// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;

可以看到,如果一个任务的优先级ImmediatePriority,对应IMMEDIATE_PRIORITY_TIMEOUT-1,那么

var expirationTime = startTime - 1;

则该任务的过期时间比当前时间还短,表示他已经过期了,需要立即被执行。