Vue 数据响应式原理
Vue 数据响应式原理
Vue2 中的数据响应式原理用的是观察者模式,下面用一张流程图来简单说明一下观察者模式的原理。
从流程图不难看出,观察者模式的核心就是 Dep 和 Watcher 这两个对象。Dep 负责收集依赖(这里的依赖实际就是 watcher),并在监听到数据变化的时候发送通知。发送通知的过程实际就是调用 watcher.update() 方法。从而更新视图内容。
下面我们来详细看看 Vue 具体是如何实现数据响应式的。
我将 Vue 实现数据响应式的过程分为三步:
- 创建响应式数据,也就是创建 Observer 对象(这里会创建 dep 对象)
- 收集依赖和发送通知的过程
- 创建依赖 Watcher
创建响应式数据
Dep 对象是收集依赖的容器。没有容器又如何能装下依赖,所以在 Vue 实例化的时候都会对数据进行初始化,如 props,data 等数据都是要进行响应式处理的。因此,首先我们要知道 Vue 如何将这些普通的数据转换为响应式属性。
首先我们要知道 Vue 内有那些数据是响应式的,这里重点分析 data 属性的初始化。vue 处理 data 属性是在 Vue 实例初始化函数 _init
中进行的
_init
方法定义的地方是在:src/core/instance/init
而初始化 data 的过程是在上图的 initState
方法中,该方法是负责初始化我们的一些状态成员
initState
方法定义在:`src/core/instance/state
标红的地方就是初始化 data 的函数。
初始化 data 的入口我们已经找到了,由于代码比较长,这里我按自己的理解来说说它是如何初始化 data 的。
这里我们假设 data 的值为
data: {
name: 'vue',
age: 3
}
基于这个data 的值看看,Vue 是如何处理的
initData
在处理 data 响应式前,会对 data 内的参数进行检查,避免 data 内的 命名与 props 或者 methods 内的属性名重复,这里不详细讨论。
在 initData 函数最后有这样一段代码
将 data 传入到 observe函数进行处理,observe 函数是对某个对象进行响应式处理的一个函数
observe
路径:src/core/observer/index
observe 内的逻辑也相对比较简单
源码
function observe(value: any, asRootData: ?boolean): Observer | void {
// 判断传入的值是否是对象,不是直接返回空
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 判断传入的对象是否存在 __ob__ 属性,__ob__属性实际就是用于存储 observe
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 存在,则将 __ob__ 值赋值给 ob
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 不存在则 new 一个 observer 对象 并赋值给 ob
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
// 最后返回 ob
return ob
}
可以看到 observe 方法实际就是给一个对象添加一个值为 observer 对象的 __ob__
属性,所以接下来要看看 Observer 实例初始化做了什么事情
Observer 类
路径:src/core/observer/index
来看看类的 constructor 做了什么
{
...
constructor(value: any) {
this.value = value
// 新建一个 dep 对象用于收集依赖
this.dep = new Dep()
this.vmCount = 0
// 给监听的对象新增一个 __ob__ 属性,ob 就代表当前的 observer
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 如果需要观察的值是数组,则需要对该值的 proto 进行处理
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
// 通过 walk 函数处理需要监听的对象内部的属性
this.walk(value)
}
}
}
传入的值是 data,data 是
data: {
name: 'vue',
age: 3
}
所以根据上面的代码,最终会走到 this.walk(value)
(value 的值就是 data)
{
// walk 源码
walk(obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 通过 defineReactive 来使内部的属性是响应式的
defineReactive(obj, keys[i])
}
}
}
walk 是observe 的一个原型方法,它主要就是遍历对象内的属性,并通过 defineReactive 来将这些属性设置为响应式的
defineReactive
路径:src/core/observer/index
由于其内部代码较多,这里主要说说其核心步骤:
- 实例化 dep 对象为该属性收集依赖
- 尝试对该属性的值进行 observe
- 缓存初始的 getter/setter 方法,如果存在,这是为了防止用户自己定义过属性的 getter/setter
- 通过
Object.definProperty
重新该属性的描述符,这里比较重要的是 getter/setter
这里总结一下,我认为创建响应式数据最核心的函数是 defineReactive,在 defineReactive 中我认为最重要的是两件事情,收集依赖和发送通知。
下面来看看收集依赖的过程
收集依赖和发送通知的过程
收集依赖
收集依赖的过程实际是在 defineReative 中重新定义 getter 的过程中
先来看看 getter 内部做了什么
{
get: function reactiveGetter() {
// 获取属性对应的值,并调用初始的 getter
const value = getter ? getter.call(obj) : val
// 若 Dep.target 存在(target 一般指的是依赖),就会尝试为该属性收集依赖
if (Dep.target) {
// 当该属性被获取的时候,使用 dep.depend() 收集依赖
dep.depend()
if (childOb) {
// 若该属性的值是一个响应式数据,使用 childOb.dep.depend() 为该对象收集依赖
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
}
}
不难看出,当 get 方法实际就是一个收集依赖的过程,当该属性被获取的时候,它会尝试为该属性收集依赖,等待未来的时候去给这些依赖发送通知更新视图。
dep.depend() 方法就是用于收集依赖,该方法会将 Dep.target (watcher) 添加到 dep.subs 数组中,有兴趣可以看看源码。
发送通知
发送通知的操作实际就是在修改响应式数据后进行的,而修改值的操作是在 setter 中,所以我们看看setter 内部的原理
{
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
}
前面都是基于对参数的修改或判断,最重要的时候后面两行代码
// 深度监听,对设置的参数进行深度的监听
childOb = !shallow && observe(newVal)
// 发送通知
dep.notify()
当响应式属性的值被修改后,会触发 dep.notify() ,调用所有 watcher 的 update 方法来进行更新操作
创建依赖 Watcher
上面说了收集依赖的过程。那么依赖又是什么时候生成的呢。Watcher 有三种类型,computed watcher、侦听器watcher、渲染watcher。
我们来看看渲染 watcher ,也就是负责渲染实例的watcher。在 mountComponent 中。
渲染 wathcer 的入口
路径:src/core/instance/lifecycle
-> mountComponent
我们先来看看 watcher 的构造函数,它在 src/core/observer/wathcer
文件中
我们看看它前几个参数的含义
根据构造函数内的形参,我们来看看 上一张图传递了什么参数
- vm:vue 实例
- expOrFn 表达式:updateComponent 函数
- cb 回调:noop 空函数
- options 配置参数:一个含有 before 函数的对象
- isRenderWatcher 是否渲染 watcher:true
渲染 wathcer 初始化
然后我们来看看 它初始化做了什么,由于源码比较长,这里我就按自己理解总结一下:
- 参数初始化
- 给 this.getter 赋值,若 expOrFn 是函数就直接赋值给 this.getter,若不是则用 parsePath 函数处理 expOrFn 的结果赋值给 this.getter。
- 执行 this.get() 并将其返回值赋值给 this.value
所以这里着重看 this.get() 执行的过程
this.get()
{
get() {
// pushTarget 会将当前的 watcher 入栈,并记录到 Dep.target 中
pushTarget(this)
let value
const vm = this.vm
try {
// 调用 this.getter 这一步是最核心的步骤,收集依赖
// 再调用 this.getter 的时候,会触发用户传入的 函数 或者 字符串表达式,在这个过程中,会触发属性的 get 方法,对于响应式数据,在这一步中会收集所有的依赖
// 如 渲染 watcher 中,this.getter 就是 updateComponent,该方法用于更新视图
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
// 深度监听
if (this.deep) {
traverse(value)
}
// 清理工作,清除栈中的 watcher
popTarget()
// 将 watcher 从 dep.subs中移除,以及将 dep 从 watcher.deps 中移除,因为整个 watcher 以及完成,不再需要了
this.cleanupDeps()
}
return value
}
}
在 this.get() 中有几个比较核心的步骤:
pushTarget()
在上面可以知道,在收集依赖的时候,会将 Dep.target 添加到 dep.subs 中。
那么 Dep.target 的值是什么、又是什么时候赋值的?
Dep.target 实际就是 watcher 对象,而pushTarget(this) 的核心就是将当前的 watcher 赋值给 Dep.target。
pushTarget 实际还会将当前的 wacther 进行入栈的操作,这个主要是为了解决父子组件嵌套的问题。
这一个步骤我称为:确认依赖
this.getter()
当在获取某个响应式属性的时候会触发其 getter,从而触发收集依赖的操作。
那么,它又是在什么时候去触发获取响应式属性这个操作的呢?
其实就是 this.getter()。我们知道 this.getter 实际就是用户传入的函数表达式或者函数,但最后都会转换为函数,当执行 this.getter() 时。在函数内部会触发某些响应式属性的 getter 方法,从而触发收集依赖的操作。并等待在合适的时候向这些依赖发送通知。
并将 this.getter() 的返回值赋值给 value,并在最后返回
这一步骤我成为:触发依赖收集
清理工作
在最后还会进行清理工作,主要是清除栈中的 watcher
总结
确认依赖
this.getter()
当在获取某个响应式属性的时候会触发其 getter,从而触发收集依赖的操作。
那么,它又是在什么时候去触发获取响应式属性这个操作的呢?
其实就是 this.getter()。我们知道 this.getter 实际就是用户传入的函数表达式或者函数,但最后都会转换为函数,当执行 this.getter() 时。在函数内部会触发某些响应式属性的 getter 方法,从而触发收集依赖的操作。并等待在合适的时候向这些依赖发送通知。
并将 this.getter() 的返回值赋值给 value,并在最后返回
这一步骤我成为:触发依赖收集
清理工作
在最后还会进行清理工作,主要是清除栈中的 watcher
总结
当以上三个步骤都做完后,当响应式数据的值发送改变的时候,就会触发其 dep.notify() 方法,从而触发其 dep.subs 数组中所有依赖的 update() 方法,从而进行了更新的操作,这个操作可能时更新视图,也可能是更新某些值。