Vue vs React

228 阅读7分钟

MVC、MVVM

MVC

  • M-Model 数据
  • V-VIew 视图、界面
  • C-Controller 控制器、逻辑处理

通常是使用controller更新model,视图从model中读取数据渲染,当有用户输入时,由控制器去更新模型,并通知视图更新

缺点:controller任务太重,项目越大,controller越复杂,不利于维护,还有就是不灵活

MVVM

  • Model-模型、数据
  • View-视图、模版(视图和模型是分离的)
  • ViewModel-连接Model和View,“桥”
<body>
  // view
  <div id="app">
    <ul>
      <li v-for="i in list" :key="i.id">{{i.name}}</li>
    </ul>
    <button @click="handleAdd">+</button>
  </div>

  <script src="../../dist/vue.js"></script>

  <script>
    // model
    var list = []
    // ViewModel
    var vm = new Vue({
      el: '#app',
      data: {
        list
      },
      methods: {
        handleAdd() {
          this.list.push({id: Math.floor(Math.random()*1000),name: 1})
        }
      }
    })
  </script>
</body>

在MVVM中引入了viewModel概念,只关心数据和业务的处理,不关心view如何处理数据,view、model都可以独立出来,任何一方改变也不一定需要改变另一方,并且可以将复用的逻辑放到ViewModel中,让多个view复用ViewModel.

以Vue为例,ViewModel就是组件的实例,View就是模板,Model在引入Vuex后,是完全可以和组件分离的。

在MVVM中还引入了一个隐式的Binder层,实现了View、ViewModel绑定,以Vue为例,就是Vue通过解析模板中的插值、指令来实现View和ViewModel的绑定.

对于MVVM,最重要的不是通过双向绑定将View、ViewModel绑定,而是通过ViewModel将视图中的状态和用户的行为分离出一个抽象。

路由原理

前端路由实现的本质就是监听url变化,然后匹配路由规则,显示对应的页面,且不用刷新页面。

路由实现模式:hash、history

  • hash

www.test.com/#/ 就是hash url,当 # 后边hash变化时,通过hashchange监听url变化。无论hash如何变化,服务器端收到的url都是www.test.com?

window.addEventListener('hashchange', () => { 
  // ... 具体逻辑
})

实现简单,兼容性也好

  • History

h5新功能,主要用history.pushState、 history.replaceState改变url。通过这种模式改变url不会引起页面刷新,只会更新浏览器的历史记录

// 新增历史记录
history.pushState(stateObject, title, URL)
// 替换当前历史记录 
history.replaceState(stateObject, title, URL)

当用户点击后退时,触发popState事件

window.addEventListener('popstate', e => { 
  // e.state 就是 pushState(stateObject) 中的stateObject
  console.log(e.state) 
})

两种模式的对比:

  • hash只能修改#后边的,history可以设置任意url
  • history可以通过api添加任意类型数据到历史记录,hash只能修改hash值
  • hash无需后端配置,兼容性好。history在用户手动输入地址或刷新后发起url请求,需要后端配置

Vue

virtual dom

虚拟dom,就是使用js对象来描述一个真实的dom结构,真实dom属性过于多,任何操作都是性能杀手,基本上所有的优化,都会提到少操作dom。

我们在任何数据修改后,操作dom之前,都对数据作对比

虚拟dom提升性能是个优势,其他的优势还在于:

  1. 虚拟dom作为兼容层,可以跨端开发,毕竟只是个对象,可以实现非Web应用
  2. 可以渲染到ssr、同构渲染等
  3. 实现组件的高度抽样化?

生命周期

beforeCreate

基本上是初始化数据之外的各种方法,都在init方法里

  vm._self = vm
  // 初始化组件属性,比如:$children、$parent、ref等
  initLifecycle(vm)
  // 添加各种事件
  initEvents(vm)
  // 在实例上添加createElement
  initRender(vm)
  callHook(vm, 'beforeCreate')
export function initRender (vm: Component) {
  ...
  // 主要用在render函数的 with(this){} 里边的_c就是这里,与用户无关
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // render(h){},里的h就是这里,就是经常提到的渲染函数
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) 
  // 使用科里化定义上边两个方法,目的就是保留当前的实例,以便在with(this)中使用
  ...
}

