1. 程式人生 > 實用技巧 >vue響應式系統--observe、watcher、dep

vue響應式系統--observe、watcher、dep

vue的響應式系統

vue最獨特的特性之一,是其非侵入性的響應式系統。資料模型僅僅是普通的JavaScript物件,而當你修改它們時,檢視會進行更新,這使得狀態管理非常簡單直接,我們可以只關注資料本身,而不用手動處理資料到檢視的渲染,避免了繁瑣的 DOM 操作,提高了開發效率。

vue 的響應式系統依賴於三個重要的類:Dep 類、Watcher 類、Observer 類,然後使用釋出訂閱模式的思想將他們揉合在一起(不瞭解釋出訂閱模式的可以看我之前的文章釋出訂閱模式與觀察者模式)。

Observer

Observe扮演的角色是釋出者,他的主要作用是呼叫definereactive函式,在definereactive函式中使用Object.defineProperty 方法對物件的每一個子屬性進行資料劫持/監聽。

部分程式碼展示

defineReactive函式,Observe的核心,劫持資料,在setter中向Dep(排程中心)新增觀察者,在getter中通知觀察者更新。

function defineReactive(obj, key, val, customSetter, shallow){
    //監聽屬性key
    //關鍵點:在閉包中宣告一個Dep例項,用於儲存watcher例項
    var dep = new Dep();

    var getter = property && property.get;
    var setter = property && property.set;
    
    if(!getter && arguments.length === 2) {
        val = obj[key];
    }
    //執行observe,監聽屬性key所代表的值val的子屬性
    var childOb = observe(val);
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            //獲取值
            var value = getter ? getter.call(obj) : val;
            //依賴收集:如果當前有活動的Dep.target(觀察者--watcher例項)
            if(Dep.target) {
                //將dep放進當前觀察者的deps中,同時,將該觀察者放入dep中,等待變更通知
                dep.depend();
                if(childOb) {
                    //為子屬性進行依賴收集
                    //其實就是將同一個watcher觀察者例項放進了兩個dep中
                    //一個是正在本身閉包中的dep,另一個是子屬性的dep
                    childOb.dep.depend();
                }
            }
            return value
        },
        set: function reactiveSetter(newVal) {
            //獲取value
            var value = getter ? getter.call(obj) : val;
            if(newVal === value || (newVal !== newVal && value !== value)) {
                return
            }
            if(setter) {
                setter.call(obj, newVal);
            } else {
                val = newVal;
            }
            //新的值需要重新進行observe,保證資料響應式
            childOb = observe(newVal);
            //關鍵點:遍歷dep.subs,通知所有的觀察者
            dep.notify();
        }
    });
}

Dep

Dep 扮演的角色是排程中心/訂閱器,主要的作用就是收集觀察者Watcher和通知觀察者目標更新。每個屬性擁有自己的訊息訂閱器dep,用於存放所有訂閱了該屬性的觀察者物件,當資料發生改變時,會遍歷觀察者列表(dep.subs),通知所有的watch,讓訂閱者執行自己的update邏輯。

部分程式碼展示

Dep的設計比較簡單,就是收集依賴,通知觀察者

//Dep建構函式
var Dep = function Dep() {
    this.id = uid++;
    this.subs = [];
};
//向dep的觀察者列表subs新增觀察者
Dep.prototype.addSub = function addSub(sub) {
    this.subs.push(sub);
};
//從dep的觀察者列表subs移除觀察者
Dep.prototype.removeSub = function removeSub(sub) {
    remove(this.subs, sub);
};
Dep.prototype.depend = function depend() {
    //依賴收集:如果當前有觀察者,將該dep放進當前觀察者的deps中
    //同時,將當前觀察者放入觀察者列表subs中
    if(Dep.target) {
        Dep.target.addDep(this);
    }
};
Dep.prototype.notify = function notify() {
    // 迴圈處理,執行每個觀察者的update介面
    var subs = this.subs.slice();
    for(var i = 0, l = subs.length; i < l; i++) {
        subs[i].update();
    }
};

//Dep.target是觀察者,這是全域性唯一的,因為在任何時候只有一個觀察者被處理。
Dep.target = null;
//待處理的觀察者佇列
var targetStack = [];

function pushTarget(_target) {
    //如果當前有正在處理的觀察者,將他壓入待處理佇列
    if(Dep.target) {
        targetStack.push(Dep.target);
    }
    //將Dep.target指向需要處理的觀察者
    Dep.target = _target;
}

function popTarget() {
    //將Dep.target指向棧頂的觀察者,並將他移除佇列
    Dep.target = targetStack.pop();
}

Watcher

Watcher扮演的角色是訂閱者/觀察者,他的主要作用是為觀察屬性提供回撥函式以及收集依賴(如計算屬性computed,vue會把該屬性所依賴資料的dep新增到自身的deps中),當被觀察的值發生變化時,會接收到來自dep的通知,從而觸發回撥函式。,

部分程式碼展示

Watcher類的實現比較複雜,因為他的例項分為渲染 watcher(render-watcher)、計算屬性 watcher(computed-watcher)、偵聽器 watcher(normal-watcher)三種,
這三個例項分別是在三個函式中構建的:mountComponent 、initComputed和Vue.prototype.$watch。

normal-watcher:我們在元件鉤子函式watch 中定義的,都屬於這種型別,即只要監聽的屬性改變了,都會觸發定義好的回撥函式,這類watch的expression是我們寫的回撥函式的字串形式。

computed-watcher:我們在元件鉤子函式computed中定義的,都屬於這種型別,每一個 computed 屬性,最後都會生成一個對應的 watcher 物件,但是這類 watcher 有個特點:當計算屬性依賴於其他資料時,屬性並不會立即重新計算,只有之後其他地方需要讀取屬性的時候,它才會真正計算,即具備 lazy(懶計算)特性。這類watch的expression是計算屬性中的屬性名。

