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的实现数据驱动的基本工作流程是这样的:
- 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的数据驱动视图原理的异同。