這一段時間工作上不是很忙,所以讓我有足夠的時間來研究一下VueJs還是比較開心的 (只要不加班怎么都開心),說到VueJs總是讓人想到雙向綁定,MVVM,模塊化,等牛逼酷炫的名詞,而通過近期的學習我也是發現了Vue一個很神奇的方法$watch,第一次嘗試了下,讓我十分好奇這是怎么實現的,
為什么變量賦值也會也會觸發回調?這背后又有什么奇淫巧技?懷着各種問題,我看到了一位大牛,楊川寶的文章,但是我還是比較愚笨,看了三四遍,依然心存疑惑,最終在楊大牛的GitHub又看了許久,終於有了眉目,本篇末尾,我會給上鏈接
在正式介紹$watch方法之前,我有必要先介紹一下實現基本的$watch方法所需要的知識點,並簡單介紹一下方便理解:
1) Object.defineProperty ( obj, key , option) 方法
這是一個非常神奇的方法,同樣也是$watch以及實現雙向綁定的關鍵
總共參數有三個,其中option中包括 set(fn), get(fn), enumerable(boolean), configurable(boolean)
set會在obj的屬性被修改的時候觸發,而get是在屬性被獲取的時候觸發,(其實屬性的每次賦值,每次取值,都是調用了函數)
2) Es6 知識,例如Class,()=>, const, let,這些也都比較基礎,但是如果不知道的話,還是還是推薦了解一下;
3) 面向對象編程,例如Object.keys,constructor,call,以及各種花式this指向,不過這些方法,也不是特別難理解,稍加搜索,OK的。
下面簡單介紹一下$watch方法的使用
其使用方法如下:
1 //----/VUE.JS
2 const v = new Vue({ 3 data:{ 4 a:1, 5 b:{ 6 c:3
7 } 8 } 9 }) 10 // 實例方法$watch,監聽屬性"a"
11 v.$watch("a",()=>console.log("你修改了a")) 12 //當Vue實例上的a變化時$watch的回調
13 setTimeout(()=>{ 14 v.a = 2
15 // 設置定時器,修改a
16 },1000)
怎么樣?是不是很簡單,而且很有用?下面我來簡單的實現一下$watch這個方法;
從實例化Vue對象開始,到調用$watch方法,再到屬性變化,觸發回調,我分為三個階段
首先第一個階段
new Vue(options)
想要實現watch,當實例化Vue對象的時候,有下面三個函數,需要被調用
1 class Vue { //Vue對象
2 constructor (options) { 3 this.$options=options; 4 let data = this._data=this.$options.data; 5 Object.keys(data).forEach(key=>this._proxy(key)); 6 // 拿到data之后,我們循環data里的所有屬性,都傳入代理函數中
7 observe(data,this); 8 } 9 $watch(expOrFn, cb, options){ //監聽賦值方法
10 new Watcher(this, expOrFn, cb); 11 // 傳入的是Vue對象
12 } 13
14 _proxy(key) { //代理賦值方法
15 // 當未開啟監聽的時候,屬性的賦值使用的是代理賦值的方法
16 // 而其主要的作用,是當我們訪問Vue.a的時候,也就是Vue實例的屬性時,我們返回的是Vue.data.a的屬性而不是Vue實例上的屬性
17 var self = this
18 Object.defineProperty(self, key, { 19 configurable: true, 20 enumerable: true, 21 get: function proxyGetter () { 22 return self._data[key] 23 // 返回 Vue實例上data的對應屬性值
24 }, 25 set: function proxySetter (val) { 26 self._data[key] = val 27 } 28 }) 29 } 30 }
以上是一個Class Vue ,這個Vue類身上有三個方法,分別是constructor (實例化默認方法),$watch(也就是我們今天要實現的方法)和一個_proxy(代理)方法
constructor :
當Vue被實例化,並傳入參數(options)的時候,constructor 就會被調用,並且接收Vue實例化的參數options,這個函數在這里做的事情,就是,對傳進來的data進行加工 第一步做的,就是讓Vue對象,和參數data,產生一個關聯,好讓你可以通過,this.a , 或者vm.a 來操作data屬性,建立關聯之后,循環data的所有鍵名,將其傳入到_proxy方法,到這里constructor方法的主要作用就結束了,什么?你說還有observe方法?這個屬於另一個方向,稍后會做出解釋;
$watch
這個方法你一看就會明白,只是實例化Watcher對象,而Watcher對象里還有其他的什么方法,稍后我會介紹;
_proxy
這個方法是一個代理方法,接收一個鍵名,作用的對象是Vue對象,具體的作用嘛,不知道大家有沒有想過,這個:
1 //首先我們實例化Vue對象
2 var vm = Vue({ 3 data:{ 4 a:1, 5 msg:'今天學習watch'
6 } 7 }) 8 console.log(vm.msg) //打印 '今天學習watch'
9 // 理論上來說,msg和a,應該是data上的屬性,但是卻可以通過vm.msg直接拿到
原因就在於,_proxy 這個方法身上,我們可以看到defineProperty方法作用的對象,是self,也就是Vue對象,而get方法里,return出來的卻是self._data[key], _data在上面的方法當中,已經和參數data相等了,所以當我們訪問Vue.a的時候,get方法返回給我們的,是Vue._data.a。
但是,表面上,他只是為了修改取值和賦值的方法,而且就算我用Vue.data.a取值,又能怎么樣?但是實際上,叫它代理方法,不是沒有原因的,當watch事件開啟了監聽屬性的時候,變量的set和get方法當然是有watch來控制,畢竟人家還有回調要搞嘛,但是,當我不需要watch的時候,怎么辦呢?
那當然就是這個代理函數加上的set和get方法來代理沒有watch時候的取值和賦值的方法啦。
下面我來說一下,剛才漏掉的,opserve(data,this)
當我們在new Vue的時候,傳進去的data很可能包括子對象,例如在使用Vue.data.a = {a1:1 , a2:2 }的時候,這種情況是十分常見的,但是剛才的_proxy函數只是循環遍歷了key,如果我們要給對象的子對象增加set和get方法的時候,最好的方法就是遞歸;
方法也很簡單,如果有屬性值 == object,那么久把他的屬性值拿出來,遍歷一次,如果還有,繼續遍歷,代碼如下:
1 class Observer{ //對象Observer
2 constructor(value) {//value 就是Vue實例上的data
3 this.value = value 4 this.dep = new Dep() 5 //Dep對象是聯絡Watcher對象和觸發監聽回調的對象,稍后會有描述
6 this.walk(value) 7 } 8 //遞歸。。讓每個字屬性可以observe
9 walk(value){ 10 Object.keys(value).forEach(key=>this.convert(key,value[key])) 11 } 12 convert(key, val){ //這里的 key value 是Vue實例data的每個鍵值對
13 defineReactive(this.value, key, val)//this.value 就是Vue實例的data
14 } 15 } 16
17
18
19
20 function defineReactive (obj, key, val) {//類似_proxy方法,循環增加set和get方法,只不過增加了Dep對象和遞歸的方法
var dep = new Dep()
21 var childOb = observe(val) 22 //這里的val已經是第一次傳入的對象所包含的屬性或者對象,會在observe進行篩選,決定是否繼續遞歸
23 Object.defineProperty(obj, key, {//這個defineProperty方法,作用對象是每次遞歸傳入的對象,會在Observer對象中進行分化
24 enumerable: true, 25 configurable: true, 26 get: ()=>{ 27 if(Dep.target){//這里判斷是否開啟監聽模式(調用watch)
28 dep.addSub(Dep.target)//調用了,則增加一個Watcher對象
29 } 30 return val//沒有啟用監聽,返回正常應該返回val
31 }, 32 set:newVal=> {var value = val 33 if (newVal === value) {//新值和舊值相同的話,return
34 return
35 } 36 val = newVal 37 childOb = observe(newVal) 38 //這里增加observe方法的原因是,當我們給屬性賦的值也是對象的時候,同樣要遞歸增加set和get方法
39 dep.notify() 40 //這個方法是告訴watch,你該行動了
41 } 42 }) 43 } 44 function observe (value, vm) {//遞歸控制函數
45 if (!value || typeof value !== 'object') {//這里判斷是否為對象,如果不是對象,說明不需要繼續遞歸
46 return
47 } 48 return new Observer(value)//遞歸
49 }
這里寫的比較亂(不會寫注釋啊!!),不過沒關系,你只需要知道,Opserver對象是使用defineReactive方法循環給參數value設置set和get方法,同時順便調了observe方法做了一個遞歸判斷,看看是否要從Opserver對象開始再來一遍。就是這樣,至於dep對象,大可不必關心。
到這里,new Vue所執行的階段就告一段落,仍然留下了一些坑,例如Dep,不過馬上就會展示出來
因為Dep起到連接的作用,所以在new Watcher之前,有必要讓你們看一下:
1 class Dep { 2 constructor() { 3 this.subs = [] //Watcher隊列數組 4 } 5 addSub(sub){ 6 this.subs.push(sub) //增加一個Watcher 7 } 8 notify(){ 9 this.subs.forEach(sub=>sub.update()) //觸發Watcher身上的update回調(也就是你傳進來的回調) 10 } 11 } 12 Dep.target = null //增加一個空的target,用來存放Watcher
new Watcher
Dep對象身上的方法和作用,大體在上面的注釋寫的比較清楚,很多涉及到Watcher對象,那么下面我就來介紹一下Watcher對象,在開始的時候,我們已經知道,Vue對象身上的一個方法,$watch,而這個方法做的事情也不是別的,正是new Watcher對象,那么上代碼:
1 //-----WATCHER
2 class Watcher { // 當使用了$watch 方法之后,不管有沒有監聽,或者觸發監聽,都會執行以下方法 3 constructor(vm, expOrFn, cb) { 4 this.cb = cb //調用$watch時候傳進來的回調 5 this.vm = vm 6 this.expOrFn = expOrFn //這里的expOrFn是你要監聽的屬性或方法也就是$watch方法的第一個參數(為了簡單起見,我們這里不考慮方法,只考慮單個屬性的監聽) 7 this.value = this.get()//調用自己的get方法,並拿到返回值 8 } 9 update(){ // 還記得Dep.notify方法里循環的update么?
10 this.run() 11 } 12 run(){//這個方法並不是實例化Watcher的時候執行的,而是監聽的變量變化的時候才執行的 13 const value = this.get() 14 if(value !==this.value){ 15 this.value = value 16 this.cb.call(this.vm)//觸發你傳進來的回調函數,call的作用,我就不說了
17 } 18 }
19 get(){
20 Dep.target = this //將Dep身上的target 賦值為Watcher對象
21 const value = this.vm._data[this.expOrFn];//這里拿到你要監聽的值,在變化之前的數值 22 // 聲明value,使用this.vm._data進行賦值,並且觸發_data[a]的get事件
23 Dep.target = null
24 return value 25 } 26 }
class Watcher在實例化的時候,重點在於get方法,我們來分析一下,get方法首先把Watcher對象賦值給Dep.target,隨后又有一個賦值,
const value = this.vm._data[this.exOrFn], 這個賦值的過程,是一個關鍵點,要知道,我們之前所做的都是什么,不就是修改了Vue對象的data(_data)的所有屬性的get和set事件么? 而Vue對象也作為第一個參數,傳給了Watcher對象,此時此刻,這個this.vm._data里的所有屬性,在取值的時候,都會觸發之前增加的get方法,此時,我們再來看一下get方法是什么?
1 get: ()=>{ 2 if(Dep.target){ //觸發這個get事件之前,我們剛剛對Dep.target賦值為Watcher對象 3 dep.addSub(Dep.target)//這里會把我們剛賦值的Dep.target(也就是Watcher對象)添加到監聽隊列里 4 } 5 return val 6 } 7 }
在吧Watcher對象放再Dep.subs數組中之后,new Watcher對象所執行的任務就告一段落,此時我們有:
Dep.subs數組中,已經添加了一個Watcher對象,
Dep對象身上有notify方法,來觸發subs隊列中的Watcher的update方法,
Watcher對象身上有update方法可以調用run方法觸發最終我們傳進去的回調,
可是你會覺得,雖然方法都這么齊全,所謂萬事具備只欠東風,那么如何觸發Dep.notify方法,來層層回調,找到Watcher的run呢?
答案就在set方法中的最后一行
1 set:newVal=> {
2 var value = val 3 if (newVal === value) { 4 return
5 } 6 val = newVal 7 childOb = observe(newVal)
8 dep.notify()//觸發Dep.subs中所有Watcher.update方法
9 }
別問我set方法怎么觸發,當然是你修改了你所監聽的那個值的時候啦,
到這里,就是我對簡易版的$watch方法的理解,不過終歸是簡易版,除了回味代碼的思路,更感慨尤大的水平之高,在寫博客的時候,也沒有太好的思路,不知道怎么寫更容易懂(反正也沒人看),可能你看了這篇博客之后,仍然對$watch的實現還有問題,當然歡迎指正和提問,雖然是Vue1的實現方法,但是思路是不會過時的,另外推薦,楊川寶大大的文章和GitHub
文章地址 https://segmentfault.com/a/1190000004384515
GitHub https://github.com/georgebbbb...
以上
2017-04-23 擊鼓賣糖