前言
面试的时候,总是逃不开一个问题,那就是Vue中的watch是如何实现的?
在我学习Vue源码到第14节的时候,总算要面对这个问题了。我此刻也知道了,这个问题还要从响应式原理和依赖收集说起
。
我在vue源码学习11:响应式原理和依赖收集一文中,深入的学习了vue的响应式原理和依赖收集,在这篇文章中获悉:
每一个组件都有一个watcher,每一个watcher内部都有一个deps,保存着这个watcher观测的所有数据。
当用户写一个watch的时候,也创建了一个watcher,并且在vue的内部,同时给这个watcher添加了user
变量,标注这个watcher是用户自己创建的,和页面渲染创建的watcher做出了区别。
从watch用法说起
在vue中,watch通常有这几种用法:
let vm = new Vue({
el: '#app',
data() {
return { name: '张三', score: { en: 18 } }
},
watch: {
// 用法1:
name(newVal, oldVal) {
console.log('newVal', newVal)
console.log('oldVal', oldVal)
},
// 用法2:
name: [
function (newVal, oldVal) {
console.log('newVal', newVal)
console.log('oldVal', oldVal)
},
function (newVal, oldVal) {
console.log('newVal', newVal)
console.log('oldVal', oldVal)
}
],
// 用法3:
'score.en'(newVal, oldVal) {
console.log('newVal', newVal)
console.log('oldVal', oldVal)
}
}
});
setTimeout(() => {
vm.name = '李四';
}, 1000);
这里只介绍这几种基础用法的实现核心。
分别是:
- 普通的用法
- 数组里面多个函数的用法
- 'xxxx.yy'的对象的属性的监听方法的实现
数据劫持时对watch做了什么?
在Vue中,数据劫持的时候,对Vue进行数据初始化,如果发现劫持的数据中,有一个属性名称是watch
,将会进行initWatch处理
/**
*
* @param {Vue的实例} vm
* initState 说明:
* 对Vue的数据进行初始化
* Vue的数据来源有:data,computed,watch,props,inject...
*/
export function initState(vm) {
// ...省略其他代码
if (opts.watch) {
// 对数据进行处理
initWatch(vm, opts.watch)
}
}
initWatch
initWatch接受两个参数,分别是vm
和watch
。
watch是一个对象,此时需要对这个对象进行遍历,取出每一个watch对应的操作handler
。
即:let handler = watch[key]
在前文的用法中,有一种watch的用法,可能是一个数组,所以要对watch进行判断,如果是数组和不是数组分别进行处理,代码如下
function initWatch(vm, watch) {
for (let key in watch) {
let 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) {
// new Watcher()
return vm.$watch(key, handler)
}
createWatcher
接受三个参数,分别是vm、key、handler
- vm: vue的实例
- key: 每一个watch的属性名称
- handler: 每一个watch对应的操作
通过vm.$watch(key, handler)
这个方法就很容易看出,在vue的原型上,挂载了一个$watch的方法。
所以在Vue中会有这样一种写法:
vm.$watch('name', function(newVal, oldVal) { // todo some thing })
在Vue实例上挂载watch方法
Vue在初始化的时候,会在原型上挂载一个$watch
方法。这个方法就是平时用的watch
方法。在这个方法中,它标识了是用户创建的watcher,并且实例化了一个watcher。
这个watcher和组件的wathcer并没有什么太多的区别,区别在于这是用户自己创建watch的时候生成的。
export function stateMixin(Vue) {
Vue.prototype.$watch = function (key, handler, options = {}) {
// 用户自己写的watcher和渲染watcher区分
options.user = true
new Watcher(this, key, handler, options)
}
}
回顾watcher
在之前的文章中,watcher类初始化接受4个参数:
class Watcher {
constructor(vm, exprOrFn, cb, options) {}
}
他们分别是:
- vm: vue实例
- exprOrFn: 页面重新渲染的方法 cb和option在上一节中并没有用到。
到了这里,如果是用户传入的watcher就会有所不同了。
- 首先需要对
exprOrFn
参数进行判断,如果是函数,则继续原来的渲染页面的操作,如果是字符串,则需要重新处理 - 获取第一次渲染的value进行保存,作为watch中的
oldValue
- 每次数据发生变化的时候,将会获取最新的值,把新的值赋值给value,作为下一次执行的时候的旧值
- 执行cb回调
具体代码如下:
// 一个组件对应一个watcher
let id = 0;
import { popTarget, pushTarget } from './dep';
import { queueWatcher } from './scheduler';
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm
this.exprOrFn = exprOrFn
this.user = !!options.user // 标识是不是用户写的watcher
this.cb = cb
this.options = options
this.id = id++ // 给watcher添加标识
// 默认应该执行exprOrFn
// exprOrFn 做了渲染和更新
// 方法被调用的时候,会取值
if (typeof exprOrFn == 'string') {
// 这里需要将表达式转换成函数
this.getter = function () {
// 当数据取值的时候,会进行依赖收集
// 每次取值的时候,用户自己写的watcher就会被收集
// 这里的取值可以类比页面渲染的取值{{}}
let path = exprOrFn.split('.')
let obj = vm
for (let i = 0; i < path.length; i++) {
obj = obj[path[i]]
}
return obj // 走getter方法
}
} else {
this.getter = exprOrFn
}
this.deps = []
this.depsId = new Set()
// 默认初始化执行get
// 第一次渲染的时候的value
this.value = this.get()
console.log('value', this.value, this)
}
get() {
// 每次获取的时候,会把当前的watcher存放到dep队列中
pushTarget(this)
// 这里拿到的值是每一次新的值
const value = this.getter()
popTarget() // 这里去除Dep.target,是防止用户在js中取值产生依赖收集
return value
}
update() {
queueWatcher(this)
}
run() {
let newValue = this.get()
let oldValue = this.value
this.value = newValue // 为了保证下一次更新的时候,这一个新值是下一个的老值
if (this.user) {
console.log('this.cb', this.cb)
this.cb.call(this.vm, newValue, oldValue)
}
}
addDep(dep) {
let id = dep.id
if (!this.depsId.has(id)) {
this.depsId.add(id)
this.deps.push(dep)
dep.addSub(this)
}
}
}
export default Watcher
总结
最后,自己做一点小小的总结:
- 用户写watch的时候,会创建一个watcher
- 给这个watcher会添加一个用户创建的标识,用来调用传入的回调
- watcher会不停的用新值提换老的值,这样在watch回调的时候,可以获取到一个新的值,一个旧的值
- 每当这个数据被修改的时候,会触发setter,去执行
dep.notify()
,通知dep中的watcher全部执行update
方法,update方法会经过一个nextTick的队列,去执行run
方法,在run
方法中,发现这个watcher是用户自己定义的,则执行watch的回调,获取新值和旧值,执行用户定义的事件。
这里的逻辑有点绕,感觉自己的文章表述不是很清晰,望各位看官见谅。