vue 簡介
漸進式框架:就是把框架分層。
最核心的是視圖層渲染,然后往外是組件機制,在這個基礎上加入路由機制,再加入狀態管理,以及最外層的構建工具。
所謂分層:就是說既可以用最核心的視圖層渲染來開發一些需求,也可以用vue全家桶來開發大型應用。可以更具自己的需求來選擇不同的層級。
數據監聽(Object)
有兩種方法可以偵測到變化:使用Object.defineProperty
和 ES6
的Proxy
function defineReactive(data, key ,val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
return val
},
set: function(newVal) {
if(val === newVal) {
return;
}
val = newVal
}
})
}
這里的函數defineReactive
用來對Object.defineProperty
進行封裝。從函數的名字可以看出,其作用是定義一個響應式數據。也就是在這個函數中進行變化追蹤,封裝后只需要傳遞data
、key
和val
就行了。
封裝好之后,每當從data
的key
中讀取數據時,get
函數被觸發;每當往data
的key
中設置數據時,set
函數被觸發。
如何收集依賴
如果只是把Object.defineProperty
進行封裝,那其實并沒什么實際用處,真正有用的是收集依賴。
思考一下,我們之所以要觀察數據,其目的是當數據的屬性發生變化時,可以通知那些曾經使用了該數據的地方。
<template>
<h1>{{ name }}</h1>
</template>
該模板中使用了數據name
,所以當它發生變化時,要向使用了它的地方發送通知。
注意:在Vue.js 2.0 中,模板使用數據等同于組件使用數據,所以當數據發生變化時,會將通知發送到組件,然后組件內部再通過虛擬DOM重新渲染。
對于上面的問題,先收集依賴,即把用到數據name 的地方收集起來,然后等屬性發生變化時,把之前收集好的依賴循環觸發一遍就好了。
總結起來,其實就一句話,在getter
中收集依賴,在setter
中觸發依賴。
依賴收集在哪里
思考一下,首先想到的是每個key
都有一個數組,用來存儲當前key
的依賴。假設依賴是一個函數,保存在window.target
上,現在就可以把defineReactive
函數稍微改造一下:
function defineReactive(data, key, val) {
let dep = [];
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.push(window.target) // 新增
return val
},
set(newVal) {
if(val === newVal) {
return;
}
// 新增
for (let i = 0; i < dep.length; i++) {
dep[i](newVal, val)
}
val = newVal
}
})
}
這里我們新增了數組dep
,用來存儲被收集的依賴。
然后在set
被觸發時,循環dep
以觸發收集到的依賴。
但是這樣寫有點耦合,我們把依賴收集的代碼封裝成一個Dep
類,它專門幫助我們管理依賴。使用這個類,我們可以收集依賴、刪除依賴或者向依賴發送通知等。其代碼如下:
export default class Dep {
constructor() {
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.subs, sub)
}
depend () {
if (window.target) {
this.addSub(window.target)
}
}
notify() {
const subs = this.subs.slice();
for(let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
function remove (arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
之后再改造下defineReactive
:
function defineReactive (data, key, val) {
let dep = new Dep() // 修改
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend() // 修改
return val
},
set: function (newVal) {
if(val === newVal){
return
}
val = newVal
dep.notify() // 新增
}
})
}
依賴是誰
在上面的代碼中,我們收集的依賴是window.target
,那么它到底是什么?我們究竟要收集誰呢?
收集誰,換句話說,就是當屬性發生變化后,通知誰。
我們要通知用到數據的地方,而使用這個數據的地方有很多,而且類型還不一樣,既有可能是模板,也有可能是用戶寫的一個watch
,這時需要抽象出一個能集中處理這些情況的類。然后,我們在依賴收集階段只收集這個封裝好的類的實例進來,通知也只通知它一個。接著,它再負責通知其他地方。所以,我們要抽象的這個東西需要先起一個好聽的名字。嗯,就叫它 Watcher
吧。
現在就可以回答上面的問題了,收集誰?Watcher
!
什么是Watcher
Watcher
是一個中介的角色,數據發生變化時通知它,然后它再通知其他地方。
關于Watcher
,先看一個經典的使用方式:
// keypath
vm.$watch('a.b.c', function (newVal, oldVal) {
// 做點什么
})
這段代碼表示當data.a.b.c
屬性發生變化時,觸發第二個參數中的函數。
思考一下,怎么實現這個功能呢?好像只要把這個watcher 實例添加到data.a.b.c 屬性的Dep 中就行了。然后,當data.a.b.c 的值發生變化時,通知Watcher。接著,Watcher 再執行參數中的這個回調函數。
export default class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
// 執行this.getter(),就可以讀取data.a.b.c 的內容
this.getter = parsePath(expOrFn)
this.cb = cb
this.value = this.get()
}
get() {
window.target = this
let value = this.getter.call(this.vm, this.vm)
window.target = undefined
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
這段代碼可以把自己主動添加到data.a.b.c
的 Dep
中去,是不是很神奇?
因為我在 get
方法中先把 window.target
設置成了this
,也就是當前watcher
實例,然后再讀一下data.a.b.c
的值,這肯定會觸發getter
。
觸發了getter
,就會觸發收集依賴的邏輯。而關于收集依賴,上面已經介紹了,會從window.target
中讀取一個依賴并添加到Dep
中。
這就導致,只要先在window.target
賦一個this
,然后再讀一下值,去觸發getter
,就可以把this
主動添加到keypath
的Dep
中。有沒有很神奇的感覺啊?
依賴注入到Dep
中后,每當data.a.b.c
的值發生變化時,就會讓依賴列表中所有的依賴循環觸發update
方法,也就是Watcher
中的update
方法。而update
方法會執行參數中的回調函數,將value
和oldValue
傳到參數中。
所以,其實不管是用戶執行的vm.$watch('a.b.c', (value, oldValue) => {})
,還是模板中用到的data
,都是通過Watcher
來通知自己是否需要發生變化。
這里有些小伙伴可能會好奇上面代碼中的parsePath 是怎么讀取一個字符串的keypath
的,下面用一段代碼來介紹其實現原理:
/**
* 解析簡單路徑
*/
const bailRE = /[^w.$]/
export function parsePath (path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
可以看到,這其實并不復雜。先將keypath 用 . 分割成數組,然后循環數組一層一層去讀數據,最后拿到的obj 就是keypath 中想要讀的數據。
遞歸偵測所有key
現在,其實已經可以實現變化偵測的功能了,但是前面介紹的代碼只能偵測數據中的某一個屬性,我們希望把數據中的所有屬性(包括子屬性)都偵測到,所以要封裝一個Observer
類。這個類的作用是將一個數據內的所有屬性(包括子屬性)都轉換成getter/setter
的形式,然后去追蹤它們的變化:
/**
* Observer 類會附加到每一個被偵測的object 上。
* 一旦被附加上,Observer 會將object 的所有屬性轉換為getter/setter 的形式
* 來收集屬性的依賴,并且當屬性發生變化時會通知這些依賴
*/
export class Observer {
constructor (value) {
this.value = value
if (!Array.isArray(value)) {
this.walk(value)
}
}
/**
* walk 會將每一個屬性都轉換成getter/setter 的形式來偵測變化
* 這個方法只有在數據類型為Object 時被調用
*/
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
function defineReactive (data, key, val) {
// 新增,遞歸子屬性
if (typeof val === 'object') {
new Observer(val)
}
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend()
return val
},
set: function (newVal) {
if(val === newVal){
return
}
val = newVal
dep.notify()
}
})
}
在上面的代碼中,我們定義了Observer
類,它用來將一個正常的object
轉換成被偵測的object
。
然后判斷數據的類型,只有Object
類型的數據才會調用walk
將每一個屬性轉換成getter/setter
的形式來偵測變化。
最后,在defineReactive
中新增new Observer(val)
來遞歸子屬性,這樣我們就可以把data
中的所有屬性(包括子屬性)都轉換成getter/setter
的形式來偵測變化。
當data
中的屬性發生變化時,與這個屬性對應的依賴就會接收到通知。
也就是說,只要我們將一個object
傳到Observer
中,那么這個object
就會變成響應式的object
。
關于Object的問題
有些語法即便數據發生了變化,vue.js也監測不到,比如向Object添加和刪除屬性。
es6 proxy方式監聽數據響應的方式
let obj = {
a: 1,
b: 2,
c: 3
}
let reactive = new Proxy(obj, {
get: function(target, key, receiver) {
console.log(`getting ${key}`);
return Reflect.get(target, key, receiver)
}
set: function(target, key, receiver) {
console.log(`setting ${key}`);
return Reflect.set(target, key, receiver)
}
})
reactive.a // getting a // 1
reactive.a = 4 // setting a
reactive.a // getting a // 4
總結
變化偵測就是偵測數據的變化,當數據發生變化時,要能偵測并發送出通知。
Object可以通過Object.defineProperty將屬性轉換成getter/setter的形式來追蹤變化。讀取數據會觸發getter,修改數據會觸發setter。
在getter中手機有哪些依賴使用了數據。當setter被觸發時,通知getter中收集到的依賴數據發生了變化
收集依賴存儲的地方是創建了一個Dep,它們用來收集依賴、刪除依賴和向依賴發送消息等。
依賴就是watcher,只有watcher觸發的getter才會收集依賴,哪個watcher觸發了getter,就把哪個watcher收集到Dep中。當數據發生變化時,會循環依賴列表,把所有的watcher都通知一遍。
watcher的原理是先把自己設置到全局唯一的指定位置(例如window.target),然后讀取數據。因為讀取了數據,所以會觸發這個數據的getter。接著在getter中就會從全局唯一的window.target讀取當前正在讀取數據的watcher,并收集這個watcher到Dep中。
此外,創建一個Observe類,作用是把一個Object中所有數據都轉換成響應式的。
Data、Observe、Dep和Watcher之間的關系:Data通過Observe轉換成getter/setter的形式來追蹤變化。當外界通過watcher讀取數據時,會觸發getter從而將watcher添加到依賴中。當數據發生了變化時, 會觸發setter,從而向Dep中的依賴(watcher)發送通知。watcher接收到通知后,會向外界發送通知,變化通知到外界后可能觸發視圖更新,也有可能觸發用戶的某個回調函數等。