通过上篇的初步分析和解读,大家对Vue应该都有了大概的了解,这一篇主要针对源码进行分析。
相信大家看完之后,自己动手写实现响应式原理 的核心代码也不成问题。
这是下篇,还有上篇
以下代码略有删减,但不印象总体流程分析,部分删减的依赖代码可详细查看vue源码
简介
在解读之前,我们先看一下这三个类之间的调用时序图,这样有助于大家理清这几个类之间的关系。如有错误请大家指正。
从上图可以看出初始化时会调用Watcher.get()方法,用于收集依赖关系,然后将依赖项存放在Dep类中。当用户重新赋值时,直接调用依赖项中的update方法。但最后的依然会调用Watcher.get(),重复上面的流程,如此往复,形成闭环。
Observer类源码解读
首先是Observer类,其核心思路是对传入的value,深度遍历每一项并使用defineProperty劫持get和set方法
const NO_INIITIAL_VALUE = {} // 该对象setter方法为空
class Dep {}
class Observer {
dep
constructor(value, shallow) {
// 每个Observer实例下都挂载了一个Dep
this.dep = new Dep()
Object.defineProperty(value, '__ob__', { value: this})
if (Array.isArray(value)) {
// shallow 默认是 undefined,进行深度观测。
// 当观测vue属性$attr,$lisenter时shallow为 false
if (!shallow) {
this.observeArray(value)
}
} else {
this.walk(value, shallow)
}
}
// 用于Object类型部署defineProperty
walk(obj, shallow) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
defineReactive(obj, key, NO_INIITIAL_VALUE, undefined, shallow)
}
}
// 用于Array类型部署defineProperty
observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
下面是Observe类中提供的observe方法,其主要功能是将value及其属性值实例化为Observer对象,与Observer类形成递归的关系,循环遍历所有子属性,直到属性值是一个不可扩展的类型,比如string,number等
let shouldObserve = true
function observe(value, shallow){
let ob
if (value.__ob__ instanceof Observer) {
// Observer对象只会实例化一次
ob = value.__ob__
} else if (shouldObserve && Object.isExtensible(value) ) {
// isExtensible判断value是一个可扩展的对象,如果是Number、String类型,则Observer结束
// shouldObserve是一个开关,可以控制是否需要添加观察者
// 比如处理props数据时,请查看updateChildComponent
ob = new Observer(value, shallow)
}
return ob
}
使用Object.defineProperty劫持get和set的核心方法defineReactive
function defineReactive(obj, key, val, customSetter, shallow) {
// 利用闭包原理,每个key下面都创建对应的 dep.subs 空间
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
// 保存原始的get和set方法
const getter = property && property.get
const setter = property && property.set
// 根据不同的参数对val进行特殊处理
// ps:obj为Object.create(null)时,getter为空
if ((!getter || setter) && (val === NO_INIITIAL_VALUE || arguments.length === 2)) {
val = obj[key]
}
// 深度遍历子属性,并转化成Observe实例
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
// 劫持get
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
// 取值时,从Dep.target开始,自顶向下收集dep依赖关系
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
// ref类型取存放在value.value中
return value
},
// 劫持set
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
// value和newVal一致,说明无更新,不需要`notify`
if (value===newVal) {
return
}
if (setter) {
setter.call(obj, newVal)
} else if (getter) {
// ps:只读的属性,setter为空
return
}else {
val = newVal
}
// 深度遍历子属性,并转化成Observe实例
childOb = !shallow && observe(newVal)
// 触发对应的依赖进行更新,可以理解为是发布事件
dep.notify()
}
})
return dep
}
let obj = { data:{ person: { name:'xiaomin', age:'30'}}}
observe(obj)
console.log(obj)
以上代码可以复制控制台运行,这样可以更直观的看出效果,obj转换成Observe实例后,结构大概如下:
以上就是vue中Observe实现的的核心代码,一个对象通过observe改造后就具有了响应式的特性,同时附加Dep实例,用于关联Watcher类。但是还不完整,如果对obj的属性赋值会发现报错,因为Dep这个类还没有实现
Dep类源码解读
下面我们接着看Dep类的实现
let uid = 0
// Watcher类是DepTarget类的实现,但是在Dep中只会用到下面的几个属性和方法
// 为了支持ts语法,所以在此事先声明,便于大家理解
class DepTarget {
id
addDep(){}
update(){}
}
// Dep 是用来管理和存放Watcher的类,而Dep又寄生在每一个Observe实例下面
class Dep {
//target是一个静态属性,全局共享,值为空或Watcher实例
static target = null
id
subs
constructor() {
this.id = uid++
// subs存放的是Watcher实例
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
const index = this.subs.indexOf(sub)
if (index > -1) {
return this.subs.splice(index, 1)
}
}
depend(info) {
// 向Watcher传递this
// 在通过addSub、removeSub管理 this.subs
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify(info) {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
// 实际调用的是 Watcher.update(),获取新的值以及更新依赖关系
subs[i].update()
}
}
}
Dep类并不复杂,主要对外提供了几个方法来处理subs中保存的DepTarget实例,从Observe类可以看到depend是在get调用,进行依赖项收集,notify是在set里调用。这样发布订阅模型就具有了初步的雏形,已经有了订阅和发布的能力,但是还少了关键的一步,那就是发布订阅后的事件回调处理。
Watcher类源码解读
接下来看Watcher类是怎么来处理的,这才整个响应机制的核心所在,从中可以看到作者设计的巧妙,重点关注get,addDep,run这几个方法即可,其他几个方法主要是为computed属性和this.$watch设计的。
// Watcher类是对DepTarget类的实现
let uid = 0 n
class Watcher implements DepTarget {
// 构造函数,进行初始化工作
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
// 每个vue组件实例,都会生成一个Watcher实例,并挂载在_watcher上
if ((this.vm = vm)) {
if (isRenderWatcher) {
vm._watcher = this
}
}
// 以下值都可以单独通过options可以配置,此处忽略该逻辑
// user表示听过 this.$watch 添加的Watcher
this.deep = this.user = this.lazy = this.sync = false
this.cb = cb
this.id = ++uid
this.active = true
this.dirty = this.lazy
// 存放上一次取值时,收集到的依赖,当dep.notify触发时
this.deps = []
this.depIds = new Set()
// 执行this.get(),取值操作时,所有新的依赖都会存放在这里
this.newDeps = []
this.newDepIds = new Set()
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 例如expOrFn值为'xxx.xxx'字符串,转换成 this.xxx.xxx 进行取值
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
}
}
// lazy = true 表示初始化时,不进行取值,用于 computed 属性
this.value = this.lazy ? undefined : this.get()
}
get() {
// 当前对象this设置为依赖收集树的起点,自顶向下收集依赖
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} finally {
// 进行深度依赖收集,这是一个递归函数
// 递归对象value,是一个Observe实例对象
if (this.deep) {
traverse(value)
}
// 释放当前 Dep.target
popTarget()
// 更新依赖关系,主要是保证dep.subs中,在下次data更新时,为对应需要更新的Watcher
this.cleanupDeps()
}
return value
}
// Observe对象取值时,会触发对应空间下dep.depend(),进行依赖收集
// 而depend()最终的调用是Dep.target.addDep()
// 该方法的调用结果是在dep.subs中添加了this,即对应的Watcher实例
addDep(dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
// 将新的依赖关系缓存起来
this.newDepIds.add(id)
this.newDeps.push(dep)
// 如果上一次的依赖关系中不存在这个dep,说明在dep.subs需要加上对应的Watcher
// 同时cleanupDeps()会删除dep.subs中不存在newDeps的中的Watcher
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
// 更新时,最终调用的方法
run() {
if (this.active) {
// 通过get方法,收集新的依赖关系,以用于下一次更新使用
const value = this.get()
if (value !== this.value || isObject(value) ||this.deep) {
// 新的值替换就得值
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue)
}
}
}
// 新的依赖关系,覆盖原来的依赖关系,并清理dep.subs中的Watcher
// ps:这里这么写是为了优化性能,避免重复创建空对象
cleanupDeps() {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp: any = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
// 该方法只在计算属性中调用
evaluate() {
this.value = this.get()
this.dirty = false
}
// 该方法也只在计算属性中调用,一般只有计算属性才会依赖多个deps
// 例如:fullName = this.firstName + this.lastName
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
// 用于卸载依赖关系,一般用于this.$watch监听的卸载
// 如果是data或者computed属性默认都是一直存在,一般无需卸载工作
teardown() {
if (this.active) {
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
// 卸载完成之后的回调
if (this.onStop) {
this.onStop()
}
}
}
}
从上述Watcher类的源码可以看出,Watcher类其实就是DepTarget类的实现,并对其进行完善,使其具有完整的事件回调处理能力。
我们接着看下面的代码
let mountComponent = ()=>{
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop,watcherOptions,true)
}
Dep.target = null
const targetStack = []
pushTarget(target?: DepTarget | null) {
targetStack.push(target)
Dep.target = target
}
popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
先看第一段代码,这样有助于大家理解第二段代码,mountComponent就是$mount()调用的方法,执行过程中实例化了一个Watcher对象,这段代码的含义就是,在不使用计算属性的情况下,每一个Vue组件只有一个Watcher实例。这样Watcher就会和dom一样形成一个Watcher树的结构,因为他挂在在vm._watcher中,而vm本身就是树结构。
接着看第二段代码,这也就是作者设计巧妙的地方,他的作用就是在每次调用Watcher.get()收集依赖时,将当前的实例this设置为Dep.target,因为此时的依赖关系只会存在当前的Watcher节点下,而无需关心其他节点,处理完之后,再替换成其他节点的Watcher
示例
下面我们通过将observe Watcher Dep结合起来,模拟下在Vue中的效果,看函数调用的输出结果
class Vue {
constructor(data) {
this.data = data;
}
render() {
console.log("this.age = " + this.data.age);
}
renderName() {
console.log("this.name = " + this.data.name);
}
}
let data = { name: "xiaomin", age: 20 };
observe(data);
let vm = new Vue(data);
let watcher = new Watcher(vm, function updateComponent() {
this.render();
});
// 输出: this.age = 20
vm.data.age = 10;
// 输出: this.age = 10
vm.data.name = "xiaohua";
// 无输出
// 因为name在vm并没用被使用,所以不在$watcher的依赖项中
vm.renderName();
// 输出: this.name = xiaohua
vm.data.name = "xiaoxiao";
// 无输出
// 此时$watcher依然无data.name的依赖,因为需要执行$watcher.get()才能添加依赖
let $watcher = new Watcher(vm, "data.name", function cb(value, oldValue) {
this.render();
this.renderName();
});
data.name = "haha";
data.age = 50;
// 输出: this.age = 10
// this.name = haha
data.age = 40;
// 输出: this.age = 40
// 注意:这是watcher中render()的输出结果
完整的输出结果就是:
this.age = 20
this.age = 10
this.name = xiaohua
this.age = 10
this.name = haha
this.age = 50
this.age = 40
思考:如果
$watcher换成以下代码,data.name替换为data.age,输出结果还一样吗?答案:不一样,具体原因大家可以自行思考
let $watcher = new Watcher(vm, "data.age", function cb(value, oldValue) {
this.render();
this.renderName();
});
替换后,完整的输出结果就是:
this.age = 20
this.age = 10
this.name = xiaohua
this.age = 50
this.age = 50
this.name = haha
this.age = 40
this.age = 40
this.name = haha
结尾
回到之前上篇中提到的疑问:
vue订阅发布模型为什么要使用观察者模式呢?可以用事件模型来实现吗?
假设vue以事件模型实现订阅和发布,那么每次注册事件都需要有定义eventName。这样就会带来一个问题,要怎么去定义eventName呢?有人可能会用说命名空间,比如:namespace.namespace.eventName,那同样的问题,怎么定义namespace呢?因为如果定义的不好的话就可能会存在同名事件,这样会引起错乱。再一个,对于不同的eventName之间要如何通信呢?这些都是棘手的问题。
所以作者设观察者模式,由于dom是一个树形的结构,每一层之间节点来进行隔离。这样就很好的解决了上述namespace的问题,一个节点可以看成是一个天然的namespace,同时Dep.subs可用于聚合不同节点的Watcher,这样就解决了不同的eventName之间通信的问题。
以上是个人拙见,错误的地方望大家指正。
完结
参考链接