render-watcher:每一個元件都會有一個 render-watcher, 當 data/computed 中的屬性改變的時候,會呼叫該 render-watcher 來更新元件的檢視。這類watch的expression是function () {vm._update(vm._render(), hydrating);}。

除了功能上的區別,這三種 watcher 也有固定的執行順序,分別是:computed-render -> normal-watcher -> render-watcher。

這樣安排是有原因的,這樣就能儘可能的保證,在更新元件檢視的時候,computed 屬性已經是最新值了,如果 render-watcher 排在 computed-render 前面,就會導致頁面更新的時候 computed 值為舊資料。

這裡我們只看其中一部分程式碼

function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm;
    if(isRenderWatcher) {
        vm._watcher = this;
    }
    vm._watchers.push(this);
    // options
    if(options) {
        this.deep = !!options.deep; //是否啟用深度監聽
        this.user = !!options.user; //主要用於錯誤處理,偵聽器 watcher的 user為true,其他基本為false
        this.lazy = !!options.lazy; //惰性求職,當屬於計算屬性watcher時為true
        this.sync = !!options.sync; //標記為同步計算,三大型別暫無
    } else {
        this.deep = this.user = this.lazy = this.sync = false;
    }
    //初始化各種屬性和option
    
    //觀察者的回撥
    //除了偵聽器 watcher外,其他大多為空函式
    this.cb = cb;
    this.id = ++uid$1; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    // 解析expOrFn,賦值給this.getter
    // 當是渲染watcher時,expOrFn是updateComponent,即重新渲染執行render(_update)
    // 當是計算watcher時,expOrFn是計算屬性的計算方法
    // 當是偵聽器watcher時,expOrFn是watch屬性的名字,this.cb就是watch的handler屬性
    
    //對於渲染watcher和計算watcher來說,expOrFn的值是一個函式,可以直接設定getter
    //對於偵聽器watcher來說,expOrFn是watch屬性的名字,會使用parsePath函式解析路徑,獲取元件上該屬性的值(執行getter)
    
    //依賴(訂閱目標)更新,執行update,會進行取值操作,執行watcher.getter,也就是expOrFn函式
    if(typeof expOrFn === 'function') {
        this.getter = expOrFn;
    } else {
        this.getter = parsePath(expOrFn);
    }
    this.value = this.lazy ? undefined : this.get();
};    
//取值操作
Watcher.prototype.get = function get() {
    //Dep.target設定為該觀察者
    pushTarget(this);
    var vm = this.vm;
    //取值
    var value = this.getter.call(vm, vm);
    //移除該觀察者
    popTarget();
    return value
};
Watcher.prototype.addDep = function addDep(dep) {
    var id = dep.id;
    if(!this.newDepIds.has(id)) {
        //為觀察者的deps新增依賴dep
        this.newDepIds.add(id);
        this.newDeps.push(dep);
        if(!this.depIds.has(id)) {
            //為dep新增該觀察者
            dep.addSub(this);
        }
    }
};
//當一個依賴改變的時候,通知它update
Watcher.prototype.update = function update() {
    //三種watcher,只有計算屬性 watcher的lazy設定了true,表示啟用惰性求值
    if(this.lazy) {
        this.dirty = true;
    } else if(this.sync) {
        //標記為同步計算的直接執行run,三大型別暫無,所以基本會走下面的queueWatcher
        this.run();
    } else {
        //將watcher推入觀察者佇列中,下一個tick時呼叫。
        //也就是資料變化不是立即就去更新的,而是非同步批量去更新的
        queueWatcher(this);
    }
};

//update執行後,執行回撥cb
Watcher.prototype.run = function run() {
    if(this.active) {
        var value = this.get();
        if(
            value !== this.value ||
            isObject(value) ||
            this.deep
        ) {
            var oldValue = this.value;
            this.value = value;
            //執行 cb 函式,這個函式就是之前傳入的watch中的handler回撥函式
            if(this.user) {
                try {
                    this.cb.call(this.vm, value, oldValue);
                } catch(e) {
                    handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
                }
            } else {
                this.cb.call(this.vm, value, oldValue);
            }
        }
    }
};

//對於計算屬性,當取值計算屬性時,發現計算屬性的watcher的dirty是true
//說明資料不是最新的了,需要重新計算,這裡就是重新計算計算屬性的值。
Watcher.prototype.evaluate = function evaluate() {
    this.value = this.get();
    this.dirty = false;
};

//收集依賴
Watcher.prototype.depend = function depend() {
    var this$1 = this;

    var i = this.deps.length;
    while(i--) {
        this$1.deps[i].depend();
    }
};

廣州品牌設計公司https://www.houdianzi.com PPT模板下載大全https://redbox.wode007.com

總結

Observe是對資料進行監聽,Dep是一個訂閱器,每一個被監聽的資料都有一個Dep例項,Dep例項裡面存放了N多個訂閱者(觀察者)物件watcher。

被監聽的資料進行取值操作時(getter),如果存在Dep.target(某一個觀察者),則說明這個觀察者是依賴該資料的(如計算屬性中,計算某一屬性會用到其他已經被監聽的資料,就說該屬性依賴於其他屬性,會對其他屬性進行取值),就會把這個觀察者新增到該資料的訂閱器subs裡面,留待後面資料變更時通知(會先通過觀察者id判斷訂閱器中是否已經存在該觀察者),同時該觀察者也會把該資料的訂閱器dep新增到自身deps中,方便其他地方使用。

被監聽的資料進行賦值操作時(setter)時,就會觸發dep.notify(),迴圈該資料訂閱器中的觀察者,進行更新操作。