前言:
上一节,我们介绍了依赖收集和依赖触发的原理,虽然知道是通过自定义属性(defineProperty)的get和set进行实现的,但还是不清楚具体实现的细节,以及怎么通过依赖收集和依赖触发实现响应式的,带着这样的疑问,开始探索
导读:
我将以图形化、流程化和代码层次逐一展开说明
官网的响应式原理:深入响应式原理
官网的解释:1、普通的 JavaScript 对象传给 Vue 实例的 data 选项时,vue会遍历此对象所有的属性.
2、属性遍历的时候,会为每个属性添加getter/setter方法
3、属性被访问时,收集当前的依赖【观察者】
4、属性被修改时,执行依赖【观察者】,更新属性
5、watcher把组件渲染的时候,对应的属性记录为依赖
6、属性被修改时,通知watcher【观察者】重新计算,关联的组件更新
复制代码
官网只是宏观上的描述了这样的一个过程,所以必须撸一波源码才能真正的理解其实现原理
代码分析
依然我们还是从源头开始看起,这里主要看数据的初始化部分(因为这部分是最重要的),你可以在instance/state.js下找到initState这个方法,然后代码块里有个initData这个方法
function initData (vm: Component) {
// 删减
// observe data
observe(data, true /* asRootData */)
}
复制代码
直接定位到最后一行
// observe data
observe(data, true /* asRootData */)
复制代码
observe工厂函数
调用了 observe 函数观测数据,observe 函数来自于 core/observer/index.js 文件,打开该文件找到 observe 函数:
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
复制代码
上面的代码,主要做了几件事
1、判断当前的值是否为一个对象或节点
2、是否存在__ob__属性,这里就是Observer的实例
3、如果不存在,满足一些条件后,然后创建Observer实例,并返回这个实例对象
复制代码
Observe构造函数
其实真正将数据对象转换成响应式数据的是 Observer 函数,它是一个构造函数,同样定义在 core/observer/index.js 文件下,如下是简化后的代码:
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
// 省略...
}
walk (obj: Object) {
// 省略...
}
observeArray (items: Array) {
// 省略...
}
}
复制代码
可以清晰的看到 Observer 类的实例对象将拥有三个实例属性
1、分别是 value、dep 和 vmCount 以及两个实例方法 walk 和 observeArray
2、Observer 类的构造函数接收一个参数,即数据对象。
复制代码
constructor
constructor (value: any) {
this.value = value // 实例对象的 value 属性引用了数据对象
this.dep = new Dep() // 实例对象的 dep 属性,保存了一个新创建的 Dep 实例对象
this.vmCount = 0 //
def(value, '__ob__', this) // 重点 创建__ob__属性是不可枚举的,避免枚举时当做属性被遍历
if (Array.isArray(value)) { // 如果是数组处理对象数组
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else { // 处理对象
this.walk(value)
}
}
复制代码
主要来看看这个方法def
def(value, '__ob__', this) // 重点
复制代码
举个例子
假设我们的数据对象如下:
const data = {
a: 1
}
那么经过 def 函数处理之后,data 对象应该变成如下这个样子:
const data = {
a: 1,
// __ob__ 是不可枚举的属性
__ob__: {
value: data, // value 属性指向 data 数据对象本身,这是一个循环引用
dep: dep实例对象, // new Dep()
vmCount: 0
}
}
复制代码
目的:为多层属性遍历添加依赖 上面代码主要做了:
1、实例对象的 value 属性引用了数据对象
2、实例对象的 dep 属性,保存了一个新创建的 Dep 实例对象
3、创建__ob__属性是不可枚举的,避免枚举时当做属性被遍历
4、如果为数组走处理数组逻辑,否则走对象处理逻辑(walk)
复制代码
walk对象处理
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
复制代码
后续就是依赖收集和触发的部分,可移步上一篇的分析.依赖收集和依赖触发我们暂时先跳过这部分,不做赘述,下面主要谈谈defineReactive方法下的get和set闭包内dep的引用
get方法下
if (Dep.target) {
dep.depend() // 重点 调用depend方法,收集当前依赖【watcher】到dep
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
// dep.depend() 方法时,addDep添加依赖 core/observer/watcher.js下
// 添加依赖
addDep (dep: Dep) {
const id = dep.id
// newDepIds避免本次get中重复收集依赖
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
// 避免多次求值中重复收集依赖,每次求值之后newDepIds会被清空,因此需要depIds来判断。newDepIds中清空
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
复制代码
1、在addDep中添加依赖,并避免对一个数据多次求值时,其观察者被重复收集。
2、newDepIds避免一次求值的过程中重复收集依赖
3、depIds 属性避免多次求值中重复收集依赖
复制代码
set方法下
dep.notify() // 执行收集的依赖
复制代码
Watcher类
watcher类的定义在core/observer/watcher.js中,代码如下:
export default class Watcher {
... //
// 构造函数
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
// 将渲染函数的观察者存入_watcher
vm._watcher = this
}
//将所有观察者push到_watchers列表
vm._watchers.push(this)
// options
if (options) {
// 是否深度观测
this.deep = !!options.deep
// 是否为开发者定义的watcher(渲染函数观察者、计算属性观察者属于内部定义的watcher)
this.user = !!options.user
// 是否为计算属性的观察者
this.computed = !!options.computed
this.sync = !!options.sync
//在数据变化之后、触发更新之前调用
this.before = options.before
} else {
this.deep = this.user = this.computed = this.sync = false
}
// 定义一系列实例属性
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.computed // for computed watchers
this.deps = []
this.newDeps = []
// depIds 和 newDepIds 用书避免重复收集依赖
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
// 兼容被观测数据,当被观测数据是function时,直接将其作为getter
// 当被观测数据不是function时通过parsePath解析其真正的返回值
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
// 除计算属性的观察者以外的所有观察者调用this.get()方法
this.value = this.get()
}
}
// get方法
get () {
...
}
// 添加依赖
addDep (dep: Dep) {
...
}
// 移除废弃观察者;清空newDepIds 属性和 newDeps 属性的值
cleanupDeps () {
...
}
// 当依赖变化时,触发更新
update () {
...
}
// 数据变化函数的入口
run () {
...
}
// 真正进行数据变化的函数
getAndInvoke (cb: Function) {
...
}
//
evaluate () {
...
}
//
depend () {
...
}
//
teardown () {
...
}
}
复制代码
watcher构造函数
由以上代码可见,在watcher构造函数中做了如下几件事:
-
将组件的渲染函数的观察者存入_watcher,将所有的观察者存入_watchers中,你可以通过$watchers访问到
-
保存before函数,在数据变化之后、触发更新之前调用 定义一系列实例属性
-
兼容被观测数据,当被观测数据是function时,直接将其作为getter;
-
当被观测数据不是function时通过parsePath解析其真正的返回值,被观测
-
数据是 'obj.name'时,通过parsePath拿到真正的obj.name的返回值
-
除计算属性的观察者以外的所有观察者调用this.get()方法
总结一下流程:
-
调用$mount()函数进入到挂载阶段
-
检查是否有render()函数,根据上述模版创建render()函数
-
调用了mountComponent()函数完成挂载,并在mountComponen()中定义并初始化updateComponent()
-
为渲染函数添加观察者,在观察者中对渲染函数求值 在求值的过程中触发数据对象str的get,在str的get中收集str的观察者到数据的dep中
-
修改str的值时,触发str的set,在set中调用数据的dep的notify触发响应 notify中对每一个观察者调用update方法
-
在run中调用getAndInvoke函数,进行数据变化。
-
在getAndInvoke函数中调用回调函数
-
对于渲染函数的观察者来说getAndInvoke就相当于执行updateComponent函数
-
在updateComponent函数中调用_render函数生成vnode虚拟节点,以虚拟节点vnode作为参数调用_update函数,生成真正的DOM
至此响应式流程已完结