Watcher
上回说到,有一个Watcher会一直去相应数据的notify。
今天说说Vue的第二个Watcher也就是$watch
-
这个watcher的回调函数不是updateComponent,不会去执行render等相关函数,只会执行自身定义的函数
-
那问题来了,如果是我们设计。我们可以怎么设计这个函数能让在数据变化的时候执行他呢
-
我们想到的就是,如果我也把他丢到数据的Dep的subs中,那么数据相应是Dep中的sub.notify不就可以一起执行updateComponent和$watch
- 现在看看代码源码实现
// initWatch
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
// createWatcher
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
//$watch
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 = options || {}
options.user = true
// 定义$watcher 的 Watcher 并且user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
// 是否立即执行
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
- 这时候我们看看Watcher (注意user=true) 并且参数的 expOrFn是一个字符串,这时候我们回想,定义数据的Wacher的第二参数expOrFn是什么,是
new Watcher(vm, updateComponent, noop,fn,fn)
也就是一个函数
export default class Watcher {
// 省略定义
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// 这里删除了不必要的东西
this.vm = vm
this.user = !!options.user
this.id = ++uid // uid for batching
// parse expression for getter
// $watcher 的第二个参数并不是一个函数 走下面
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.lazy
? undefined
: this.get()
}
// 老规矩 改掉Target指向
get () {
pushTarget(this)
let value
const vm = this.vm
try {
// 执行expOrFn方法 这个方法其实就是调用数据的get然后让他们Dep绑定这个Watcher
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
}
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
// 返回一个函数,这个函数里返回的obj其实相当于this.xxxx,
// 那么之后怎么说,就是调用数据的get(),绑定Watcher!
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
- 我就省略get的过程啦,有兴趣可以打个断点进去试试
- 这个时候就绑定进去$watcher
- 最后看看notify后怎么执行
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
//老的值
const oldValue = this.value
// 新的值
this.value = value
// 当user为true的时候就执行cb
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
- 最后就响应该方法了
- 如果是deep = true 呢 , Vue的处理是 其实跟parsePath 差不多,只不过就是所有子孙元素都加上$watch而已
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
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)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
// 通过回调来不断出发子孙元素的get()方法
while (i--) _traverse(val[keys[i]], seen)
}
}
- 下面就是我自己简单实现$watch(有点小问题😆 😆)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id='text'>o.b</div>
<script>
// Watcher
var _this = this
class Watcher {
constructor(expOrFn, cb, user, deep) {
this.newDepIds = new Set()
this.newDeps = []
this.depIds = new Set()
this.getter = expOrFn;
this.value = this.get()
this.cb = cb
this.user = user
this.deep = deep
}
addDep(dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
// watcher保存和它有关的dep
this.newDepIds.add(id)
this.newDeps.push(dep)
// 反过来
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
get() {
var val = ''
setTarget(this)
val = this.getter.call()
if(this.deep){
_traverse(val)
}
removeTarget(this)
return val
}
update() {
this.run()
}
run() {
if (this.user) {
const oldValue = this.value
this.value = this.get()
this.cb(this.value, oldValue)
} else {
this.get()
}
}
}
function $watch(func, data) {
// 调用一下这个方法,已添加这个方法的Watcher
new Watcher(data, func, true, true)
}
//Dep
var uid = 0
class Dep {
constructor() {
this.id = uid++;
this.subs = []
}
addSub(Watcher) {
this.subs.push(Watcher)
}
depend() {
Dep.target.addDep(this)
}
notify() {
for (let i = 0, l = this.subs.length; i < l; i++) {
this.subs[i].update()
}
}
}
function setTarget(Watcher) {
Dep.target = Watcher
}
function removeTarget(Watcher) {
Dep.target = null
}
// Observer
var data = {
o: { b: '666' },
c: '333'
}
function _traverse(val) {
let i, keys
const isA = Array.isArray(val)
if (!(isA || val instanceof Object)) {
return
}
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]])
}
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep()
if (value instanceof Object) {
this.walk(value);
}
}
observe(value) {
// 初始化时创建一个
let ob;
if (value instanceof Object) {
ob = new Observer(value)
}
return ob
}
defineReactive(obj, key, value) {
const dep = new Dep();
if (arguments.length == 2) {
value = obj[key]
// console.log(value)
}
this.observe(obj[key])
// 递归遍历
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
if (Dep.target) {
dep.depend()
}
// console.log(obj, key, value)
return value
},
set: function reactiveSetter(newVal) {
value = newVal
dep.notify()
}
})
}
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
this.defineReactive(obj, keys[i])
}
}
}
//模拟Vue执行的过程 暂时用setTimeout函数
setTimeout(() => {
// initData
new Observer(data)
// initWatch
this.$watch((val, oldValue) => {
console.log('发生变化', 'newValue:', val, 'oldValue:', oldValue)
}, () => {
//这里是需要相应的值
// 这个其实相当于parsePath, parsePath 相当于解析字符串然后调用每个数据的get,这个方法直接去调用get,简化了过程
// 源码在lang.js parsePath()方法
// 这是有个bug,如果没有另提出一份空间,会导致对象新旧值一直一样,可能和Vue的定义的data作用域不一样导致,如果懂得请跟我解释下
return JSON.parse(JSON.stringify(this.data.o))
})
new Watcher(() => {
// .. render 渲染
console.log('...', '发生改变继续调用')
document.getElementById('text').innerText = this.data.o.b
})
})
setTimeout(() => {
this.data.o.b =888
this.data.c = 666
}, 5000)
</script>
</body>
</html>