vue 簡介
漸進式框架:就是把框架分層。
最核心的是視圖層渲染居灯,然后往外是組件機制,在這個基礎(chǔ)上加入路由機制恭垦,再加入狀態(tài)管理圈匆,以及最外層的構(gòu)建工具漠另。
所謂分層:就是說既可以用最核心的視圖層渲染來開發(fā)一些需求,也可以用vue全家桶來開發(fā)大型應(yīng)用臭脓⌒锍可以更具自己的需求來選擇不同的層級。
數(shù)據(jù)監(jiān)聽(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
}
})
}
這里的函數(shù)defineReactive
用來對Object.defineProperty
進行封裝。從函數(shù)的名字可以看出砚作,其作用是定義一個響應(yīng)式數(shù)據(jù)窘奏。也就是在這個函數(shù)中進行變化追蹤,封裝后只需要傳遞data
葫录、key
和val
就行了着裹。
封裝好之后,每當從data
的key
中讀取數(shù)據(jù)時米同,get
函數(shù)被觸發(fā)骇扇;每當往data
的key
中設(shè)置數(shù)據(jù)時,set
函數(shù)被觸發(fā)面粮。
如何收集依賴
如果只是把Object.defineProperty
進行封裝少孝,那其實并沒什么實際用處,真正有用的是收集依賴熬苍。
思考一下稍走,我們之所以要觀察數(shù)據(jù),其目的是當數(shù)據(jù)的屬性發(fā)生變化時柴底,可以通知那些曾經(jīng)使用了該數(shù)據(jù)的地方婿脸。
<template>
<h1>{{ name }}</h1>
</template>
該模板中使用了數(shù)據(jù)name
,所以當它發(fā)生變化時柄驻,要向使用了它的地方發(fā)送通知狐树。
注意:在Vue.js 2.0 中,模板使用數(shù)據(jù)等同于組件使用數(shù)據(jù)鸿脓,所以當數(shù)據(jù)發(fā)生變化時抑钟,會將通知發(fā)送到組件,然后組件內(nèi)部再通過虛擬DOM重新渲染答憔。
對于上面的問題味赃,先收集依賴,即把用到數(shù)據(jù)name 的地方收集起來虐拓,然后等屬性發(fā)生變化時心俗,把之前收集好的依賴循環(huán)觸發(fā)一遍就好了。
總結(jié)起來蓉驹,其實就一句話城榛,在getter
中收集依賴,在setter
中觸發(fā)依賴态兴。
依賴收集在哪里
思考一下狠持,首先想到的是每個key
都有一個數(shù)組,用來存儲當前key
的依賴瞻润。假設(shè)依賴是一個函數(shù)喘垂,保存在window.target
上甜刻,現(xiàn)在就可以把defineReactive
函數(shù)稍微改造一下:
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
}
})
}
這里我們新增了數(shù)組dep
,用來存儲被收集的依賴正勒。
然后在set
被觸發(fā)時得院,循環(huán)dep
以觸發(fā)收集到的依賴。
但是這樣寫有點耦合章贞,我們把依賴收集的代碼封裝成一個Dep
類祥绞,它專門幫助我們管理依賴。使用這個類鸭限,我們可以收集依賴蜕径、刪除依賴或者向依賴發(fā)送通知等。其代碼如下:
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
兜喻,那么它到底是什么?我們究竟要收集誰呢赡麦?
收集誰虹统,換句話說,就是當屬性發(fā)生變化后隧甚,通知誰。
我們要通知用到數(shù)據(jù)的地方渡冻,而使用這個數(shù)據(jù)的地方有很多戚扳,而且類型還不一樣,既有可能是模板族吻,也有可能是用戶寫的一個watch
帽借,這時需要抽象出一個能集中處理這些情況的類。然后超歌,我們在依賴收集階段只收集這個封裝好的類的實例進來砍艾,通知也只通知它一個。接著巍举,它再負責通知其他地方脆荷。所以,我們要抽象的這個東西需要先起一個好聽的名字懊悯。嗯蜓谋,就叫它 Watcher
吧。
現(xiàn)在就可以回答上面的問題了炭分,收集誰桃焕?Watcher
!
什么是Watcher
Watcher
是一個中介的角色捧毛,數(shù)據(jù)發(fā)生變化時通知它观堂,然后它再通知其他地方让网。
關(guān)于Watcher
,先看一個經(jīng)典的使用方式:
// keypath
vm.$watch('a.b.c', function (newVal, oldVal) {
// 做點什么
})
這段代碼表示當data.a.b.c
屬性發(fā)生變化時师痕,觸發(fā)第二個參數(shù)中的函數(shù)溃睹。
思考一下,怎么實現(xiàn)這個功能呢七兜?好像只要把這個watcher 實例添加到data.a.b.c 屬性的Dep 中就行了丸凭。然后,當data.a.b.c 的值發(fā)生變化時腕铸,通知Watcher惜犀。接著,Watcher 再執(zhí)行參數(shù)中的這個回調(diào)函數(shù)狠裹。
export default class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
// 執(zhí)行this.getter()虽界,就可以讀取data.a.b.c 的內(nèi)容
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
設(shè)置成了this
莉御,也就是當前watcher
實例,然后再讀一下data.a.b.c
的值俗冻,這肯定會觸發(fā)getter
礁叔。
觸發(fā)了getter
,就會觸發(fā)收集依賴的邏輯迄薄。而關(guān)于收集依賴琅关,上面已經(jīng)介紹了,會從window.target
中讀取一個依賴并添加到Dep
中讥蔽。
這就導(dǎo)致涣易,只要先在window.target
賦一個this
,然后再讀一下值冶伞,去觸發(fā)getter
新症,就可以把this
主動添加到keypath
的Dep
中。有沒有很神奇的感覺跋烨荨徒爹?
依賴注入到Dep
中后,每當data.a.b.c
的值發(fā)生變化時金抡,就會讓依賴列表中所有的依賴循環(huán)觸發(fā)update
方法瀑焦,也就是Watcher
中的update
方法。而update
方法會執(zhí)行參數(shù)中的回調(diào)函數(shù)梗肝,將value
和oldValue
傳到參數(shù)中榛瓮。
所以,其實不管是用戶執(zhí)行的vm.$watch('a.b.c', (value, oldValue) => {})
巫击,還是模板中用到的data
禀晓,都是通過Watcher
來通知自己是否需要發(fā)生變化精续。
這里有些小伙伴可能會好奇上面代碼中的parsePath 是怎么讀取一個字符串的keypath
的,下面用一段代碼來介紹其實現(xiàn)原理:
/**
* 解析簡單路徑
*/
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
}
}
可以看到粹懒,這其實并不復(fù)雜重付。先將keypath 用 . 分割成數(shù)組,然后循環(huán)數(shù)組一層一層去讀數(shù)據(jù)凫乖,最后拿到的obj 就是keypath 中想要讀的數(shù)據(jù)确垫。
遞歸偵測所有key
現(xiàn)在,其實已經(jīng)可以實現(xiàn)變化偵測的功能了帽芽,但是前面介紹的代碼只能偵測數(shù)據(jù)中的某一個屬性删掀,我們希望把數(shù)據(jù)中的所有屬性(包括子屬性)都偵測到,所以要封裝一個Observer
類导街。這個類的作用是將一個數(shù)據(jù)內(nèi)的所有屬性(包括子屬性)都轉(zhuǎn)換成getter/setter
的形式披泪,然后去追蹤它們的變化:
/**
* Observer 類會附加到每一個被偵測的object 上。
* 一旦被附加上搬瑰,Observer 會將object 的所有屬性轉(zhuǎn)換為getter/setter 的形式
* 來收集屬性的依賴款票,并且當屬性發(fā)生變化時會通知這些依賴
*/
export class Observer {
constructor (value) {
this.value = value
if (!Array.isArray(value)) {
this.walk(value)
}
}
/**
* walk 會將每一個屬性都轉(zhuǎn)換成getter/setter 的形式來偵測變化
* 這個方法只有在數(shù)據(jù)類型為Object 時被調(diào)用
*/
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
轉(zhuǎn)換成被偵測的object
。
然后判斷數(shù)據(jù)的類型翼悴,只有Object
類型的數(shù)據(jù)才會調(diào)用walk
將每一個屬性轉(zhuǎn)換成getter/setter
的形式來偵測變化姆钉。
最后,在defineReactive
中新增new Observer(val)
來遞歸子屬性抄瓦,這樣我們就可以把data
中的所有屬性(包括子屬性)都轉(zhuǎn)換成getter/setter
的形式來偵測變化。
當data
中的屬性發(fā)生變化時陶冷,與這個屬性對應(yīng)的依賴就會接收到通知钙姊。
也就是說,只要我們將一個object
傳到Observer
中埂伦,那么這個object
就會變成響應(yīng)式的object
煞额。
關(guān)于Object的問題
有些語法即便數(shù)據(jù)發(fā)生了變化,vue.js也監(jiān)測不到沾谜,比如向Object添加和刪除屬性膊毁。
es6 proxy方式監(jiān)聽數(shù)據(jù)響應(yīng)的方式
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
總結(jié)
變化偵測就是偵測數(shù)據(jù)的變化,當數(shù)據(jù)發(fā)生變化時基跑,要能偵測并發(fā)送出通知婚温。
Object可以通過Object.defineProperty將屬性轉(zhuǎn)換成getter/setter的形式來追蹤變化。讀取數(shù)據(jù)會觸發(fā)getter媳否,修改數(shù)據(jù)會觸發(fā)setter栅螟。
在getter中手機有哪些依賴使用了數(shù)據(jù)荆秦。當setter被觸發(fā)時,通知getter中收集到的依賴數(shù)據(jù)發(fā)生了變化
收集依賴存儲的地方是創(chuàng)建了一個Dep力图,它們用來收集依賴步绸、刪除依賴和向依賴發(fā)送消息等。
依賴就是watcher吃媒,只有watcher觸發(fā)的getter才會收集依賴瓤介,哪個watcher觸發(fā)了getter,就把哪個watcher收集到Dep中赘那。當數(shù)據(jù)發(fā)生變化時刑桑,會循環(huán)依賴列表,把所有的watcher都通知一遍漓概。
watcher的原理是先把自己設(shè)置到全局唯一的指定位置(例如window.target)漾月,然后讀取數(shù)據(jù)。因為讀取了數(shù)據(jù)胃珍,所以會觸發(fā)這個數(shù)據(jù)的getter梁肿。接著在getter中就會從全局唯一的window.target讀取當前正在讀取數(shù)據(jù)的watcher,并收集這個watcher到Dep中觅彰。
此外吩蔑,創(chuàng)建一個Observe類,作用是把一個Object中所有數(shù)據(jù)都轉(zhuǎn)換成響應(yīng)式的填抬。
Data烛芬、Observe、Dep和Watcher之間的關(guān)系:Data通過Observe轉(zhuǎn)換成getter/setter的形式來追蹤變化飒责。當外界通過watcher讀取數(shù)據(jù)時赘娄,會觸發(fā)getter從而將watcher添加到依賴中。當數(shù)據(jù)發(fā)生了變化時宏蛉, 會觸發(fā)setter遣臼,從而向Dep中的依賴(watcher)發(fā)送通知。watcher接收到通知后拾并,會向外界發(fā)送通知揍堰,變化通知到外界后可能觸發(fā)視圖更新,也有可能觸發(fā)用戶的某個回調(diào)函數(shù)等嗅义。