created

基本上所有的数据拦截,都在这里了

  // 获取inject,并对其observe
  initInjections(vm) // resolve injections before data/props
  // 这里处理的数据就多了,data、methods都有
  initState(vm)
  // 将project挂载到实例的_provided上
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')

beforeMount

挂载之前,init方法最后一步

  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
$mount

执行原型上的$mount方法,生成render的mount方法

找render,如果有用户自定义的render。

  1. 如果没有找template,找到后通过compileToFunctions编译,生成render函数,
  2. 中间又经过ast、代码优化、代码生成。
  3. 将render挂到实例的options
$mount

包含mountComponent的mount方法 检查是否有render,

callHook(vm, 'beforeMount')

mounted

注册更新函数,创建Watcher实例,至关重要

  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  ...
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

Watcher实例化时,会执行一次updateComponent

  1. 执行_render函数,会触发依赖收集的get取值,生成vnode
  2. 执行_update,这里已经有vnode了,先将vnode挂到实例的_vnode属性上。执行_patch_,将vnode渲染到页面
  3. 真实dom挂到实例的$el,完毕
callHook(vm, 'mounted')

beforeUpdate

在Watcher实例化时候,注册的 修改页面数据时,触发set,执行dep.notify(),就是以上Watcher中的before

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

updated

watcher.run(),内部还是执行updateComponent

  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

重新执行_render,得到新的vnode

传入新旧节点,做patch。更新完以后:

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}

keep-alive

独有生命周期:activated、deactivated,切换时不会进行销毁,是缓存到内存中

切换组件时,保存一些组件状态防止多次渲染,就可以使用keep-alive组件报过组要保存的组件

extend

扩展一个组件生成一个构造函数,通常与$mount配合使用

  const Ctor = Vue.extend(Component)
  const comp = new Ctor({
    propsData: props
  })
  // 只挂载,不插入。源码中$mount后会将真实dom挂到$el
  comp.$mount();
  document.body.appendChild(comp.$el)
  comp.remove = () => {
    // 移除dom document.body.removeChild(comp.$el) // 销毁组件
    comp.$destroy();
  }

let SuperComponent = Vue.extend(Component)
new SuperComponent({
  created() {
    console.log(1)
  }
})
new SuperComponent().$mount('#app')

mixin、mixins

  • mixin 多用于混入,会影响到每个组件,插件一般就是基于这个做的初始化
  let Vue;
  class VueRouter {
    constructor(options) {
      this.$options = options; // path、component映射 
      this.routeMap = {};
      // current保存当前hash、vue使其是响应式的 
      this.app = new Vue({
        data: {
          current: "/"
        }
      });
    }
  }
  // 插件逻辑:注册$router,初始化router 
  VueRouter.install = function (_Vue) {
    Vue = _Vue;
    Vue.mixin({
      beforeCreate() {
        // router选项存在确定是根组件 
        if (this.$options.router) {
          // 执行一次,将router实例放到Vue原型,以后所有组件实例就均有$router 
          Vue.prototype.$router = this.$options.router;
          this.$options.router.init();
        }
      }
    });
  };
  export default VueRouter;
  • mixins 如果多个组件需要引入相同的数据,或者相同的业务逻辑,就可以将这些抽离出来,通过mixins混入代码
import billMixin from "../mixin/bill";
export default {
  name: "resubmit",
  mixins: [billMixin],
  components: {
    ...
  }
}

混入的钩子,先于组件同名钩子函数执行,同名选项会合并

响应式原理

- 缺陷 通过下标方式修改数组数据或者给对象新增属性,组件不会重新渲染

  var vm = new Vue({
    el: '#app',
    data: {
      obj: {
        cur: 0
      },
      arr: [1]
    },
    methods: {
      handleClick() {
        this.obj.cur++
        this.obj.num = 10
      },
      handleClick2() {
        this.obj.num ++
        this.arr[0] = 30
      }
    }
  })

