vue 簡(jiǎn)介
漸進(jìn)式框架:就是把框架分層。
最核心的是視圖層渲染,然后往外是組件機(jī)制,在這個(gè)基礎(chǔ)上加入路由機(jī)制,再加入狀態(tài)管理,以及最外層的構(gòu)建工具。
所謂分層:就是說既可以用最核心的視圖層渲染來開發(fā)一些需求,也可以用vue全家桶來開發(fā)大型應(yīng)用??梢愿咦约旱男枨髞磉x擇不同的層級(jí)。
數(shù)據(jù)監(jiān)聽(Object)
有兩種方法可以偵測(cè)到變化:使用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 用來對(duì)Object.defineProperty 進(jìn)行封裝。從函數(shù)的名字可以看出,其作用是定義一個(gè)響應(yīng)式數(shù)據(jù)。也就是在這個(gè)函數(shù)中進(jìn)行變化追蹤,封裝后只需要傳遞data、key 和val 就行了。
封裝好之后,每當(dāng)從data 的key 中讀取數(shù)據(jù)時(shí),get 函數(shù)被觸發(fā);每當(dāng)往data 的key 中設(shè)置數(shù)據(jù)時(shí),set 函數(shù)被觸發(fā)。
如何收集依賴
如果只是把Object.defineProperty 進(jìn)行封裝,那其實(shí)并沒什么實(shí)際用處,真正有用的是收集依賴。
思考一下,我們之所以要觀察數(shù)據(jù),其目的是當(dāng)數(shù)據(jù)的屬性發(fā)生變化時(shí),可以通知那些曾經(jīng)使用了該數(shù)據(jù)的地方。
<template>
<h1>{{ name }}</h1>
</template>
該模板中使用了數(shù)據(jù)name,所以當(dāng)它發(fā)生變化時(shí),要向使用了它的地方發(fā)送通知。
注意:在Vue.js 2.0 中,模板使用數(shù)據(jù)等同于組件使用數(shù)據(jù),所以當(dāng)數(shù)據(jù)發(fā)生變化時(shí),會(huì)將通知發(fā)送到組件,然后組件內(nèi)部再通過虛擬DOM重新渲染。
對(duì)于上面的問題,先收集依賴,即把用到數(shù)據(jù)name 的地方收集起來,然后等屬性發(fā)生變化時(shí),把之前收集好的依賴循環(huán)觸發(fā)一遍就好了。
總結(jié)起來,其實(shí)就一句話,在getter 中收集依賴,在setter 中觸發(fā)依賴。
依賴收集在哪里
思考一下,首先想到的是每個(gè)key 都有一個(gè)數(shù)組,用來存儲(chǔ)當(dāng)前key 的依賴。假設(shè)依賴是一個(gè)函數(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,用來存儲(chǔ)被收集的依賴。
然后在set 被觸發(fā)時(shí),循環(huán)dep 以觸發(fā)收集到的依賴。
但是這樣寫有點(diǎn)耦合,我們把依賴收集的代碼封裝成一個(gè)Dep 類,它專門幫助我們管理依賴。使用這個(gè)類,我們可以收集依賴、刪除依賴或者向依賴發(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,那么它到底是什么?我們究竟要收集誰呢?
收集誰,換句話說,就是當(dāng)屬性發(fā)生變化后,通知誰。
我們要通知用到數(shù)據(jù)的地方,而使用這個(gè)數(shù)據(jù)的地方有很多,而且類型還不一樣,既有可能是模板,也有可能是用戶寫的一個(gè)watch,這時(shí)需要抽象出一個(gè)能集中處理這些情況的類。然后,我們?cè)谝蕾囀占A段只收集這個(gè)封裝好的類的實(shí)例進(jìn)來,通知也只通知它一個(gè)。接著,它再負(fù)責(zé)通知其他地方。所以,我們要抽象的這個(gè)東西需要先起一個(gè)好聽的名字。嗯,就叫它 Watcher 吧。
現(xiàn)在就可以回答上面的問題了,收集誰?Watcher!
什么是Watcher
Watcher 是一個(gè)中介的角色,數(shù)據(jù)發(fā)生變化時(shí)通知它,然后它再通知其他地方。
關(guān)于Watcher,先看一個(gè)經(jīng)典的使用方式:
// keypath
vm.$watch('a.b.c', function (newVal, oldVal) {
// 做點(diǎn)什么
})
這段代碼表示當(dāng)data.a.b.c 屬性發(fā)生變化時(shí),觸發(fā)第二個(gè)參數(shù)中的函數(shù)。
思考一下,怎么實(shí)現(xiàn)這個(gè)功能呢?好像只要把這個(gè)watcher 實(shí)例添加到data.a.b.c 屬性的Dep 中就行了。然后,當(dāng)data.a.b.c 的值發(fā)生變化時(shí),通知Watcher。接著,Watcher 再執(zhí)行參數(shù)中的這個(gè)回調(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)
}
}
這段代碼可以把自己主動(dòng)添加到data.a.b.c 的 Dep 中去,是不是很神奇?
因?yàn)槲以?get 方法中先把 window.target 設(shè)置成了this,也就是當(dāng)前watcher 實(shí)例,然后再讀一下data.a.b.c 的值,這肯定會(huì)觸發(fā)getter。
觸發(fā)了getter,就會(huì)觸發(fā)收集依賴的邏輯。而關(guān)于收集依賴,上面已經(jīng)介紹了,會(huì)從window.target 中讀取一個(gè)依賴并添加到Dep 中。
這就導(dǎo)致,只要先在window.target 賦一個(gè)this,然后再讀一下值,去觸發(fā)getter,就可以把this 主動(dòng)添加到keypath 的Dep 中。有沒有很神奇的感覺???
依賴注入到Dep 中后,每當(dāng)data.a.b.c 的值發(fā)生變化時(shí),就會(huì)讓依賴列表中所有的依賴循環(huán)觸發(fā)update 方法,也就是Watcher 中的update 方法。而update 方法會(huì)執(zhí)行參數(shù)中的回調(diào)函數(shù),將value 和oldValue 傳到參數(shù)中。
所以,其實(shí)不管是用戶執(zhí)行的vm.$watch('a.b.c', (value, oldValue) => {}),還是模板中用到的data,都是通過Watcher 來通知自己是否需要發(fā)生變化。
這里有些小伙伴可能會(huì)好奇上面代碼中的parsePath 是怎么讀取一個(gè)字符串的keypath 的,下面用一段代碼來介紹其實(shí)現(xiàn)原理:
/**
* 解析簡(jiǎ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
}
}
可以看到,這其實(shí)并不復(fù)雜。先將keypath 用 . 分割成數(shù)組,然后循環(huán)數(shù)組一層一層去讀數(shù)據(jù),最后拿到的obj 就是keypath 中想要讀的數(shù)據(jù)。
遞歸偵測(cè)所有key
現(xiàn)在,其實(shí)已經(jīng)可以實(shí)現(xiàn)變化偵測(cè)的功能了,但是前面介紹的代碼只能偵測(cè)數(shù)據(jù)中的某一個(gè)屬性,我們希望把數(shù)據(jù)中的所有屬性(包括子屬性)都偵測(cè)到,所以要封裝一個(gè)Observer 類。這個(gè)類的作用是將一個(gè)數(shù)據(jù)內(nèi)的所有屬性(包括子屬性)都轉(zhuǎn)換成getter/setter 的形式,然后去追蹤它們的變化:
/**
* Observer 類會(huì)附加到每一個(gè)被偵測(cè)的object 上。
* 一旦被附加上,Observer 會(huì)將object 的所有屬性轉(zhuǎn)換為getter/setter 的形式
* 來收集屬性的依賴,并且當(dāng)屬性發(fā)生變化時(shí)會(huì)通知這些依賴
*/
export class Observer {
constructor (value) {
this.value = value
if (!Array.isArray(value)) {
this.walk(value)
}
}
/**
* walk 會(huì)將每一個(gè)屬性都轉(zhuǎn)換成getter/setter 的形式來偵測(cè)變化
* 這個(gè)方法只有在數(shù)據(jù)類型為Object 時(shí)被調(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 類,它用來將一個(gè)正常的object 轉(zhuǎn)換成被偵測(cè)的object。
然后判斷數(shù)據(jù)的類型,只有Object 類型的數(shù)據(jù)才會(huì)調(diào)用walk 將每一個(gè)屬性轉(zhuǎn)換成getter/setter 的形式來偵測(cè)變化。
最后,在defineReactive 中新增new Observer(val)來遞歸子屬性,這樣我們就可以把data 中的所有屬性(包括子屬性)都轉(zhuǎn)換成getter/setter 的形式來偵測(cè)變化。
當(dāng)data 中的屬性發(fā)生變化時(shí),與這個(gè)屬性對(duì)應(yīng)的依賴就會(huì)接收到通知。
也就是說,只要我們將一個(gè)object 傳到Observer 中,那么這個(gè)object 就會(huì)變成響應(yīng)式的object。
關(guān)于Object的問題
有些語法即便數(shù)據(jù)發(fā)生了變化,vue.js也監(jiān)測(cè)不到,比如向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é)
變化偵測(cè)就是偵測(cè)數(shù)據(jù)的變化,當(dāng)數(shù)據(jù)發(fā)生變化時(shí),要能偵測(cè)并發(fā)送出通知。
Object可以通過Object.defineProperty將屬性轉(zhuǎn)換成getter/setter的形式來追蹤變化。讀取數(shù)據(jù)會(huì)觸發(fā)getter,修改數(shù)據(jù)會(huì)觸發(fā)setter。
在getter中手機(jī)有哪些依賴使用了數(shù)據(jù)。當(dāng)setter被觸發(fā)時(shí),通知getter中收集到的依賴數(shù)據(jù)發(fā)生了變化
收集依賴存儲(chǔ)的地方是創(chuàng)建了一個(gè)Dep,它們用來收集依賴、刪除依賴和向依賴發(fā)送消息等。
依賴就是watcher,只有watcher觸發(fā)的getter才會(huì)收集依賴,哪個(gè)watcher觸發(fā)了getter,就把哪個(gè)watcher收集到Dep中。當(dāng)數(shù)據(jù)發(fā)生變化時(shí),會(huì)循環(huán)依賴列表,把所有的watcher都通知一遍。
watcher的原理是先把自己設(shè)置到全局唯一的指定位置(例如window.target),然后讀取數(shù)據(jù)。因?yàn)樽x取了數(shù)據(jù),所以會(huì)觸發(fā)這個(gè)數(shù)據(jù)的getter。接著在getter中就會(huì)從全局唯一的window.target讀取當(dāng)前正在讀取數(shù)據(jù)的watcher,并收集這個(gè)watcher到Dep中。
此外,創(chuàng)建一個(gè)Observe類,作用是把一個(gè)Object中所有數(shù)據(jù)都轉(zhuǎn)換成響應(yīng)式的。
Data、Observe、Dep和Watcher之間的關(guān)系:Data通過Observe轉(zhuǎn)換成getter/setter的形式來追蹤變化。當(dāng)外界通過watcher讀取數(shù)據(jù)時(shí),會(huì)觸發(fā)getter從而將watcher添加到依賴中。當(dāng)數(shù)據(jù)發(fā)生了變化時(shí), 會(huì)觸發(fā)setter,從而向Dep中的依賴(watcher)發(fā)送通知。watcher接收到通知后,會(huì)向外界發(fā)送通知,變化通知到外界后可能觸發(fā)視圖更新,也有可能觸發(fā)用戶的某個(gè)回調(diào)函數(shù)等。