理解Vue中Watch的實現原理和方式之前,你需要深入的理解MVVM的實現原理,如果你還不是很理解,推薦你閱讀我之前的幾篇文章:
vue.js原始碼解讀系列 - 雙向繫結具體如何初始化和工作
vue.js原始碼解讀系列 - 剖析observer,dep,watch三者關係 如何具體的實現資料雙向繫結
也可以關注我的部落格檢視關於Vue更多的原始碼解析:github.com/wangweiange…
備註:
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(){
console.log(`這裡會去執行Vue的diff相關方法,進而更新資料`)
const value = this.get()
const oldValue = this.value
this.value = value
if (this.user) {
//watch 監聽走此處
this.cb.call(this.vm, value, oldValue)
}else{
//data 監聽走此處
}
}
// 此方法獲得每個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實現原理及其實現方式