本篇实现watch,watch用到的仍然是watcher类,依赖收集我们已经实现了,只要数据更新的时候我们调一下传进来的回调函数即可,我们此次主要要解决的是 watch的回调要获取到旧值和新值的问题。
写watch之前我们要知道watch是一个对象,里边是以key:walue的形式存在的,以下是常用的几种写法:
- 1.value是函数
- 2.value是字符串,这个字符串会从methods里去找
- 3.value是数组,当数据改变会依次执行数组里的回调函数
- 4.value是对象,主要是有handler和immediate
- 5.函数式调用,this.$watch(()=>key,回调)
export function initState(vm) {
const opts = vm.$options; // 获取所有选项
if(opts.data) {
initData(vm)
}
if(opts.computed) {
initComputed(vm)
}
// 初始化watch
if(opts.watch) {
initWatch(vm)
}
}
function initWatch(vm) {
let watch =vm.$options.watch
for (let 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)
}
}
}
function createWatcher(vm, key, handler) {
let options = {}
if(typeof handler === 'string') {
// 如果回调是字符串 会从methods里找,我们现在还没实现methods
handler = vm[handler]
}
// 兼容一下对象
else if (Object.prototype.toString.call(handler) === '[object Object]') {
options = handler
handler = handler.handler
}
// 无论哪种调用方式最后都是通过$watch
return vm.$watch(key, handler, options)
}
以上不过都是在处理watch的不同写法,最后统一下调用方式,下一步就是挂上$watch,$watch也很简单就是创建一个watcher,不过我们又需要改造watcher类,需要给传个唯一标识和回调函数。
export function initStateMixin(Vue) {
Vue.prototype.$nextTick = nextTick
Vue.prototype.$watch = function(experOrFn, cb, options={}) {
options.user = true
const watcher = new Watcher(this, experOrFn, options, cb)
// 立即执行
if(options.immediate) {
cb.call(this, watcher.value, undefined)
}
}
}
watcher改动思路:为什么要通过watcher实现?是因为watch是个典型的发布订阅,我们watcher类已经实现了基本的功能,watch只要数据变化就会去读取一次新值,在$watch这种调用方式时第一个传的是函数(不是函数要处理成函数),也就是会执行一次这个函数,同时也要执行一下传进来的回调函数,由于我们写computed的时候已经会把getter的值记录到value上(不过是lazy的,所以我们要改下,在watch的时候第一次执行就要记录到value),所以我们把key的函数(不是函数处理成函数)当做第二个参数传进来挂到value上,这样可以通过实例获取到value的旧值,当数据更新,再次执行getter并重新对value赋值拿到新值,并执行下传进来的回调即可
完整watcher代码:
class Watcher {
constructor(vm, expreOrFn, options, cb) {
this.id = id++
this.renderWatcher = options
if(typeof expreOrFn === 'string') {
this.getter = function() {
return vm[expreOrFn]
}
} else {
this.getter = expreOrFn // getter 意味着调用函数发生取值操作
}
this.deps = [] // 后续实现计算属性 和清理工作
this.depsId = new Set()
this.vm = vm
// 计算属性标示
this.lazy = options.lazy
this.dirty = this.lazy // 缓存值
this.cb = cb
this.user = options.user // 标示是否是用户自己的user
// 执行user watcher 直接把值记录下来
this.value = this.lazy? undefined: this.get()
}
get() {
pushTarget(this) // 调用的时候把当前watcher赋值给Dep.target
let value = this.getter.call(this.vm) // 会去vm上取值
popTarget(this) // 渲染完成置空
return value
}
addDep(dep) { //一个组件对应多个属性 ,重复属性不用记录
let id = dep.id
if(!this.depsId.has(id)) {
this.deps.push(dep)
this.depsId.add(id)
dep.addSub(this) // watcher已经记住dep了而且去重 此时让dep也记住watcher
}
}
evaluate() {
this.value = this.get() // 获取用户的返回值 并且标记为脏
this.dirty = false
}
depend() {
let i = this.deps.length
while(i--) {
this.deps[i].depend() //
}
}
update() {
if(this.lazy) {
this.dirty = true
} else {
queueWatcher(this)
}
}
run() {
// 更新的时候通过value获取旧值 重新执行get获取新值 同时执行下回调即可
let oldValue = this.value
let newValue = this.value = this.get()
if(this.user) {
this.cb.call(this.vm ,newValue, oldValue)
}
}
}
大功告成,检验下效果叭