这一次 彻底理解Vue的watch实现原理及其实现方式
理解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()
_traverse(val, seenObjects)
}
function _traverse (val, seen) {
let i, keys
const isA = Array.isArray(val)
if (!isA && Object.prototype.toString.call(val)!= '[object Object]') 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--){
if(i == '__ob__') return;
_traverse(val[i], seen)
}
} else {
keys = Object.keys(val)
i = keys.length
while (i--){
if(keys[i] == '__ob__') return;
_traverse(val[keys[i]], seen)
}
}
}
Observer代码如下:
class Observer{
constructor (value) {
this.value = value
// 增加dep属性(处理数组时可以直接调用)
this.dep = new Dep()
//将Observer实例绑定到data的__ob__属性上面去,后期如果oberve时直接使用,不需要从新Observer,
//处理数组是也可直接获取Observer对象
def(value, '__ob__', this)
if (Array.isArray(value)) {
//这里只测试对象
} else {
//处理对象
this.walk(value)
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
//此处我做了拦截处理,防止死循环,Vue中在oberve函数中进行的处理
if(keys[i]=='__ob__') return;
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
//数据重复Observer
function observe(value){
if(typeof(value) != 'object' ) return;
let ob = new Observer(value)
return ob;
}
// 把对象属性改为getter/setter,并收集依赖
function defineReactive (obj,key,val) {
const dep = new Dep()
//处理children
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
console.log(`调用get获取值,值为${val}`)
const value = val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
}
return value
},
set: function reactiveSetter (newVal) {
console.log(`调用了set,值为${newVal}`)
const value = val
val = newVal
//对新值进行observe
childOb = observe(newVal)
//通知dep调用,循环调用手机的Watcher依赖,进行视图的更新
dep.notify()
}
})
}
//辅助方法
function def (obj, key, val) {
Object.defineProperty(obj, key, {
value: val,
enumerable: true,
writable: true,
configurable: true
})
}
此文的重点来了,watch代码的实现
watch代码大部摘自于Vue源码,我做了部分修改,把Watch改写成一个cass类,代码如下:
class stateWatch{
constructor (vm, watch) {
this.vm = vm
//初始化watch
this.initWatch(vm, watch)
}
initWatch (vm, watch) {
//遍历watch对象
for (const key in watch) {
const handler = watch[key]
//数组则遍历进行createWatcher
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
this.createWatcher(vm, key, handler[i])
}
} else {
this.createWatcher(vm, key, handler)
}
}
}
createWatcher (vm, key, handler) {
let options
if (Object.prototype.toString.call(handler) == '[object Object]' ) {
//处理对象
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
vm.$watch(key, handler, options)
}
}
初始化watch的类已经写好,其中createWatcher有调用到vm.$watch,下面来实现$watch方法
新建一个Vue构造函数:
function Vue(){
}
为Vue新增原型方法$watch代码如下:
Vue.prototype.$watch=function(expOrFn,cb,options){
const vm = this
options = options || {}
//此参数用于给data从新赋值时走watch的监听函数
options.user = true
//watch依赖收集的Watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
//immediate=true时 会调用一次 watcher.run 方法,因此会调用一次watch中相关key的函数
if (options.immediate) {
cb.call(vm, watcher.value)
}
//返回一个取消监听的函数
return function unwatchFn () {
watcher.teardown()
}
}
OK 万事具备,所有的代码已经写完,完整代码如下:
/*----------------------------------------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()
_traverse(val, seenObjects)
}
function _traverse (val, seen) {
let i, keys
const isA = Array.isArray(val)
if (!isA && Object.prototype.toString.call(val)!= '[object Object]') 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--){
if(i == '__ob__') return;
_traverse(val[i], seen)
}
} else {
keys = Object.keys(val)
i = keys.length
while (i--){
if(keys[i] == '__ob__') return;
_traverse(val[keys[i]], seen)
}
}
}
/*----------------------------------------Observer------------------------------------*/
class Observer{
constructor (value) {
this.value = value
// 增加dep属性(处理数组时可以直接调用)
this.dep = new Dep()
//将Observer实例绑定到data的__ob__属性上面去,后期如果oberve时直接使用,不需要从新Observer,
//处理数组是也可直接获取Observer对象
def(value, '__ob__', this)
if (Array.isArray(value)) {
//这里只测试对象
} else {
//处理对象
this.walk(value)
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
//此处我做了拦截处理,防止死循环,Vue中在oberve函数中进行的处理
if(keys[i]=='__ob__') return;
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
//数据重复Observer
function observe(value){
if(typeof(value) != 'object' ) return;
let ob = new Observer(value)
return ob;
}
// 把对象属性改为getter/setter,并收集依赖
function defineReactive (obj,key,val) {
const dep = new Dep()
//处理children
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
console.log(`调用get获取值,值为${val}`)
const value = val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
}
return value
},
set: function reactiveSetter (newVal) {
console.log(`调用了set,值为${newVal}`)
const value = val
val = newVal
//对新值进行observe
childOb = observe(newVal)
//通知dep调用,循环调用手机的Watcher依赖,进行视图的更新
dep.notify()
}
})
}
//辅助方法
function def (obj, key, val) {
Object.defineProperty(obj, key, {
value: val,
enumerable: true,
writable: true,
configurable: true
})
}
/*----------------------------------------初始化watch------------------------------------*/
class stateWatch{
constructor (vm, watch) {
this.vm = vm
//初始化watch
this.initWatch(vm, watch)
}
initWatch (vm, watch) {
//遍历watch对象
for (const key in watch) {
const handler = watch[key]
//数组则遍历进行createWatcher
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
this.createWatcher(vm, key, handler[i])
}
} else {
this.createWatcher(vm, key, handler)
}
}
}
createWatcher (vm, key, handler) {
let options
if (Object.prototype.toString.call(handler) == '[object Object]' ) {
//处理对象
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
vm.$watch(key, handler, options)
}
}
/*----------------------------------------Vue------------------------------------*/
function Vue(){
}
Vue.prototype.$watch=function(expOrFn,cb,options){
const vm = this
options = options || {}
//此参数用于给data从新赋值时走watch的监听函数
options.user = true
//watch依赖收集的Watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
//immediate=true时 会调用一次 watcher.run 方法,因此会调用一次watch中相关key的函数
if (options.immediate) {
cb.call(vm, watcher.value)
}
//返回一个取消监听的函数
return function unwatchFn () {
watcher.teardown()
}
}
代码测试:
再回头看看上面那张简单的Vue watch流程图,测试代码我们严格按照流程图顺序进行
为了方便观看此处复制一份流程图:
1、新建vue对象,并定义data和watch值:
let vue = new Vue()
定义一个data值并挂载到vue中,并给vue新增一个doSomething的方法:
let data={
name:'zane',
blog:'https://blog.seosiwei.com/',
age:20,
fn:'',
some:{
f:'xiaowang'
}
}
vue.data = data
vue.doSomething=()=>{
console.log(`i will do something`)
}
定义一个watch值
let watch={
name: function (val, oldVal) {
console.log('----------name--------')
console.log('new: %s, old: %s', val, oldVal)
},
blog:function (val, oldVal) {
console.log('----------blog---------')
console.log('new: %s, old: %s', val, oldVal)
},
age:'doSomething',
fn:[
function handle1 (val, oldVal) { console.log('111111') },
function handle2 (val, oldVal) { console.log('222222') }
],
some:{
handler: function (val, oldVal) {
console.log('----------some---------')
console.log('new: %s, old: %s', val, oldVal)
},
immediate: true
},
'some.f': function (val, oldVal) {
console.log(`----some.f-----`)
console.log('new: %s, old: %s', val, oldVal)
},
}
2、始化Wathcer
let updateComponent = (vm)=>{
// 收集依赖
data.age
data.blog
data.name
data.some
data.some.f
data.fn
}
new Watcher(vue,updateComponent)
3、初始化Data数据并收集依赖
observe(data)
//此处会调用上面的函数updateComponent,从而调用 get 收集依赖
4、初始化watch
其中会新创建watcher对象即(Dep.target=watcher),调用watch对象key对应的data数据的set,从而收集依赖
new stateWatch(vue, watch)
5、触发set更新
所有依赖都已经收集好是时候触发了
//首先会立即调用一次watch中的some的函数
//会触发vue下的doSomething方法
data.age=25
//会触发watch中监听的blog的函数
data.blog='http://www.seosiwei.com/'
//会触发watch中监听的name的函数
data.name='xiaozhang'
//会触发watch中some.f监听的函数
data.some.f='deep f'
//会触发watch中fn监听的两个函数
data.fn='go fn'
完整测试代码如下:
let data={
name:'zane',
blog:'https://blog.seosiwei.com/',
age:20,
fn:'',
some:{
f:'xiaowang'
}
}
let watch={
name: function (val, oldVal) {
console.log('----------name--------')
console.log('new: %s, old: %s', val, oldVal)
},
blog:function (val, oldVal) {
console.log('----------blog---------')
console.log('new: %s, old: %s', val, oldVal)
},
age:'doSomething',
fn:[
function handle1 (val, oldVal) { console.log('111111') },
function handle2 (val, oldVal) { console.log('222222') }
],
some:{
handler: function (val, oldVal) {
console.log('----------some---------')
console.log('new: %s, old: %s', val, oldVal)
},
immediate: true
},
'some.f': function (val, oldVal) {
console.log(`----some.f-----`)
console.log('new: %s, old: %s', val, oldVal)
},
}
let vue = new Vue()
vue.data = data
vue.doSomething=()=>{
console.log(`i will do something`)
}
let updateComponent = (vm)=>{
// 收集依赖
data.age
data.blog
data.name
data.some
data.some.f
data.fn
}
new Watcher(vue,updateComponent)
observe(data)
new stateWatch(vue, watch)
watch实现完毕。
下一篇准备写:彻底搞懂Vue中computed(计算属性)的实现方式和原理
上一篇: Java 集合框架 Map