主體
實例方法歸類: 先看個作者推薦, 清晰易懂的 23232
重點:
最簡單的訂閱者模式
// Observer class Observer { constructor (data) { this.walk(data) } walk (data) { // 遍歷 let keys = Object.keys(data) for(let i = 0; i < keys.length; i++){ defineReactive(data, keys[i], data[keys[i]]) } } } function defineReactive (data, key, val) { observer(val)
// dep 為什么要在這里實例化, 就是為了實現, 對象每一層的 每一個key都有自己的一個訂閱實例, 比如 a.b 對應 dep1, a.c 對應dep2, 這里雖然都是let dep = new Dep()
// 但每次來到這個方法, dep都是獨立的, 會一直保留在內存. 這樣在每次調用set方法都能找到這個a.b對應的dep
// dep 這里會一直保存, 是因為閉包的關系, Object這個全局的函數, 引用了上層的作用域, 這個作用域包含了 dep, 除非Object = null, 或者退出瀏覽器, dep才會消失
//實例化之后, dep就有了被訂閱, 和發布消息的功能, dep不寫在這里也是可以的, 多定義一個全局函數, 每次obser的時候增加一個dep let dep = new Dep() Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () {
//每次new Watch('a.b'), 都會先執行get方法, 進而來到這里, 觸發 dep.depend(), 這個dep就是 a.b 對應的 訂閱, dep.depend() return val }, set: function (newVal) { if(val === newVal){ return } observer(newVal) dep.notify() } }) } function observer (data) { if(Object.prototype.toString.call(data) !== '[object Object]') { return } new Observer(data) } class Dep { constructor () { this.subs = [] } depend () {
//這里是收集回調函數的過程, 也就是收集依賴項, 數據改變后, 需要觸發的改變UI和其他函數 this.subs.push(Dep.target) } notify () { for(let i = 0; i < this.subs.length; i++){ this.subs[i].fn() } } } Dep.target = null function pushTarget(watch){ Dep.target = watch } // watch class Watch { constructor (exp, fn) { this.exp = exp this.fn = fn pushTarget(this) data[exp] } } var data = { a: 1, b: { c: 2 } }
//遞歸對象的屬性 , 層層監聽 observer(data)
//new 產生 this, this掛載上exp 和 回調fn, 再利用data[exp] 觸發get方法 從而訂閱 dep.push( this) new Watch('a', () => { console.log(9) }) new Watch('a', () => { console.log(90) }) new Watch('b.c', () => { console.log(80) }) setTimeout(() => { data.a = 2 }, 1000)
observer: 檢測每一個對象每一層的屬性, 每個屬性都具備get set的方法, 如果這些屬性有變化, 調用相對的dep處理
Dep: 根據不同的數據生成不同的dep依賴, 這個依賴收集了相關回調方法, 和觸發這些回調執行
wathcer: 包含需要觸發的回調函數, 在get方法中, 訂閱這個屬性的dep, 如果 wacher('a.b', fn1) wacher('a.b', fn2),
就對同一個類型生成了 兩個訂閱者 subs1, subs2 ,當 a.b = 3; 賦值
的時候, 觸發set方法, 從而依次執行了: subs1的回調方法fn1, 和 subs2的回調方法 fn2,
主要還是利用了 Object.defineProperty 方法, set get 會找到同一個dep00, 第一次訪問數據的時候, 觸發get , dep00收集依賴, 后面設置新值的時候, 觸發set方法, dep00觸發所有訂閱者的回調函數。
整個vm 核心, 就是實現了 observer(觀察數據變化) parser(解析依賴) watcher(觀察到的數據更新,通知指令的執行更新相應頁面方法) 這三個東西
具體實現
數據監聽機制
如何監聽某一個對象屬性的變化呢?我們很容易想到 Object.defineProperty 這個 API,
為此屬性設計一個特殊的 getter/setter,然后在 setter 里觸發 ob.notify一個函數 來執行指令,就可以達到監聽的效果。
看數組的方法的監聽例子
[ 'push', 'pop', 'shift', 'unshift', splice', 'sort', 'reverse'].forEach(function( method){
var original = arrayProto[method] // Array.prototype.sort
//數組的方法執行的時候, 會觸發下面這個函數
_.define( arrayMethods, method, function mutator(){
//先在原生的數組原型方法中按傳入的參數執行一遍, 得到結果
var result = original.apply(this , args);
var ob = this.__ob__;
var inserted
switch (method){
case 'push': inserted = args ;break
case 'unshift': inserted = args ; break
case 'splice': inserted = args.slice(2);
}
if(inserted) ob.observeArray(inserted)
ob.notify()
return result
})
})
同時 Vue.js 提供了兩個額外的“糖方法” arr.$set[0] = "c" 和 $remove(index) 來彌補這方面限制帶來的不便。
path 解析器
var path = 'a.b[1].v'
var obj = {
a: { b:[ {v:1}, {v:2}, {v:3} ] }
}
parse( obj, path ) // => 2
如何解析 這個字符串 成為 js語句 是關鍵
vue.js 是通過狀態機管理 來實現對路徑的解析的
Vue 將表達式的訪問路徑字符串 解析成 更易於js使用的狀態
比如 b.c.d 將會解析成 ['b', 'c', 'd' ]
經過js處理后 變成 a[ arr[0] ][ arr[1] ][ arr[2] ] 就可以訪問的這個深層的屬性值
對一個合法的路徑來說, 是有規律的, 如果第一個字符為a
第二個字符可能是有四種情況
a.
a[
ab
a(沒有了, undefinde 就是解析完畢了)
Vue 的狀態機模式解析 path 實際上是將 path 的每個索引的字符視為一個狀態,
將 接下來一個字符 視為 當前狀態的 輸入,
並 根據輸入 進行 狀態轉移 以及 響應操作,
如果輸入不是期望的,那么狀態機將異常中止。
只有狀態機正常運行直到轉移到結束狀態,才算解析成功。
Vue 的 pathStateMachine 有八種狀態,例如 BEFORE_PATH
BEFORE_PATH 是 pathStateMachine 的初始狀態,它的狀態模型為
pathStateMachine[BEFORE_PATH] = {
'ws': [BEFORE_PATH],
'ident': [IN_IDENT, APPEND],
'[': [IN_SUB_PATH],
'eof': [AFTER_PATH]
}
從狀態模型中知道 BEFORE_PATH 接受四種輸入,
這些狀態起的作用是分割字符, 再將這些字符依次放入數組中
ws ,狀態轉移到 BEFORE_PATH
indent ,狀態轉移到 IN_IDENT,並執行 APPEND 操作
[ ,狀態轉移到 IN_SUB_PATH
eof ,AFTER_PATH
indent 表示 a-z A-Z字符, 繼續輸入, 這個子路徑名稱還沒有結束
ws 表示 空格 換行等
[ 表示要開記錄下一個子路徑
eof 表示路徑結束
具體的意思和其他7種狀態模型可以看這個函數 getPathCharType
( https://github.com/vuejs/vue/blob/e9872271fa9b2a8bec1c42e65a2bb5c4df808eb2/src/parsers/path.js#L33-L81 )
狀態機運行過程中,Vue 在通過 action 處理每一級 path 的路徑值。
比如當處於狀態 IN_IDENT 時,再次輸入字符,會執行 APPEND 操作,將該字符串與之前的字符作 字符串拼接。
再次輸入 .或 [ 會執行 PUSH 操作,將之間的字符串視為訪問對象的一個屬性。
Vue 的 pathStateMachine 有四種 action,他們主要是根據 path 特征和狀態提取出對象的訪問屬性,並按照層級關系依次推入數組。
詳細見代碼。
下面是是一個詳細例子分析狀態機的狀態轉移過程,需要分析的 path 為 md[0].da["ky"]
先聲明
keys = 存放對象訪問屬性的數組
key = 臨時變量
index = 索引
mode = 當前狀態
input = 輸入
transfer = 狀態轉移
action = 操作
現在進入狀態極 (簡單使用字符串 for循環)
index = 0 (md[0].da["ky"])
mode = BEFORE_PATH input = 'm' transfer => IN_IDENT action => APPEND keys = [] key = 'm'
index = 1 (md[0].da["ky"])
mode = IN_IDENT input = 'd' transfer => IN_IDENT action => APPEND keys = [] key = 'md'
index = 2 (md[0].da["ky"])
mode = IN_IDENT input = '[' transfer => IN_SUB_PATH action => PUSH keys = ['md'] key = undefined
index = 3 md[0].da["ky"]
mode = IN_SUB_PATH input = '0' transfer => IN_SUB_PATH action => APPEND keys = ['md'] key = '0'
index = 4 (md[0].da["ky"])
mode = IN_SUB_PATH input = ']' transfer => IN_PATH action => INC_SUB_PATH_DEPTH keys = ['md', '0'] key = undefined
index = 5 md[0].da["ky"]
mode = IN_PATH input = '.' transfer => BEFORE_IDENT action => None keys = ['md', '0'] key = undefined
index = 6
mode = BEFORE_IDENT input = 'd' transfer => IN_IDENT action => APPEND keys = ['md', '0'] key = 'd'
index = 7
mode = IN_IDENT input = 'a' transfer => IN_IDENT action => APPEND keys = ['md', '0'] key = 'da'
index = 8 md[0].da["ky"]
mode = IN_IDENT input = '[' transfer => IN_SUB_PATH action => PUSH keys = ['md', '0', 'da'] key = undefined
index = 9
mode = IN_SUB_PATH input = '"' transfer => IN_DOUBLE_QUOTE action => APPEND keys = ['md', '0', 'da'] key = '"'
index = 10 md[0].da["ky"]
mode = IN_DOUBLE_QUOTE input = 'k' transfer => IN_DOUBLE_QUOTE action => APPEND keys = ['md', '0', 'da'] key = '"k'
index = 11
mode = IN_DOUBLE_QUOTE input = 'y' transfer => IN_DOUBLE_QUOTE action => APPEND keys = ['md', '0', 'da'] key = '"ky'
index = 12
mode = IN_DOUBLE_QUOTE input = '"' transfer => IN_SUB_PATH action => APPEND keys = ['md', '0', 'da'] key = '"ky"'
index = 13
mode = IN_SUB_PATH input = ']' transfer => IN_PATH action => PUSH_SUB_PATH keys = ['md', '0', 'da', 'ky'] key = undefined
index = 14
mode = IN_SUB_PATH input = 'eof' transfer => AFTER_PATH action => None keys = ['md', '0', 'da', 'ky'] key = undefined
最后的結果是
['md', '0', 'da', 'ky']
再拼接成 md[0]['da']['ky'] 就可以訪問這個屬性值了 arr[0][ arr[1]] [ arr[2] ]..
比如這個
var a = { b:[ {t1:1},{t2:2} ]};
a['b'][0]['t1'] => 1
表達式解析
vue 的表達式是通過自己的來解析的, 做了setter監聽處理
而不是直接調用eval方法, 所以並不是所以的js表達式都支持
其解析 {{ mess.splite('').reverse().join('') }} 過程:
首先, 調用Vue.parsers.text.parseText(str), 解析成一個tokens對象
tokens = [
{
html: false,
hasOneTime: false,
tag: true,
value: "mess.split('').reverse().join('')" //通過正則獲取到了大括號的內容
}
]
然后用 Vue.parsers.text.tokensToExp(tokens) 取出 value, 賦值為一個字符串表達式:
expression = "mess.split('').reverse().join('')";
這個 expression 正是創建 watcher 時所用的表達式,
wathcer 為表達式 和 數據 建立聯系的時候, 會解析這個表達式 並獲取值
var res = Vue.parsers.expresssion.parseExpresstion(expression)
解析這個表達式, 其實是為它定義了 getter 和 setter 方法
為了能定義並使用這兩個方法, 表達式必須是合法的路徑, 而且要有值
表達式的getter 方法結合組件的數據獲取表達式的值,
通過 Function 構造器為表達式求值
var getter = function( s, expression ){
return new Function( 'return ' + s+'.'+ expression + ';' );
}
獲取表達式的值時, 執行 getter方法 從作用域對象內取值
var model = { mess: 'abc' }
這里利用 new Function 可以傳入字符串,解析成js語句, 來解析字符串表達式;
比如頁面有 {{mess.substring(0,1)}}
var fn = getter( 'model', 'mess.substring(0,1)' );
fn(); // 輸出 a
Vue中的雙向綁定原理:
在視圖中改變組件數據,驅動數據更新。
進而觸發表達式的中熟悉的setter方法, 在根據訂閱者更新表達式的內容, 和頁面的內容
大概的實現參考 $watch方法, 所有的表達式 , 比如 mess.children.split('')
