理解Vue中Watch的实现原理和方式之前,你需要深入的理解MVVM的实现原理,如果你还不是很理解,推荐你阅读我之前的几篇文章:
vue.js源码解读系列 - 剖析observer,dep,watch三者关系 如何具体的实现数据双向绑定
也可以关注我的博客查看关于Vue更多的源码解析:https://github.com/wangweianger/myblog
备注:
1、此文大部分代码来自于Vue源码
2、此文MVVM部分代码来自于【彻底搞懂Vue针对数组和双向绑定(MVVM)的处理方式】,若有不懂之处,建议先看上文
3、部分代码为了兼容测试做了部分更改,但原理跟Vue一致
画一张watch的简单工作流程图:
把上文的 Dep,Oberver,Wather拿过来并做部分更改(增加收集依赖去重处理):
Dep代码如下:
//标识当前的Dep id
let uidep = 0
class Dep{
constructor () {
this.id = uidep++
// 存放所有的监听watcher
this.subs = []
}
//添加一个观察者对象
addSub (Watcher) {
this.subs.push(Watcher)
}
//依赖收集
depend () {
//Dep.target 作用只有需要的才会收集依赖
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 调用依赖收集的Watcher更新
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
const targetStack = []
// 为Dep.target 赋值
function pushTarget (Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = Watcher
}
function popTarget () {
Dep.target = targetStack.pop()
}
Watcher代码如下:
//去重 防止重复收集
let uid = 0
class Watcher{
constructor(vm,expOrFn,cb,options){
//传进来的对象 例如Vue
this.vm = vm
if (options) {
this.deep = !!options.deep
this.user = !!options.user
}else{
this.deep = this.user = false
}
//在Vue中cb是更新视图的核心,调用diff并更新视图的过程
this.cb = cb
this.id = ++uid
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
if (typeof expOrFn === 'function') {
//data依赖收集走此处
this.getter = expOrFn
} else {
//watch依赖走此处
this.getter = this.parsePath(expOrFn)
}
//设置Dep.target的值,依赖收集时的watcher对象
this.value =this.get()
}
get(){
//设置Dep.target值,用以依赖收集
pushTarget(this)
const vm = this.vm
//此处会进行依赖收集 会调用data数据的 get
let value = this.getter.call(vm, vm)
//深度监听
if (this.deep) {
traverse(value)
}
popTarget()
return value
}
//添加依赖
addDep (dep) {
//去重
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
//收集watcher 每次data数据 set
//时会遍历收集的watcher依赖进行相应视图更新或执行watch监听函数等操作
dep.addSub(this)
}
}
}
//更新
update () {
this.run()
}
//更新视图
run(){
const value = this.get()
const oldValue = this.value
this.value = value
if (this.user) {
//watch 监听走此处
this.cb.call(this.vm, value, oldValue)
}else{
//data 监听走此处
//这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图
console.log(`这里会去执行Vue的diff相关方法,进而更新数据`)
}
}
// 此方法获得每个watch中key在data中对应的value值
//使用split('.')是为了得到 像'a.b.c' 这样的监听值
parsePath (path){
const bailRE = /[^w.$]/
if (bailRE.test(path)) return
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
//此处为了兼容我的代码做了一点修改
//此处使用新获得的值覆盖传入的值 因此能够处理 'a.b.c'这样的监听方式
if(i==0){
obj = obj.data[segments[i]]
}else{
obj = obj[segments[i]]
}
}
return obj
}
}
}
//深度监听相关代码 为了兼容有一小点改动
const seenObjects = new Set()
function traverse (val) {
seenObjects.clear()