使用$set,对象是defineReactive(ob.value, key, val),又做了一次响应式处理,数组是将修改下标的值,转为使用slice修改值

数组是重写了7个方法

编译过程

parse Ast

通过各种正则表达式去匹配模板内容,将内容提取出来做各种逻辑操作,生成最基本的ast

optimize Ast

只是对静态节点打上标记static,以便在patch过程直接跳过。 官方还说静态节点,在每次从新渲染就不用了再重新创建了

generate code

nextTick

可以让我们在下次dom更新循环结束后,执行延时回调,用于获取更新后的dom

let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  ...
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }

React

生命周期

Fiber本质是虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧。将同步渲染改成异步渲染,在不影响体验的情况下分段计算、更新

对于异步渲染,生命周期分两个阶段:reconciliation、commit,前者可以打断,可能会调用多次,后者不能打断

Reconciliation阶段:componentWillMount、componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate

commit阶段:componentDidMount、componentDidUpdate、componentWillUnmount

除了shouldComponentUpdate,其他周期函数尽量少用。新加了两个周期函数解决这个问题

  1. getDerivedStateFromProps用于替换componentWillReceiveProps,该函数会在初始化和update时被调用。
  2. getSnapshotBeforeUpdate用于替换componentWillUpdate,该函数会在update后dom更新前被调用,用于读取最新dom结构

setState

  • 直接修改值,值会改变但是不会触发render,想触发需要forceUpdate
  this.state.counter += 1
  this.forceUpdate()
  • 批量执行,只会执行最后一次操作,{...{counter: 1}, ...{counter: 2}}
  componentDidMount() {
    this.setState({ counter: this.state.counter + 1 })
    this.setState({ counter: this.state.counter + 2 })
  }
  // 页面显示2

如果想让每次操作都生效,比如上例,页面想输出3,三种方法:定时器、传递函数、原生事件

总结:setState只有在合成事件和⽣生命周期函数中是异步的,在原 ⽣生事件和setTimeout中都是同步的,这⾥里里的异步其实是批量量更更 新

shouldComponentUpdate

PureComponent,底层就是实现了浅比较 state

// 函数组件:
const Test = React.memo(() => <div>PureComponent</div>)

组件通讯

  import React from 'react';
  const StoreContext = React.createContext('light');
  export {
      StoreContext
  }
  // parent组件
  render() {
      return (
          <StoreContext.Provider value="dark">
              <Child />
          </StoreContext.Provider>
      )
  }
  // 子组件
  render() {
    return (
      <StoreContext.Consumer>
        {(context) => <span>{context}</span>}
      </StoreContext.Consumer>
    )
  }

事件机制

React其实是自家实现了一套事件机制,jsx并没有将事件绑定到真实dom上,而是通过事件代理绑定到了document,这样做可以减少内存消耗,还能在组件销毁时,统一订阅和移除事件?

event.stopPropagation没用,要用event.preventDefault

抹平了浏览器之间的兼容问题,有跨平台的能力。

vue和react对比

虚拟dom

Vue组件外部是响应式,组件内部是虚拟dom。 源码中渲染组件时,mountComponent会实例化一个Watcher,后期通过Watcher来执行渲染函数。组件内部虚拟dom变化是通过dom diff比较

React里边全是虚拟dom

Vue表单可以使用v-model,相对于react更方便些,但v-model就是个语法糖,本质上和react写表单方式没啥区别

数据响应式

改变数据方式不一样,Vue修改状态相比react简单,react需要手动触发setState去改变,并且使用这个也有一些坑?。Vue底层使用依赖跟踪,页面渲染已经是最优了,但react还需要用户手动去优化这些方面的问题,就是说Vue已经为你考虑了很多,但是react给了你自由

React16以后,由于fiber架构,生命周期可能会触发多次

上手难度

React需要使用Jsx有上手成本,并需要一整套的工具链,但是完全可以通过js控制页面,更加灵活。

Vue使用模板语法,相比jsx,没那么灵活。但完全脱离工具链?,直接编写render函数就能直接在浏览器运行

Vue开始的定位就是尽可能的降低开发难度,React更多的是改变用户去接受它的概念和理念