阅读过程中的相关demo代码:github.com/AlanLee97/r...
简单概括
开发者定义的options.watch
,就是Vue内部的Watcher
类的实例,。
通过Dep
类连接data的属性与Watcher
之间的关系,在初始化Vue/Vue组件时,使用Object.defineProperty
的属性描述符的get/set完成响应式,其中就是使用到Dep
类的实例进行依赖收集,在getter中进行依赖收集dep.depend()
,就是把Watcher
的实例收集起来,在setter中进行通知依赖更新dep.notify()
,notify函数则会遍历dep
中收集的watcher
实例,执行watcher
的run()
方法,实际就是执行开发者定义的options.watch
的回调函数。
那么具体过程是怎么实现的呢?下面来分析源码。
思维导图
watch的全链路图
watch的主要使用方式
先回顾下watch的几个主要的定义方式
- 函数方式
js
data() {
return {
count: 0
}
},
watch: {
count(newVal, oldVal) {
console.log('alan->watch count', newVal, oldVal)
}
},
- 对象方式
js
data() {
return {
countObj: {
value: 0
}
}
},
watch: {
countObj: {
handler(newVal) {
console.log('alan->watch countObj', newVal)
},
immediate: true,
deep: true
}
},
- 字符串方式
js
data() {
return {
countObj: {
value: 0
}
}
},
watch: {
'countObj.value': function (newVal) {
console.log('alan->watch countObj.value', newVal)
}
},
源码分析
开发者定义的watch的每一个属性,Vue会给它new一个Watcher类的实例,所以我们主要分析它的源码。
先简单过一下Watcher类的源码
Watcher类
Watcher类思维导图
完整Watcher类源码:
js
// src\core\observer\watcher.js
let uid = 0
// Watcher解析表达式,收集依赖项,并在表达式值发生改变时触发回调
// 用于$watch() api和指令
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
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
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
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)
}
popTarget()
this.cleanupDeps()
}
return value
}
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = 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
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
其中比较重要属性和方法的有:
-
属性
- cb
- expression
- getter
- value
-
方法
- get()
- update()
- run()
属性/方法浅析
cb
就是开发者写的回调函数,如当前示例中的
js
count(newVal, oldVal) {
console.log('alan->watch count', newVal, oldVal)
},
expression
是watch对象的键
getter
是一个取值函数,如果expOrFn是函数,则直接使用expOrFn,否则通过parsePath得到一个取值函数
js
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
value
是这个watch观察的这个属性的值,初始化Watcher
时通过this.get()
函数取到get()
函数获取当前观察的data的属性的值,通过getter函数取的值,这里getter
取值函数,会触发初始化时Object.defineProperty
观察data时的getter,进入到getter,收集依赖。到这里完成了一个watch的初始化流程,那么另外的状态就是等待用户改变data,触发setter,执行更新流程。
update()
函数,执行更新,其实里面是调用run()
方法,判断是同步调用还是异步调用run()
,这个方法是】里执行用户写的回调函数cb
我们可以把watch的执行过程分为两个流程:
- 初始化watch流程
- 改变数据时,watch的执行流程
先看初始化流程
初始化Watch流程
- 从图中可以看到,首先是执行Vue的初始化过程,进入
Vue._init
- 接着调用
initWatch
,里面调用createWatcher
,实际上就是调用$watch
$watch
函数是new Vue
之前在stateMixin(Vue)
给Vue.prototype
挂载实例方法中赋值的函数
js
// src\core\instance\state.js
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options.user = true
// new Watcher对象
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn () {
watcher.teardown()
}
}
这里可以看到主要逻辑是
- 如果是纯对象,则调用
createWatcher
,直接返回结果,否则执行下面的逻辑 - 做了一个user的标记,用于区分渲染用的Watcher(即如果options.user=true,则表示这个watcher实例是对应开发者写在组件中的watch)
- new了一个Watcher
- 如果有
immediate
,就立即执行回调函数 - 返回销毁函数
unwatchFn
- 重点看一下
new Watcher
,实例化Watcher时的逻辑,构造时,给一系列属性赋值
js
class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
getter: Function;
value: any;
// ...
constructor (
vm: Component,
expOrFn: string | Function, // 定义watch时的键名
cb: Function, // 定义watch时的键名对应的函数
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 根据data的属性的键名,得到一个取值函数
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
}
}
// 取值的这一步挺重要的,会触发data属性的getter
// this.lazy为true是给computed用的
this.value = this.lazy
? undefined
: this.get()
}
// more code ...
}
- 取值的这一步,
this.get()
挺重要的,会触发data属性的getter,dep.depend()
收集依赖 - 看看
get()
的实现
js
/**
* Evaluate the getter, and re-collect dependencies.
* 求值getter,并重新收集依赖
*/
get () {
// 把当前实例的值放到Dep.target中
pushTarget(this)
let value
const vm = this.vm
try {
// 取options.data中的值,触发getter,收集依赖
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)
}
// 把Dep.target清空
popTarget()
this.cleanupDeps()
}
return value
}
这里的逻辑:
- 首先
pushTarget(this)
,把当前实例的值放到Dep.target
中 - 再调用
getter
取值,进行依赖收集dep.depend()
,实际上就是Dep.target.addDep(this)
,其实就是调用watcher.addDep(dep)
- 执行完取值过程,然后调用
popTarget()
把Dep.target
清空 - 返回取到的值
- 上面
getter
取值过程中会触发依赖收集,其实就是调用watcher.addDep(dep)
,看看watcher.addDep
方法实现
js
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
其实也很简单,就是把当前watcher的实例,添加到dep中,这样的Dep与Watcher就产生了关联,到这里初始化Watch的过程也完成了。
简单总结
new Vue
时初始化watch
,创建Watcher
的实例- 创建
Watcher
的实例过程中会,取一下当前watch的data的属性值,触发依赖收集,把watcher的实例添加到Dep
中,这样Dep
与Watcher
就产生了关联 - 完成初始化
如果对这个过程还是很迷糊,可以再看一看初始化过程的流程图:
上面分析的过程中出现了Watcher,那么我们可以对Watcher的源码进行一个预览
改变数据时,watch的执行流程
- 用户改变data的数据,触发
Object.defineProperty
的setter - 触发setter时,会setter里调用
dep.notify()
,然后dep
遍历收集的subs
,其实每一个sub
就是一个watcher
,再调用watcher.update()
,update实际是调用watcher.run()
,然后会判断是否是同步调用,如果是,则立即执行,否则进行异步调用,这里的异步调用就是使用的nextTick
(简单来讲,实际上就是Promise.resolve().then(cb)
),在nextTick里调用watcher.run()
,run()
方法里执行我们写的回调函数cb()
- 执行完
cb()
函数,一个watch的工作过程就结束了
调用链路如图:
简单总结
简单总结执行过程,就是:改变data数据,触发setter,通知watcher执行回调函数。
总结
总结watch的工作过程,主要分为2个流程:
- 初始化watch
new Vue
时初始化watch
,创建Watcher
的实例- 创建
Watcher
的实例过程中,会取一下当前watch的data的属性值,触发依赖收集,把watcher的实例添加到Dep
中,这样Dep
与Watcher
就产生了关联 - 完成初始化,然后等待改变数据时触发watch的回调
- 改变data时,触发watch的回调
- 改变data数据,触发setter,
dep.notify()
通知watcher执行cb()
回调函数
可以看到,Watcher的整个工作流程需要Vue的响应式的配合才能完成,并且Watcher类对于Vue来说是一个核心的代码模块,它的用处很多,例如最重要的渲染更新的过程也是由Watcher配合实现的,Vue本身初始化时会new一个Watcher的实例,用更新函数_update()
作为回调函数,当数据发生改变,触发watcher的回调,执行更新函数更新视图。另外computed
也是由Watcher实现的(后期分析)。
动手实现一个MiniWatcher
在了解了Watcher的原理后,我们可以动手实现一个简单的Watcher
实现
首先实现几个重要的属性和方法
- vm
- cb
- getter
- expression
- user
- value
- get()
- update()
- run()
js
class MiniWatcher {
vm = null; // 当前vue/vue组件实例
cb = () => {}; // 回调函数
getter = () => {}; // 取值函数
expression = ''; // watch的键名
user = false; // 是否是用户定义的watch
value; // 当前观察的属性的值
constructor(vm, expOrFn, cb, options = {}) {
this.vm = vm;
this.cb = cb;
this.expression = expOrFn;
this.getter = parseExpression(this.expression, vm, this);
this.user = options.user;
this.value = this.get();
}
get() {
const value = this.getter();
return value;
}
update() {
nextTick(() => {
this.run();
})
}
run() {
// 获取新值和旧值
const newValue = this.get();
const oldValue = this.value;
this.value = newValue;
this.cb.call(this.vm, newValue, oldValue);
}
}
// 解析表达式,返回一个函数
function parseExpression(key, vm, watcher) {
return () => {
MiniDep.target = watcher;
// 取值,触发getter,取值前先把watcher实例放到target中
const value = vm.data[key];
// 取完值后,清空Dep.target
MiniDep.target = null;
return value;
}
}
function nextTick(cb) {
return Promise.resolve().then(cb);
}
当然,Watcher的实现需要Dep的配合,我们可以实现一个简单的Dep
js
class MiniDep {
static target = null;
subs = [];
depend(sub) {
if(sub && !this.subs.includes(sub)) {
this.subs.push(sub);
}
}
notify() {
this.subs.forEach(sub => {
sub && sub.update();
})
}
}
光有了Watcher和Dep还是不够,需要在Vue中才可以验证我们的实现,所以我们也可以实现一个简单的Vue
js
function MiniVue(options = {}) {
const vm = this;
this.vm = this;
this.data = options.data;
this.watch = options.watch;
this.deps = new Set();
initData(this.data); // 初始化data
initWatch(this.watch); // 初始化watch
function observe(data) {
for (const key in data) {
defineReactive(data, key);
}
}
function defineReactive(data, key) {
const dep = new MiniDep();
vm.deps.add(dep);
const clonedData = JSON.parse(JSON.stringify(data));
Object.defineProperty(data, key, {
get: function reactiveGetter() {
// console.log('alan->', 'get', clonedData[key]);
dep.depend(MiniDep.target);
return clonedData[key];
},
set: function reactiveSetter(value) {
// console.log('alan->', 'set', key, value);
dep.notify();
clonedData[key] = value;
return value;
}
});
}
function initData(data = {}) {
for (const key in data) {
vm[key] = vm.data[key];
observe(vm.data);
}
}
function initWatch(watch = {}) {
for (const key in watch) {
new MiniWatcher(vm, key, watch[key], {user: true}); // user = true,标记这是用户定义的watch
}
}
}
测试效果,new 一个MiniVue
js
const vm = new MiniVue({
data: {
count: 0
},
watch: {
count(newVal, oldVal) {
console.log('alan->watch count', {newVal, oldVal})
}
}
})
const btn = document.getElementById('btnPlus');
const res = document.getElementById('res');
btn.onclick = () => {
vm.data.count = vm.data.count + 1;
const count = vm.data.count
console.log('alan->count', count);
console.log('alan->vm', vm);
res.innerHTML = count;
}
可以看到效果正常。
完整代码
mini-watcher.js
js
class MiniWatcher {
vm = null; // 当前vue/vue组件实例
cb = () => {}; // 回调函数
getter = () => {}; // 取值函数
expression = ''; // watch的键名
user = false; // 是否是用户定义的watch
value; // 当前观察的属性的值
constructor(vm, expOrFn, cb, options = {}) {
this.vm = vm;
this.cb = cb;
this.expression = expOrFn;
this.getter = parseExpression(this.expression, vm, this);
this.user = options.user;
this.value = this.get();
}
get() {
const value = this.getter();
return value;
}
update() {
nextTick(() => {
this.run();
})
}
run() {
// 获取新值和旧值
const newValue = this.get();
const oldValue = this.value;
this.value = newValue;
this.cb.call(this.vm, newValue, oldValue);
}
}
class MiniDep {
static target = null;
subs = [];
depend(sub) {
if(sub && !this.subs.includes(sub)) {
this.subs.push(sub);
}
}
notify() {
this.subs.forEach(sub => {
sub && sub.update();
})
}
}
// 解析表达式,返回一个函数
function parseExpression(key, vm, watcher) {
return () => {
MiniDep.target = watcher;
// 取值,触发getter,取值前先把watcher实例放到target中
const value = vm.data[key];
// 取完值后,清空Dep.target
MiniDep.target = null;
return value;
}
}
function nextTick(cb) {
return Promise.resolve().then(cb);
}
function MiniVue(options = {}) {
const vm = this;
this.vm = this;
this.data = options.data;
this.watch = options.watch;
this.deps = new Set();
initData(this.data); // 初始化data
initWatch(this.watch); // 初始化watch
function observe(data) {
for (const key in data) {
defineReactive(data, key);
}
}
function defineReactive(data, key) {
const dep = new MiniDep();
vm.deps.add(dep);
const clonedData = JSON.parse(JSON.stringify(data));
Object.defineProperty(data, key, {
get: function reactiveGetter() {
// console.log('alan->', 'get', clonedData[key]);
dep.depend(MiniDep.target);
return clonedData[key];
},
set: function reactiveSetter(value) {
// console.log('alan->', 'set', key, value);
dep.notify();
clonedData[key] = value;
return value;
}
});
}
function initData(data = {}) {
for (const key in data) {
vm[key] = vm.data[key];
observe(vm.data);
}
}
function initWatch(watch = {}) {
for (const key in watch) {
new MiniWatcher(vm, key, watch[key], {user: true}); // user = true,标记这是用户定义的watch
}
}
}
const vm = new MiniVue({
data: {
count: 0
},
watch: {
count(newVal, oldVal) {
console.log('alan->watch count', {newVal, oldVal})
}
}
})
const btn = document.getElementById('btnPlus');
const res = document.getElementById('res');
btn.onclick = () => {
vm.data.count = vm.data.count + 1;
const count = vm.data.count
console.log('alan->count', count);
console.log('alan->vm', vm);
res.innerHTML = count;
}
mini-watcher.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mini Watcher</title>
</head>
<body>
<section id="mini-vue-app">
<button id="btnPlus">+1</button>
<h1 id="res"></h1>
</section>
<script src="./mini-watcher.js"></script>
</body>
</html>