Vue 源码解析一:observer
要理解 Vue,从 observer 开使是一个不错的选择。因为从本质上来讲,除去生命周期函数,虚拟DOM,组件系统等以外,Vue首先建立在数据监测之上,可以收集依赖,并在数据变化时自动通知到Vue的实例。
observer 的相关代码在 core/observer
下,数据绑定的逻辑主要集中在 dep/watcher/index/traverse
4 个文件中。
observer 工作原理简述
当有一个表达式,我们可以收集它的依赖,并在数据变化时,让依赖反过来去通知表达式这种变化。用一个例子来示意整个工作流程:
1、首先假设我们有个 expOrFn 函数,内部返回一个表达式的值:
function expOrFn(vm) { return vm.user }
很显然,expOrFn 依赖 vm.user
,当 user
变化时,expOrFn 应该自动重新执行。但,怎么知道这种依赖关系呢?
2、Vue 引入 getter 来帮助检查依赖,通过运行一次函数来确定依赖。
对数据 vm 来说,假设我们用 getter 来改写它的所有属性;那么当我们访问 vm.user
的时候,getter 函数会执行,所以,只要我们执行一次 expOrFn
,它的所有依赖就都知道了!
const vm = { user: 'creeper' } defineGetter(vm) let value = expOrFn(vm)
一切看起来很简单。然后很自然地,我们加上 setter 来感知数据的更新。
3、Vue 用 setter 来截获数据更新。
当 vm 变化时,我们必须能够感知这种变化,否则收集依赖是完全没有意义的。
const vm = { user: 'creeper' } defineGetterAndSetter(vm) let value = expOrFn(vm) // 然后我们更新数据 vm.user = 'who?' // 因为调用了setter,所以我们可以知道数据更新了!
看起来一切都搞定了。但上面的代码只是伪代码,实际开发中,我们必须要解决怎么定义依赖,怎么收集依赖,怎么通知更新的整个流程。
4、Vue 定义了依赖(Dep
),并设计了巧妙的 getter/setter 来收集依赖和通知更新:
// 依赖,作为纽带来用,本身设计的很薄 class Dep { // 添加订阅者——即谁依赖这个依赖 addSub(sub) { this.subs.push(sub) } // 当有变化时,通知订阅者 notify() { this.subs.forEach(sub => sub.update()) } // 很有意思的方法,下一步重点说,或者直接看源代码的注释 depend() { Dep.target.addDep(this) } }
接下来看看 Dep 是怎么用在 getter/setter 里的:
// 假设有数据 vm,我们对 vm 的每个属性调用 defineReactive 来设置 getter/setter // defineReactive(vm, 'user', 'creeper') function defineReactive(obj, key, val) { // 每个属性创建一个dep,这是一个一一对应的关系 const dep = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { if (Dep.target) { // 收集依赖 dep.depend() } return val }, set: function reactiveSetter(newVal) { val = newVal // 通知数据更新了 dep.notify() } }) }
如上,getter/setter 配合对应的 dep,可以完成依赖收集和更新通知。下面描述整个流程是怎么工作的(比如 dep.depend()
怎么收集依赖的):
5、Vue 用 Watcher
来串联整个流程。
class Watcher { // vm 是数据,expOrFn 是表达式,cb 是更新时的回调 constructor(vm, expOrFn, cb) { this.vm = vm // 用于收集依赖 this.deps = [] // 收集依赖 this.value = this.get() } get() { // 设置Dep.target,方便依赖收集时 dep.depend 可以正确调用 Dep.target = this // 调用 expOrFn 来收集依赖 const val = this.expOrFn.call(this.vm, this.vm) } // 联系上面的 dep.depend,是不是恍然大悟? addDep(dep) { this.deps.push(dep) dep.addSub(this) }, // 联系上面的 dep.notify,是不是懂了? update() { this.cb.call(this, this.get(), this.value) } } const vm = { user: 'creeper' } // 设置 getter/setter observe(vm) const exp = vm => vm.user // 让exp可以监测数据变化 new Watcher(vm, exp, function updateCb() {})
以上即整个 observer 流程,当然里面简化了很多细节,详细的可看代码注释和下面的核心代码解读。
核心代码解读
observe
函数和 Observer
类
observe(value, asRootData)
方法用于为 value 创建 getter/setter,从而实现对数据变化的监听;该方法会为 value 创建对应的 Observer 实例,而 observer 则是实际转化 value 的属性为 getter/setter,收集依赖和转发更新的地方。
observer 相关的核心代码是 defineReactive
来创建 getter/setter,下面是相关注释:
/** * 把 property 转化为 getter 和 setter * - 创建dep(dep是一个纽带,连接watcher和数据,getter时收集依赖,setter时通知更新); * - 在 getter 里面进行依赖收集,在非shallow时递归收集 * - 在 setter 里面进行更新通知,在非shallow时重新创建childOb */ export function defineReactive( obj, key, val, customSetter, shallow ) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } // 大部分情况shallow默认是false的,即默认递归observe。 // - 当val是数组时,childOb被用来向当前watcher收集依赖 // - 当val是普通对象时,set/del函数也会用childOb来通知val的属性添加/删除 let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, // watcher 初始化时调用自己的 watcher.get(),最终调用这个 getter, // 而 dep.depend 执行,把 watcher 放到了自己的 subs 里;所以当 // set 执行时,watcher 被通知更新。 get: function reactiveGetter() { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() // 现在问题是为什么要依赖 childOb 呢? // 考虑到如果 value 是数组,那么 value 的 push/shift 之类的操作, // 是触发不了下面的 setter 的,即 dep.depend 在这种情况不会被调用。 // 此时,childOb 即value这个数组对应的 ob,数组的操作会通知到childOb, // 所以可以替代 dep 来通知 watcher。 if (childOb) { childOb.dep.depend() // 同时,对数组元素的操作,需要通过 dependArray(value) 来建立依赖。 if (Array.isArray(value)) { dependArray(value) } } } return value }, 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() } if (setter) { setter.call(obj, newVal) } else { val = newVal } // 对新val重新创建childOb childOb = !shallow && observe(newVal) // 通知更新 dep.notify() } }) }
入口 Watcher
observer 核心入口是 Watcher
,创建一个 watcher 可以监测数据的变化,并在变化时执行回调。
下面是 traverse 的代码:
// 递归遍历 val,深度收集依赖 function _traverse(val, seen) { let i, keys const isA = Array.isArray(val) // 如果不是数组或对象,或者是 VNode,或者frozen,则不再处理。 if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { return } if (val.__ob__) { const depId = val.__ob__.dep.id // 如果已经收集过,则不再重复处理了。 if (seen.has(depId)) { return } seen.add(depId) } // 对数组的每个元素调用 _traverse if (isA) { i = val.length while (i--) _traverse(val[i], seen) } // 对子属性(val[keys[i]])访问,即调用 defineReactvie 定义的 getter,收集依赖 else { keys = Object.keys(val) i = keys.length while (i--) _traverse(val[keys[i]], seen) } }
测试和代码执行过程简述
下面是一些测试代码,帮助理解 observer 的运行。
const { observe } = require('./dist/core/observer') const Watcher = require('./dist/core/observer/watcher').default function createWather(data, expOrFn, cb, options) { const vm = { data: data || {}, _watchers: [] } observe(vm.data, true) return new Watcher(vm, expOrFn, cb, options) } const raw = { s: 'hi', n: 100, o: {x: 1, arr: [1, 2]}, arr: [8, 9] } const w = createWather(raw, function expOrFn() { // 1 return this.data.o }, (a, b) => { console.log('--->', a, b) }, { deep: false, sync: true }) // 2 raw.o.x = 2
我在设置 getter 的地方加了一些输出语句:
get: function reactiveGetter() { const value = getter ? getter.call(obj) : val; console.log('call getter --->', key, val, !!_dep.default.target) if (_dep.default.target) { dep.depend(); if (childOb) { childOb.dep.depend(); console.log('call childOb.dep.depend') if (Array.isArray(value)) { dependArray(value); } } } return value; },
并且测试代码中,序号1和2下面的一行代码会替换来测试不同的情况,测试结果如下:
1. retrun this.data.o 2. raw.o.x = 101
call getter ---> o { x: [Getter/Setter], arr: [Getter/Setter] } true
call childOb.dep.depend
call getter ---> o { x: [Getter/Setter], arr: [Getter/Setter] } false
1. retrun this.data.o 2. raw.o = 101
call getter ---> o { x: [Getter/Setter], arr: [Getter/Setter] } true
call childOb.dep.depend
call getter ---> o 101 true
---> 101 { x: [Getter/Setter], arr: [Getter/Setter] }
1. retrun this.data.arr 2. raw.arr.push(10)
call getter ---> arr [ 8, 9 ] true
call childOb.dep.depend // 因为child depend,数组的操作可以被监测。
call getter ---> arr [ 8, 9 ] false // push 产生的 getter
call getter ---> arr [ 8, 9, 10 ] true // 调用回调时调用了 this.get()
call childOb.dep.depend
---> [ 8, 9, 10 ] [ 8, 9, 10 ]
可下载本 repo,在 codes/vue 下跑 npm i && npm run build
,然后如果是 vscode,可以直接调试 测试 vue-observer
,了解 observer 的工作原理。
更多
本篇的分析尽量不把 Vue 其它部分牵扯进来,所以遗留了 computed
型 watcher 和 scheduler 没有涉及。下一篇将解析 instance 部分,会把遗留的补上。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论