一、深入响应式原理
Vue 的数据驱动除了数据渲染 DOM
之外,还有一个很重要的体现就是数据的变更会触发 DOM
的变化。
下面我们就分析一下用户交互或者是其它方面导致数据发生变化重新对页面渲染的原理。
Vue的响应式
如下示例,说明当数据变更,会自动触发 DOM
重新渲染:
<div id="app"></div>
// 这里的Vue是自己实现简版的Vue,render函数这里并没有返回虚拟DOM
const vm = new Vue({
el: '#app',
data: {
message: 'vue-1'
},
render() {
return this.message
}
})
setTimeout(() => {
vm.message = 'vue-' + Math.floor(Math.random() * 100)
}, 1000)
当我们去修改 vm.message
的时候,div容器内容也会渲染成新的数据,那么这一切是怎么做到的呢?
Vue背后帮我们做了哪些事情?
在分析前,我们先直观的想一下,如果不用 Vue 的话,我们会通过最简单的方法实现这个需求:定时器过1000ms
之后,数据被修改,然后手动操作 DOM 重新渲染。这个过程和使用 Vue 的最大区别就是多了一步“手动操作 DOM 重新渲染”。这一步看上去并不多,但它背后又潜在的几个要处理的问题:
-
我需要修改哪块的 DOM?
-
我的修改效率和性能是不是最优的?
-
我需要对数据每一次的修改都去操作 DOM 吗?
-
我需要 case by case 去写修改 DOM 的逻辑吗?
如果我们使用了 Vue,那么上面几个问题 Vue 内部就帮你做了,那么 Vue 是如何在我们对数据修改后自动做这些事情呢,接下来我们将进入一些 Vue 响应式系统的底层的细节。
二、响应式对象
可能很多小伙伴之前都了解过 Vue.js 实现响应式的核心是利用了 ES5 的 Object.defineProperty
,这也是为什么 Vue.js 不能兼容 IE8 及以下浏览器的原因,我们先来对它有个直观的认识。
Object.defineProperty
Object.defineProperty
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象,先来看一下它的语法:
Object.defineProperty(obj, prop, descriptor)
obj
是要在其上定义属性的对象;prop
是要定义或修改的属性的名称;descriptor
是将被定义或修改的属性描述符。
比较核心的是 descriptor
,它有很多可选键值,具体的可以去参阅它的文档。这里我们最关心的是 get
和 set
,get
是一个给属性提供的 getter 方法,当我们访问了该属性的时候会触发 getter 方法;set
是一个给属性提供的 setter 方法,当我们对该属性做修改的时候会触发 setter 方法。
一旦对象拥有了 getter 和 setter,我们可以简单地把这个对象称为响应式对象。那么 Vue.js 把哪些对象变成了响应式对象了呢,接下来我们从源码层面分析。
简版Vue
先看下简版Vue的实现:
import { Watcher } from './Watcher'
import { observe } from './observe'
export default class Vue {
constructor(options = {}) {
this._init(options)
}
_init(options) {
this.$options = options
this._initState()
if (this.$options.el) {
this.$mount(this.$options.el)
}
}
_initState() {
this._watchers = []
if (this.$options.data) {
this._initData()
}
}
_initData() {
let data = this.$options.data
try {
data = this._data = typeof data === 'function'
? data.call(this, this)
: data || {}
} catch (e) {
data = {}
} finally { }
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
proxy(this, '_data', key)
}
// observe data
observe(data, true)/* asRootData */
}
$mount(el) {
this.$el = el && document.querySelector(el)
let updateComponent = () => {
this._update(this._render())
}
new Watcher(this, updateComponent, () => { }, {}, true)
return this
}
_render() {
const { render } = this.$options
let val
try {
val = render.call(this)
} catch (e) { } finally { }
return val
}
_update(val) {
this.$el = this.__patch__(this.$el, val)
}
__patch__($el, val) {
// TODO
$el.innerText = val
return $el
}
}
_initState
在 Vue 的初始化阶段,_init
方法执行的时候,会执行 _initState()
方法。
源码 initState
方法主要是对 props
、methods
、data
、computed
和 wathcer
等属性做了初始化操作。这里我们主要分析 data
,对于其它属性的初始化我们之后再详细分析。
_initData
data
的初始化主要过程做两件事,一个是对传入 data
对象挂载到 vm._data
下,然后将其进行属性遍历,通过 proxy
函数把每一个属性 vm._data.xxx
都代理到 vm.xxx
上;另一个是调用 observe
方法观测整个 data
的变化,把 data
也变成响应式。
proxy
首先介绍一下代理,代理的作用是把 data
上的属性代理到 vm
实例上,这也就是为什么我们定义了如下 data
,却可以通过 vm
实例访问到它。
const vm = new Vue({
el: '#app',
data: {
message: 'vue-1'
},
render() {
return this.message
}
})
console.log('message:', vm.message)
我们可以通过 vm.message
访问到我们定义在 data
中的 message
,这个过程发生在 proxy
阶段:
function proxy(target, sourceKey, key) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: function proxyGetter() {
return this[sourceKey][key]
},
set: function proxySetter(val) {
this[sourceKey][key] = val
}
})
}
proxy
方法的实现很简单,通过 Object.defineProperty
把 target[sourceKey][key]
的读写变成了对 target[key]
的读写。所以对于 data
而言,对 vm._data.xxx
的读写变成了对 vm.xxx
的读写,而 data
对象就是挂载到 vm._data
下,反之我们通过 vm.xxx
就可以访问到定义在 data
对象中的 xxx
属性了。
observe
observe
的功能就是用来监测数据的变化。
/**
* 尝试为某个值创建一个观察者实例,
* 如果成功观察到该观察者,则返回新观察者,
* 如果该值已经包含一个观察者,则返回现有观察者。
* @param value
* @param asRootData
*/
export function observe(value, asRootData) {
if (!isObject(value)) {
return
}
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
Array.isArray(value) ||
isPlainObject(value)
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
observe
方法的作用就是给对象类型数据 value
添加一个 Observer
实例对象 ob
,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer
实例对象。接下来我们来看一下 Observer
的作用。
Observer
Observer
是一个类,它的作用是给数据对象 value
添加 Observer
的实例对象,即保存到 value
的 __ob__
不可枚举属性下,还遍历该数据对象 value
的属性来添加 getter 和 setter,用于依赖收集和派发更新:
/**
* 附加到每个观察对象的Observer类。
* 附加后,观察者将目标对象的属性转换为
* 用于收集依赖关系并调度更新的getter/setter
*/
export class Observer {
constructor(value) {
this.value = value
this.vmCount = 0
// 缓存当前实例到 value.__ob__ 下,并且不作枚举
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 数组 TODO
} else {
this.walk(value)
}
}
/**
* 遍历所有属性并将它们转换为getter/setter。
* 仅当值类型为Object时才应调用此方法
* @param obj
*/
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
Observer
的构造函数逻辑很简单,通过执行 def
函数把自身实例添加到数据对象 value
的 __ob__
属性上,def
的定义如下:
/**
* Define a property.
*/
function def(obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
def
函数是一个非常简单的Object.defineProperty
的封装,这就是为什么我在开发中输出 data
上对象类型的数据,会发现该对象多了一个 __ob__
的属性。
回到 Observer
的构造函数,接下来会对 value
做判断,对于数组这里暂不分析,对于纯对象,则调用 walk
方法,即遍历对象的 key 调用 defineReactive
方法,那么我们来看一下这个方法是做什么的。
defineReactive
defineReactive
的功能就是定义一个响应式对象,给数据对象动态添加 getter 和 setter,此时这两个方法是不会被调用的:
/**
* 在对象上定义反应性属性。
* @param obj
* @param key
* @param val
* @param constructor
* @param shallow 是否深度观测
*/
export function defineReactive(obj, key, val, customSetter, shallow) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 递归观测,当然前提val不是基础类型,observe中已作判断
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
console.log('getter:', value)
/***********************依赖收集*************************/
if (Dep.target) {
dep.depend()
}
/***********************依赖收集*************************/
return value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
console.log('setter:', newVal)
// 新旧的值没有发生改变
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (customSetter) {
customSetter()
}
// 用于没有setter的访问器属性
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 递归观测,当然前提val不是基础类型,observe中已作判断
childOb = !shallow && observe(newVal)
/***********************派发更新*************************/
dep.notify()
/***********************派发更新*************************/
}
})
}
defineReactive
函数拿到数据对象 obj
的属性描述符,然后对子对象递归调用 observe
方法,这样就保证了无论 obj
的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj
中一个嵌套较深的属性,也能触发 getter 和 setter。最后利用 Object.defineProperty
去给 obj
的属性 key
添加 getter 和 setter。而关于 getter 和 setter 的具体实现,我们会在之后介绍。
总结
这一节我们介绍了Vue会把数据对象 data
等变成响应式对象,在创建过程中,如果发现子属性也是对象,则将递归地把该对象变成响应式的。还介绍了响应式对象,核心就是利用 Object.defineProperty
给对象的属性添加了 getter 和 setter,目的就是为了在我们访问数据或者修改数据的时候能自动执行一些逻辑:getter 做的事情是依赖收集,setter 做的事情是派发更新,接下来的我们会重点对这两个过程分析。