1.1 什么是變化偵測
vue.js會自動通過狀態(tài)生成DOM悲关,并將其輸出到頁面顯示,這個過程叫渲染。vue.js的渲染過程是生命式的,我們通過模板來描述狀態(tài)和DOM之間的映射關(guān)系叮雳。
通常,在運(yùn)行時應(yīng)用內(nèi)部的狀態(tài)會不斷發(fā)生變化妇汗,此時需要不停的重新渲染帘不,這時候如何確定狀態(tài)發(fā)生了變化?它分為兩種類型:一種是“推”(push)铛纬,另一種是“拉”(pull)
Angular和React中的變化偵測都屬于“拉”厌均,這就是說當(dāng)狀態(tài)發(fā)生變化時,它不知道那個狀態(tài)變了告唆,只知道狀態(tài)有可能變了,然后會發(fā)送一個信號給框架晶密,框架內(nèi)部收到信號后擒悬,會進(jìn)行暴力對比來找出哪些DOM節(jié)點(diǎn)需要重新渲染。這在Angular中是臟檢查的流程稻艰,在React中使用的是虛擬DOM懂牧,
而Vue.js的變化測試屬于“推”,當(dāng)狀態(tài)發(fā)生變化時候,Vue.js立刻就知道僧凤,而且在一定程度上知道哪些狀態(tài)變了畜侦。因此,它知道的信息更多躯保,也就可以進(jìn)行更細(xì)粒度的更新旋膳。
所謂更細(xì)粒度的更新,就是說:假如有一個狀態(tài)綁定了好多個依賴途事,每個依賴表示一個具體的DOM節(jié)點(diǎn)验懊,那么當(dāng)這個狀態(tài)發(fā)生變化時,像這個狀態(tài)的所有依賴發(fā)送通知尸变,讓他們進(jìn)行DOM更新操作义图,相比較而言,“拉”的粒度時最粗的召烂。
但是它也有一定的代價碱工,因?yàn)榱6仍郊?xì),每個狀態(tài)綁定的依賴也就越多奏夫,依賴追蹤在內(nèi)存上的消耗也就越大怕篷,因此,在Vue.js2.0中桶蛔,它引入了虛擬DOM匙头,將粒度調(diào)整為中等粒度,即一個狀態(tài)所綁定的依賴不再是具體的某個DOM節(jié)點(diǎn)仔雷,而是一個組件蹂析,這樣狀態(tài)變化后,會通知到組件碟婆,組件內(nèi)部在使用虛擬DOM進(jìn)行對比电抚,這樣可以降低依賴的數(shù)量,從而降低內(nèi)存消耗竖共。
1.2 如何追蹤變化
1.Object.defineProperty
2.ES6的Proxy
知道Object.defineProperty可以檢測到對象的變化蝙叛,那么我們可以寫出如下代碼:
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ù)defineDirective用來對Object.defineProperty進(jìn)行封裝,從函數(shù)的名字可以看書公给,其作用是定義一個響應(yīng)式數(shù)據(jù)借帘,也就是在這個函數(shù)中進(jìn)行變化追蹤,封裝后只需要傳遞data淌铐,key肺然,val即可。
封裝好之后每當(dāng)從data的key中讀取數(shù)據(jù)的時候腿准,get被觸發(fā)际起,每當(dāng)往data的key中設(shè)置數(shù)據(jù),set函數(shù)觸發(fā)。
1.3 如何收集依賴街望?
之所以要觀察數(shù)據(jù)校翔,其目的是當(dāng)數(shù)據(jù)的屬性發(fā)生變化是,可以通知那些曾經(jīng)使用了該數(shù)據(jù)的地方灾前。
舉個例子
<template>
<h1>{{name}}</h1>
</template>
該模板中使用了數(shù)據(jù)name,所以當(dāng)它發(fā)生變化時防症,要向使用它的地方發(fā)送通知
注意:在Vue.js 2.0中,模板使用數(shù)據(jù)等于組件使用數(shù)據(jù)豫柬,所以當(dāng)數(shù)據(jù)發(fā)生變化時告希,會通知發(fā)送到組件,然后組件內(nèi)部在通過虛擬DOM重新渲染烧给。
對于上面的問題燕偶,我的回答是,先收集依賴础嫡,即把用到數(shù)據(jù)name的地方收集起來指么,然后等屬性發(fā)生變化時候,把收集好的依賴循環(huán)觸發(fā)一下榴鼎。
總結(jié)起來就是: 在getter中收集依賴伯诬,在setter中觸發(fā)依賴。
1.4 依賴收集在哪里
現(xiàn)在我們已經(jīng)有了很明確的目標(biāo)巫财,就是在getter中收集依賴盗似,那么要把依賴收集到哪里去?
思考一下平项,說先想到的是每個key都有一個數(shù)組赫舒,用來存儲當(dāng)前key的依賴,假設(shè)依賴時一個函數(shù)闽瓢,保存在window.target上接癌,現(xiàn)在就可以吧defindReactive函數(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: function (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ā)收集到的依賴缺猛。
這樣寫有點(diǎn)耦合,我們把依賴收集的代碼封裝成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; i < subs.length ; 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(); // 修改
}
})
}
此時曙搬,代碼收集到Dep中!
1.5 依賴是誰
從上面的代碼中,我們收集的依賴時window.target纵装,那么它到底是什么征讲?我們究竟要收集誰呢?
收集誰橡娄?換句話說诗箍,就是當(dāng)屬性發(fā)生變化后,通知誰挽唉。
我們要通知到用到數(shù)據(jù)的地方滤祖,而是用這個數(shù)據(jù)的地方有很多,而且類型還不一樣瓶籽,即有可能是模板匠童,也有可能是用戶?的一個watch,這是需要抽象出一個能集中處理這些情況的類塑顺,然后我們在依賴收集階段只收集封裝好的類的實(shí)例進(jìn)來汤求,通知也只通知他一個,接著严拒,他在負(fù)責(zé)通知其他地方扬绪。所以,我們要抽象這個動作需要掀起一個名字裤唠,就是Watcher挤牛。
總結(jié):收集誰?Watcher种蘸!
1.6 什么是Watcher
Watcher是一個中介的角色墓赴,數(shù)據(jù)發(fā)生變化時通知它,然后它再通知其他地方劈彪。
關(guān)于Watcher竣蹦,先看一個經(jīng)典的使用方式:
// keypath
vm.$watch('a.b.c', function(newVal, oldVal){
// todo
})
這段代碼表示當(dāng)data.a.b.c屬性發(fā)生變化時,觸發(fā)第二個參數(shù)中的函數(shù)
怎么實(shí)現(xiàn)這個功能沧奴?好像只要把這個watcher實(shí)例添加到data.a.b.c屬性的Dep中就行痘括,然后當(dāng)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中去疮绷,是不是很神奇翰舌?
因?yàn)樵趃et方法中先把window.target設(shè)置成了this,也就是當(dāng)前watcher實(shí)例,然后在讀一下data.a.b.c的值冬骚,這肯定會觸發(fā)getter椅贱。
觸發(fā)了getter懂算,就會觸發(fā)收集依賴的邏輯,上面已經(jīng)介紹了庇麦,會從window.target中讀取一個依賴并添加到Dep中计技。
這就導(dǎo)致,只要現(xiàn)在window.target賦一個this,然后再讀一下值山橄,去觸發(fā)getter,就可以把this主動添加到keypath中的Dep垮媒。
依賴注入到Dep中后,每當(dāng)data.a.b.c的值發(fā)生變化時航棱,就會讓依賴列表中所欲的依賴循環(huán)觸發(fā)update方法睡雇,也就是Watcher中的update方法,而update方法會執(zhí)行參數(shù)中回調(diào)函數(shù)饮醇,將value和oldValue傳到參數(shù)中它抱。
所以,其實(shí)不管是用戶執(zhí)行vm.$watch('a.b.c'驳阎,(value, oldValue) => {}),還是模板中用到的data,都是通過Watcher來通知自己是否需要發(fā)生變化抗愁。
/**
* 解析簡單路徑
* */
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ù)雜呵晚,現(xiàn)將keypath蜘腌,用.分割成數(shù)組,然后循環(huán)數(shù)組一層一層去讀取數(shù)據(jù)饵隙,最后拿到的obj就是keypath中想要的數(shù)據(jù)撮珠。
1.7 遞歸偵測所有key
現(xiàn)在其實(shí)已經(jīng)可以實(shí)現(xiàn)變化偵測的功能了,但是前面介紹的代碼只能偵測數(shù)據(jù)的某一個屬性金矛,我們希望吧數(shù)據(jù)中的所有屬性包括子屬性都偵測到芯急,所以要封裝一個Observer類,這個類的作用是將一個數(shù)據(jù)內(nèi)的所有屬性包括子屬性都轉(zhuǎn)化成getter/setter的形式驶俊,然后去追蹤他們的變化:
/**
* Observer類會附加到每一個被偵測的object上
* 一旦被附加上娶耍,Observer會將object的所有屬性轉(zhuǎn)化為getter/setter的形式
* 來收集屬性的依賴,并且當(dāng)屬性發(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)用getter/setter的形式來偵測變化,最后在defineReactive中新增new Observer(val)來遞歸子屬性药版,這樣我們就可以把data中的所有屬性(包括子屬性)都轉(zhuǎn)化為getter/setter的形式來偵測變化
當(dāng)data中的屬性發(fā)生變化時辑舷,與這個屬性對應(yīng)得依賴就會收到通知,也就是說我們將一個object傳到Observer中槽片,那么這個object就會變成響應(yīng)式的object何缓。
1.8 關(guān)于Object的問題
前面介紹了object類型的變化偵測原理肢础,了解了數(shù)據(jù)變化時通過getter/setter來追蹤變化的,也正是由于這種追蹤方式歌殃,有些語法即便發(fā)生了變化乔妈,Vue.js也追蹤不到,比如像object添加屬性:
var vm = new Vue({
el: '#el',
template: '#demo',
methods: {
action() {
this.obj.name = 'test';
}
},
data: {
obj: {}
}
})
在action方法中氓皱,我們在obj上面新增了name屬性,Vue.js無法偵測到這個變化勃刨,所以不會向依賴發(fā)送通知
var vm = new Vue({
el: '#el',
template: '#demo',
methods: {
action() {
delete this.obj.name
}
},
data: {
obj: {
name: 'test'
}
}
})
在action方法中波材,我們在obj上面刪除了name屬性,Vue.js無法偵測到這個變化身隐,所以不會向依賴發(fā)送通知
Vue.js 通過Object.defineProperty來將對象的key轉(zhuǎn)化成getter/setter的形式追蹤變化廷区,只能追蹤到是否被修改,無法追蹤到新屬性和刪除屬性贾铝,所以vue.js提供了兩個api隙轻,vm.delete。
1.9總結(jié)
1.變化偵測就是偵測數(shù)據(jù)的變化垢揩,當(dāng)數(shù)據(jù)發(fā)生變化時玖绿,要能偵測到并發(fā)出通知。
2.Object可以通過Object.defineProperty將屬性轉(zhuǎn)化為getter/setter的形式來追蹤變化叁巨,讀取數(shù)據(jù)時用getter斑匪,修改數(shù)據(jù)時用setter。
3.我們需要在getter中收集哪些依賴使用了數(shù)據(jù)锋勺,當(dāng)setter被觸發(fā)時蚀瘸,就去通知getter中收集的依賴數(shù)據(jù)發(fā)生變化。
4.收集依賴需要為依賴找一個存儲的地方庶橱,為此我們創(chuàng)建了Dep,它用來收集依賴贮勃,刪除依賴和向依賴發(fā)送通知。
5.所謂的依賴苏章,其實(shí)就是Watcher寂嘉。只有Watcher觸發(fā)的getter才會收集依賴,哪個Watcher觸發(fā)了getter布近,就把哪個Watcher收集到Dep中垫释。當(dāng)數(shù)據(jù)發(fā)生變化時,會循環(huán)依賴列表撑瞧,把所有的Watcher都通知一遍.
Watcher的原理就是先把自己設(shè)置到全局唯一的指定位置棵譬,例如window.target。然后讀取數(shù)據(jù)预伺,因?yàn)樽x取了數(shù)據(jù)订咸,所以會觸發(fā)數(shù)據(jù)的getter,在getter中就會從全局唯一的那個位置讀當(dāng)前正在讀取數(shù)據(jù)的Watcher曼尊,并把這個Watcher收集到Dep中去,通過這樣的方式脏嚷,Watcher可以主動的去訂閱任意一個數(shù)據(jù)的變化骆撇。
6.此外,我們創(chuàng)建了Observer類父叙,它的作用是吧一個object中的數(shù)據(jù)神郊,包括子數(shù)據(jù)都轉(zhuǎn)化成響應(yīng)式的,也就是他會偵測object的所有數(shù)據(jù)的變化趾唱。
文章出自vue.js深入淺出一書