React Vs Vue 之数据驱动视图

4,602 阅读6分钟

mvc框架中数据的变化驱动视图的变化,当数据改变时,比如num由1变为10:

// react
this.setState({num: 10})
// vue
this.data.num = 10

只需执行简单的代码,就是引起页面视图的变化。两个框架之间是如何做的的呢?

1. React数据驱动视图

要想了解数据驱动视图的过程,首先要了解react的渲染过程:

1. 编写:我们编写的组件都是继承自React.Component(函数式组件除外),组件内部的视图模板文件则
采用jsx的语法进行编写
2. 编译:将我们编写的组件实例编译成一个个的虚拟dom节点(本质就是json对象)
3. 挂载:将虚拟dom节点编译成真实dom,并挂载到界面

这只是渲染过程的非常简化的说法,实际每个过程细节都很复杂,在此先不做细节讨论。 而数据驱动视图 this.setState({num:10}) 并非直接导致真实dom的改变,它首先映射到虚拟的dom节点,虚拟dom节点的挂载导致页面视图的变化。

从流程上看,似乎多此一举,直接操作dom节点改变数据不是更直接,为什么要绕弯操作虚拟dom?首先dom操作的会导致浏览器的重绘重排,性能开销比较大。当我们在一个方法中出现了多次数据变化时,每次都要操作dom很浪费性能。有了虚拟dom后,把多次的数据变化先映射到虚拟dom中,最后进行一次挂载,相当于只操作了一次真实dom,有效的降低了性能开销。

今天的主角:setState

上文说到this.setState()会将变化的数据首先映射到虚拟的dom中,最后一并进行挂载使页面数据更新。那setState()是异步的吗?从表现上面来看确实是异步的,而官方的说法更是让人摸不到头脑:setState()可能是异步的。下面以一个计数器为例子,逐步的揭开setState的神秘面纱:

class Counter extends React.Component {
  state = {count: 0}
  add = () => {
    this.setState({count: this.state.count+1})
    console.log('count:', this.state.count)
  }
  render() {
    return (
      <div>
        <h1>curNumber: {this.state.count}</h1>
        <button onClick={this.add}>add</button>
      </div>
    )
  }
}
// 点击add自增count时,页面上curNumber展示的count总比 add函数中打印的count大1
// 这种用法是开发中最为频繁使用的方法,当我们在add函数中需要或获取最新的count时,利用setState的函数的的第二个参数:回调函数中获取即可:
// this.setState({count: this.state.count+1},() => {
//    console.log('count:', this.state.count)
// })

看完上面的案例,可能会认为setState提供了一个回调函数作为参数,这不就是明显的异步吗?为什么说setState可能是异步的?下面add换个写法:

add = () => {
    setTimeout(() => {
      this.setState({count: this.state.count+1})
      console.log('count:', this.state.count)
    }, 50);
}
// 点击add自增count时,页面上展示的curNumber和打印出的count是一致的!setState()又同步了

为什么setState放到异步函数中执行表现出来同步的效果,同步执行却表现了异步的效果,这需要从根本上理解setState是如何工作的:

<!-- 模板 -->
<h1>
    count:<span id="count">0</span>
</h1>
<button onclick="add()">add</button>
let state = {count: 0}
let cacheState = {count: 0}
function setState(stateObj) {
  cacheState = {...cacheState,...stateObj}
}
function render (id,value) {
  document.getElementById(id).innerHTML = value
  state = {...state,...cacheState}
}
function add () {
  // setTimeout(() => {
    setState({ count: state.count + 1 })
    console.log('count:', state.count)
  // }, 50)
  render('count', cacheState.count)
}
// 点击add时 setState表现出异步 当放开 setTimeout的注释时,setState又表现为同步 和 react中表现一致

这是一个极其简单的例子但却表明了setState的工作原理:

当在方法中调用setState时,首先将设置的state状态值缓存在cacheState中(源码中表现为一个更新器),在方法中的代码都执行完成后,再调用render方法渲染出最新的界面,state中的值此时才变为最新值。

所以当add方法执行时,调用setState只是同步的将count值存在了cacheState中,而真正的state里的count值是在render页面后才更新,所以在setState()后取state的值不会是最新值。

2. Vue数据驱动视图

要了解vue的数据驱动视图原理,就要了解Object.defineProperty,vue就是以此为基础实现的数据驱动。vue利用Object.defineProperty的“劫持”作用,实现了对data对象中属性改变时的监听。

let data  = {name: 'tom', age: 10}
Object.defineProperty(data,'name',{
    get: function () {
        console.log('获取对象属性')
    },
    set: function () {
        console.log('设置对象属性')
    }
})
data.name  // 读取对象属性时, 会走进get方法
data.name = 'jack' // 设置对象属性时,会走进set方法 

如此,对data对象的属性改变就会被监听到,从而实现“劫持作用”。vue的实现数据驱动的基本工作流程是这样的:

  1. data函数中返回的对象中属性递归,每个属性都会注册Object.defineProperty方法,这样每个属性的改变都可以被监听到。这也是为什么vue可以设置状态对象的二级属性,而react不可以。
// vue 
data () {
    return {
        person: {name: 'tom', age: 10}
    }
}
// this.person.name = 'jack'

// react
state = {
    person: {name: 'tom', age: 10}
}
// this.setState({person: {name: 'jack', age: 10}})

2.每个组件都有一个watcher观察者实例,类似于react组件中对应的updater实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

这个过程最为关键的点在依赖项的setter触发时通知watcher,但watcher并不会马上触发关联的组件进行重新渲染,Vue 在更新 DOM 时是异步执行的。

<template>
    <div ref="name">{{name}}</div>
    <button @changeName="changeName">click</button>
</template>
<script>
    export default {
        data () {
            name: 'tom'
        },
        methods: {
            changeName () {
                this.name = 'jack'
                console.log(this.name)  // jack
                console.log(this.$refs.name.innerHTML) // tom
            }
        }
    }
</script>

点击按钮时,可以看到如图所示结果。data中的属性更新了,页面上没有更新,证明了上述观点。那什么时候更新呢?这个道理和react一样,为了防止多次更新dom,提高效率,setState后所有改变的状态都收集到updater,页面ui和state状态数据始终保持一致,组件更新时state和ui是一致更新的,但和vue在表现手法上还是有区别的,vue则是直接修改了data中的状态,但暂时先不更新页面,当方法结束后,再根据状态更新页面。

以上从宏观上分析了react和vue的数据驱动视图原理的异同。