手寫簡易版Vue源碼之數據響應化的實現

當前,Vue和React已成爲兩大炙手可熱的前端框架,這兩個框架都算是業內一些最佳實踐的集合體。其中,Vue最大的亮點和特色就是數據響應化,而React的特點則是單向數據流與jsx。

筆者近期正在研究Vue源碼,在此過程中嘗試實現一個簡易版的Vue,而實現Vue的第一步便是解決數據響應化的問題。以下便是對Vue響應化的簡易版實現。

數據響應的原理:

1、依賴收集:data通過Observer變成帶有getter和setter方法的響應式對象,當外界通過Watcher獲取數據時,會將該Watcher加入到Dep的依賴列表中,至此,就算完成了依賴收集

2、通知更新:當外界對已經響應化的對象,即data中的對象進行修改時,會觸發setter方法,setter會通過Dep的通知方法,循環調用Dep依賴列表中Watcher的update方法通知外界更新視圖或觸發用戶所給的監聽回調

 

// kinerVue.js 簡易版小程序入口
import Watcher from './Watcher.js';
import Observer,{set, del} from './Observer.js'
// 預期用法
// let vue = new KinerVue({
//     data(){
//         return {
//             name: "kiner",
//             userInfo: {
//                 age: 20
//             },
//             classify:['game','reading','running']
//         }
//     }
// });


// 數據響應的原理:
// 1、依賴收集:data通過Observer編程帶有getter和setter方法的響應式對象,當外界通過Watcher獲取數據時,會將該Watcher加入到Dep的依賴列表中,至此,就算完成了依賴收集
// 2、通知更新:當外界對已經響應化的對象,即data中的對象進行修改時,會觸發setter方法,setter會通過Dep的通知方法,循環調用Dep依賴列表中Watcher的update方法通知外界更新視圖或觸發用戶所給的監聽回調


/**
 * 自定義簡易版Vue
 */
class KinerVue{


    constructor(options){
        this.$options = options;
        this.$data = options.data.apply(this);

        // 將數據交給Observer,讓Observer將這個數據變成響應式對象
        new Observer(this,this.$data);

        this.isVue = true;

        // test data start


        //測試$watch start
        let unWatchUserInfo = this.$watch("userInfo",(newVal,oldVal)=>{
            console.log(`$watch監聽到[userInfo]發生改變,新值:`,newVal,`;舊值:`,oldVal);
        },{deep: false, immediate: true});
        this.$watch("userInfo.age",(newVal,oldVal)=>{
            console.log(`$watch監聽到[userInfo.age]發生改變,新值:${newVal};舊值:${oldVal}`);
        });
        this.$watch("classify",function classifyWatcher(newVal,oldVal){
            console.log(`$watch監聽到[classify]發生改變,新值:${newVal},;舊值:${oldVal}`);
        });
        this.$watch("friends",function classifyWatcher(newVal,oldVal){
            console.log(`$watch監聽到[friends]發生改變,新值:${newVal},;舊值:${oldVal}`);
        });
        this.userInfo.age = 11;
        // 取消訂閱,執行了這行代碼之後$watch("userInfo",()=>{})將失效
        // unWatchUserInfo();
        this.userInfo.age = 20;

        //通過$set爲數組設置值
        this.$set(this.classify,3,'999');
        this.$set(this.userInfo,'sex','男');
        this.$set(this.userInfo,'sex','女');

        //通過$delete刪除後屬性
        this.$delete(this.userInfo,"sex");

        console.log('sex:',this.userInfo)


        // console.log(this.classify);
        //測試$watch end


        // new Watcher(this,"name");
        // this.name;
        // new Watcher(this,"userInfo.age");
        // this.userInfo.age;
        // new Watcher(this,"classify");
        // this.classify;
        //
        // this.name = 'kanger';
        // console.log(this.name);
        // this.userInfo.age = 18;
        // console.log(this.userInfo.age);
        //
        this.classify.push(10);
        this.classify.splice(5,1,11);
        this.classify.unshift(12);
        this.classify.shift();
        this.classify.sort((a,b)=>a-b);
        this.classify.reverse();


        this.friends.push('zzz');
        this.friends.splice(2,1,'fff');
        this.friends.unshift('kkk');
        this.friends.sort((a,b)=>a-b);
        this.friends.reverse();
        this.friends.shift();
        // 由於未採用ES6的元編程能力,也就是proxy和reflect,因此無法監控類似arr[0]=xxxx和arr.length=0之類的數值變化,
        // 因此,在編碼時要儘量避免這些寫法,以免產生一些不可意料的問題
        //
        // this.classify[2] = 'working'; //錯誤用法
        // console.log(this.classify);


        // test data end
    }

    /**
     * 監聽器,用於監聽屬性變化,並將新舊值傳遞回來,方便做一些攔截操作
     * @param exp       表達式或函數
     * @param cb        回調
     * @param options   配置項
     * @returns {Function}  取消觀察的方法
     */
    $watch(exp,cb,options={immediate: true,deep: false}){
        let watcher = new Watcher(this,exp,cb,options);
        return ()=>{
            watcher.unWatch();
        };
    }

    /**
     * 設置屬性,用來解決無法使用arr[0]=xxx,obj={} obj.name=xxx
     * @param target
     * @param key
     * @param value
     */
    $set(target,key,value){
        return set(target,key,value);
    }

    /**
     * 刪除目標對象上的數據
     * @param target
     * @param key
     * @returns {undefined}
     */
    $delete(target,key){
        return del(target,key);
    }

}

export default KinerVue;

 

// utils.js 基礎工具庫,提供一些工具方法

/**
 * 判斷對象是否支持__proto__屬性
 * @type {boolean}
 */
export const hasProto = '__proto__' in {};
/**
 * 判斷傳遞過來的對象是否是純對象
 * @param obj
 * @returns {boolean}
 */
export const isPlainObject =  function(obj){
    let prototype;

    return Object.prototype.toString.call(obj) === '[object Object]'
        && (prototype = Object.getPrototypeOf(obj), prototype === null ||
        prototype === Object.getPrototypeOf({}))
};

/**
 * 判斷是否爲非空對象
 * @param obj
 * @returns {boolean}
 */
export const isObject = obj => (obj !== null && typeof obj === 'object');

/**
 * 顯示警告消息
 * @param message
 */
export const warn = function (message) {
    console.warn(message);
};



/**
 * 定義不可枚舉的屬性
 * @param obj
 * @param key
 * @param value
 * @param enumerable 能否枚舉
 */
export const def = function (obj,key,value,enumerable) {
    if(typeof obj === "object"){
        Object.defineProperty(obj,key,{
            value: value,
            configurable: true,
            enumerable: !!enumerable,
            writable: true
        });
    }
};

/**
 * 刪除數組中的元素
 * @param arr
 * @param item
 * @returns {T[]}
 */
export const removeArrItem = function (arr, item) {
    const index = arr.indexOf(item);
    if(index!==-1){
        return arr.splice(index,1);
    }
};

/**
 * 根據表達式從目標對象中找到對應的值
 * e.g.
 *      若obj={userInfo:{userName}}
 *      exp="userInfo.userName"
 *
 * @param obj
 * @param exp
 * @returns {*}
 */
export const parseExp = function (exp) {

    return obj => {
        let reg = /[^\w.$]/;
        if(reg.test(exp)){
            return;
        }else{
            let subExp = exp.split('.');
            subExp.forEach(item=>{
                obj = obj[item];
            });
            return obj;
        }
    };
};

/**
 * 判斷兩個變量是否相等(但因爲一個特殊情況,當a和b都等於NaN時,因爲NaN===NaN輸出爲false)
 * @param a
 * @param b
 * @returns {boolean}
 */
export const isEqual = (a,b) => a===b||(a!==a&&b!==b);

/**
 * 將攔截器方法直接覆蓋到目標對象的原型鏈上__proto__
 * @param obj
 * @param target
 * @returns {*}
 */
export const patchToProto = (obj,target) => obj.__proto__ = target;

/**
 * 直接在目標對象上定義不可枚舉的屬性
 * @param obj
 * @param arrayMethods
 * @param keys
 * @returns {*}
 */
export const copyArgument = (obj,arrayMethods,keys) => keys.forEach(key=>def(obj,key,arrayMethods[key]));

/**
 * 判斷當前瀏覽器是否支持__proto__若支持,這直接將目標方法覆蓋到__proto__上,否則,直接將方法定義在目標對象上
 * @param obj
 * @param src
 * @param keys
 * @returns {*}
 */
export const defProtoOrArgument = (obj,src,keys=Object.getOwnPropertyNames(src)) => hasProto ? patchToProto(obj,src) : copyArgument(obj,src,keys);

/**
 * 判斷目標對象是否含有指定屬性
 * @param obj
 * @param key
 * @returns {boolean}
 */
export const hasOwn = (obj,key) => obj.hasOwnProperty(key);

/**
 * 判斷目標對象是否已經響應化
 * @param obj
 * @returns {boolean}
 */
export const hasOb = obj => hasOwn(obj,'__ob__');

/**
 * 判斷傳入參數類型是否爲函數
 * @param fn
 * @returns {boolean}
 */
export const isFn = fn => typeof fn === "function";

/**
 * 判斷所給參數是否是一個數組
 * @param arr
 * @returns {arg is Array<any>}
 */
export const isA = arr => Array.isArray(arr);

/**
 * 判斷給定參數是否是合法的數組索引
 */
export const isValidArrayIndex = (val) => {
    const n = parseFloat(String(val));
    return n >= 0 && Math.floor(n) === n && isFinite(val)
};


export default {
    hasProto,
    isPlainObject,
    isObject,
    warn,
    def,
    removeArrItem,
    isEqual,
    parseExp,
    patchToProto,
    copyArgument,
    defProtoOrArgument,
    hasOwn,
    hasOb,
    isFn,
    isA
}
// Array.js 定義一些針對數組響應化時需要用到的輔助數據以及定義了
import {def} from "./utils.js";

// 數組原型,在對數組方法打補丁的時候,需要用到數組原型方法用於實現原本的數組操作
export const arrayProto = Array.prototype;

/**
 * 需要打補丁的數組方法,即會改變數組的方法
 * @type {string[]}
 */
export const needPatchArrayMethods = [
    "push",
    "pop",
    "unshift",
    "shift",
    "sort",
    "reverse",
    "splice"
];


// 根據數組原型創建一個新的基礎數組對象,避免爲數組方法打補丁的時候污染原始數組
export const arrayMethods = Object.create(arrayProto);


// 實現數組攔截器,通過這個攔截器實現攔截數組操作方法操作
needPatchArrayMethods.forEach(method=>{
    // 從數組原型中將原始方法取出
    const originalMethod = arrayProto[method];

    def(arrayMethods,method,function mutator(...args){
        // const oldVal = [...this];
        // 調用數組原始方法實現數組操作
        const res = originalMethod.apply(this,args);
        // 若當前數組已經是響應化後的數組,則將其Observe實例取出,用戶後續通知更新操作
        const ob = this.__ob__;

        // 若執行的是會新增數組元素的方法,我們需要對新增的元素也進行響應化處理
        // 其中push和unshift接收的所有參數都是新增元素,因此直接將參數對象傳遞給defineReactiveForArray進行響應化處理
        // splice第2個之後的參數便爲新增或替換的元素,因此將第2個之後的參數提取出來,傳遞給defineReactiveForArray進行響應化處理
        let inserted;
        switch (method){
            case "push":
            case "unshift":
                inserted = args;
                break;
            case "splice":
                inserted = args.splice(2);
                break;
        }
        inserted && ob.defineReactiveForArray(inserted);


        //通知依賴更新
        ob&&ob.dep.notify();
        // console.log(`---->觸發了數組的${method}方法:新值:`,this,`;舊值:`,oldVal);
        return res;
    });
});
// Dep.js 依賴類,用於統一管理觀察者,一旦依賴跟新,便可通過此類的notify方法通知其訂閱的所有
// 觀察者進行更新數據

import {removeArrItem} from "./utils.js";

let uid = 0;
/**
 * 用來管理所有的watcher
 */
class Dep {
    constructor(){
        // 訂閱者列表
        this.subs = [];
        // 爲每一個依賴定義一個唯一的id
        this.id = uid++;
    }

    /**
     * 觸發添加依賴
     */
    depend(){
        //爲實現取消訂閱的功能,將訂閱的方法放在watcher中,此處通過調用watcher的addDep將當前依賴加入到訂閱列表,
        Dep.target&&Dep.target.addDep(this);
        // 初版實現,未實現取消訂閱功能
        // Dep.target&&this.addDep(Dep.target);
    }

    /**
     * 添加訂閱者
     * @param watcher 訂閱者
     */
    addSub(watcher){
        // 爲解決當調用數組的splice和sort方法時,會觸發多次更新的問題,加入訂閱時先看一下該依賴是否已經被添加
        if(this.subs.indexOf(watcher)<0){
            this.subs.push(watcher);
        }

    }

    /**
     * 從從訂閱列表中移除訂閱者
     * @param watcher
     */
    removeSub(watcher){
        removeArrItem(this.subs,watcher);
    }


    /**
     * 通知訂閱者更新
     */
    notify(){
        this.subs.forEach(watcher=>{
            watcher.update()
        });
    }
}

export default Dep;
/**
 * Observer.js 數據響應化對象
 * Vue數據響應化的核心,Vue2.0時代通過Object.defineProperty方式進行數據響應化,而Vue3.0時代則採用Proxy和Reflect方式實現
 * 無論採用哪種方式,但其實現原理都是一樣的,都是通過數據劫持的方式實現響應化
 */

import Dep from "./Dep.js";
import {isPlainObject, warn, isEqual,defProtoOrArgument, hasOb, def, isA, isValidArrayIndex, hasOwn} from "./utils.js";
import {arrayMethods} from "./Array.js";

class Observer {

    /**
     * 定義統一的操作方法,方便之後收集依賴和響應通知的統一操作
     * @param obj   待響應的對象
     * @param key   待響應的鍵值
     * @param value 待響應的值
     * @param childOb 子響應對象
     * @returns {*}
     */
    static baseHandler(obj, key, value,childOb) {
        //定義一個依賴對象,與data的key存在一一對應的關係
        const dep = new Dep();
        return {
            enumerable: true,
            configurable: true,
            get() {
                // Dep.target && dep.addDep(Dep.target);
                // 在訪問對象屬性時,將當前屬性加入到依賴列表中
                dep.depend();

                // console.log('收集依賴',obj,key,value,childOb);
                // 用於收集數組對象的依賴
                childOb && childOb.dep.depend();

                // console.log(`獲取${key}的值:${value}`);
                return value;
            },
            set(val) {
                // isEqual:原本的目的是爲了判斷新值val和舊值value相等的情況下,便直接退出,
                // 但因爲一個特殊情況,當val和value都等於NaN時,因爲NaN===NaN輸出爲false
                // 會讓set方法繼續往下執行,因此多加了一個(value!==value&&val!==val)進行攔截
                // 
                if (isEqual(val, value)) {
                    return;
                }
                // 由於舊值仍處於閉包當中,this.$data未釋放的情況下,直接對value賦值可直接操作this.$data下對應鍵值下的數據,所以進行以下賦值操作
                value = val;
                // 通知依賴列表循環更新依賴
                dep.notify();

                // console.log(`設置${key}的值:${val}`);

            }
        }
    };


    constructor(vm, target) {
        this.$vm = vm;

        // 將跟數據target設置爲已響應,以免重複創建示例
        def(target,'__ob__',this);

        //在此定義依賴收集對象,用來收集數組的依賴
        this.dep = new Dep();

        // 將目標變化變爲響應式對象
        this.observer(target);

    }

    /**
     * 對傳入的數據進行響應化處理
     * @param data
     */
    observer(data) {
        if (Array.isArray(data)) {//傳過來的數據是否是數組
            defProtoOrArgument(data,arrayMethods);
            return this.defineReactiveForArray(data)
        } else if (isPlainObject(data)) {//傳遞過來的
            return this.defineReactiveForObject(data);
        } else {
            warn(`傳遞的數據必須是對象或數組,當前傳遞的值【${data}】類型爲:${typeof data},因此無需響應化`);
        }
    }


    /**
     * 實現對象類型的響應化處理
     * @param obj
     */
    defineReactiveForObject(obj) {
        let keys = Object.keys(obj);

        keys.forEach(key => {
            this.defineReactive(obj, key, obj[key]);
            // 添加數據代理,將$data中的值代理到this,這樣就可以直接通過this.xxx訪問$data中的屬性了
            this.proxyData(key);
        });
    }

    /**
     * 實現數組類型的響應化處理
     * @param data
     */
    defineReactiveForArray(data) {

        data.forEach(item=>this.createObserver(item));

    }

    /**
     * 將對象變爲響應式對象,通過遞歸調用observer方法可以實現嵌套對象響應化
     * @param obj   帶響應化對象
     * @param key   待響應的鍵值
     * @param value 待響應的值
     */
    defineReactive(obj, key, value) {

        let childOb = this.createObserver(value);

        Object.defineProperty(obj, key, Observer.baseHandler(obj, key, value,childOb));

    }


    /**
     * 判斷目標數據是否已經響應化,如果響應化,則直接返回其響應化對象__ob__,佛則示例話一個響應化對象
     * @param data
     * @returns {*}
     */
    createObserver(data){
        let ob;
        if(hasOb(data)){//該對象已經響應化,直接獲取
            ob = data.__ob__;
        }else{
            ob = new Observer(this.$vm,data);
        }
        return ob;
    }
   


    /**
     * 代理$data,將$data中的數據代理到vue實例中,便可直接通過this.xxx獲取或設置值
     * @param key
     * @returns {*}
     */
    proxyData(key) {
        Object.defineProperty(this.$vm, key, {
            get() {
                return this.$data[key];
            },
            set(val) {
                this.$data[key] = val;
            }
        })
    }

}

/**
 * 爲目標對象或數組增加設置/新增值
 * @param target
 * @param key
 * @param val
 * @returns {*}
 */
export const set = (target,key,val)=>{

    //如果target是數組且key是合法的數組索引,則將目標值加入到數組中
    if(isA(target) && isValidArrayIndex(key)){
        target.length = Math.max(target.length,key);
        target.splice(key,1,val);
        return val;
    }

    //如果key是target非原型鏈上的屬性,說明該key已經是響應化對象了,無需重複響應化,直接修改對應的值即可
    if(key in target && !(key in Observer.prototype)){
        target[key] = val;
        return val;
    }

    //新增屬性
    const ob = target.__ob__;

    //如果當前對象未被響應化,則直接設置目標值
    if(!ob){
        target[key] = val;
        return val;
    }

    // TODO 不能在跟對象this.$data和Vue示例上添加屬性

    // 如果target是響應化對象,則通過Observer的defineRelative方法設置屬性
    ob.defineReactive(target,key,val);
    ob.dep.notify();
    return val;

};

export const del = (target,key) => {
    //如果target是數組且key是合法的數組索引,則刪除掉指定索引的數組項
    if(isA(target) && isValidArrayIndex(key)){
        target.splice(key,1);
        return;
    }
    //若target本身就不具有key屬性,則無需刪除,直接返回
    if(!hasOwn(target,key)) return;

    // TODO 不能在跟對象this.$data和Vue示例上刪除屬性

    const ob = target.__ob__;
    delete target[key];
    // 通知依賴更新
    ob && ob.dep.notify();
};


export default Observer;

 

// Watcher.js
// 它相當於是依賴Dep與具體的更新操作的一箇中介,也可以理解爲他是一個物流中轉站,依賴就像是快遞,具體更新操作就是快遞的目的地,具體流程是這樣的:
// 我們把快遞(更新)交給快遞代收點(Dep),當快遞代收點(Dep)接收到快遞之後,會有人來收集快遞送到快遞中轉站(watcher),然後再由快遞中轉賬再統一派發到不同的地址。
import Dep from './Dep.js';
import {parseExp, isObject,isFn} from "./utils.js";
import {arrayMethods} from "./Array.js";
import {traverse} from "./Traverse.js";

class Watcher {

    constructor(vm,expOrFn,cb=function(){},options={immediate: true,deep: false}){
        // 創建實例時,將當前實例對象指向Dep的靜態屬性target
        this.$vm = vm;
        // 需要堅挺的表達式或者是給定的函數(注:如爲函數,則可在函數內使用到的響應化對象屬性都會被觀察,一旦任一屬性值發生變化,都會觸發cb回調通知)
        this.expOrFn = expOrFn;
        // 選項
        // options.immediate  true|false  代表是否在創建watcher實例時變直接運行表達式或函數獲取結果
        // options.deep       true|false  代表是否進行深度觀察,如果爲true,會對指定表達式或對象下使用的屬性的子屬性進行遞歸觀察操作
        //// e.g. data下的對象userInfo的結構是:userInfo:{friends:[{name:'kiner'},{name:'kanger'}],bankInfo:{bankCardNum: 'xxxxxxx'}}
        //// 那麼,如果我們要進行深度觀察,則如:this.$watch("userInfo",()=>{},{deep:true})
        //// 此後,一旦userInfo下面的任一屬性,包括子對象、數組中的值發生改變,上述的$watch都能夠觀察得到
        this.options = options;

        // 若給出的是函數,則直接將其賦值給gutter
        if(isFn(expOrFn)){
            this.gutter = expOrFn;
        }else{
            // 若給出的是一個如:userInfo.name或age之類的表達式,則通過parseExp這個高階函數將表達式進行一定的處理並賦值給gutter
            // 使我們可以直接通過this.gutter.call(this.$vm,this.$vm);的方式直接獲得表達式對應的結果
            this.gutter = parseExp(expOrFn);
        }

        // 觀察者通知的回調函數
        this.cb = cb;


        // 爲實現取消訂閱功能,需要知道watcher都訂閱了哪些依賴,在取消訂閱時,秩序把對應的依賴從依賴列表移除即可
        // 爲方便訂閱,將依賴列表從Dep移到watcher
        this.deps = [];

        // 爲了標誌依賴的唯一性,定義一個不可重複的Set用於存儲依賴的id
        this.depIds = new Set();

        // 如果指定immediate=true則在實例化時離開觸發get獲取目標值
        if(options.immediate){
            this.value = this.get();
        }


    }

    /**
     * 嘗試通過表達式或者所給方法獲取目標值
     * @returns {*}
     */
    get(){
        Dep.target = this;//指定快遞代收點所屬的中轉站,這樣才能夠將快遞精確的從代收點送到中轉站
        //根據給定的表達式或函數直接或取目標值,與此同時,因爲觸發了get,會將Dep.target添加到依賴列表當中
        let value = this.gutter.call(this.$vm,this.$vm);
        this.sourceValue = value;

        // 若需要觀察對象系所有子對象的變化(注:此步驟必須放在`Dep.target = undefined;`之前,因爲遞歸收集子對象依賴時仍需要使用到Dep.target)
        if(this.options.deep){
            traverse(value);
        }


        //嘗試解決當value爲數組或對象時,newVal和oldVal恆等問題(注:此步驟是因爲個人開發原因需要獲取對象或數組的新舊值,爲方便操作,嘗試性實現,Vue官方並無此步驟)
        if(value.__proto__===arrayMethods){
            value = [...value];
        }else if(isObject(value)){
            value = {...value}
        }

        // 加入依賴列表之後釋放target
        Dep.target = undefined;
        return value;
    }

    // 中轉站已經收到快遞了,準備派送,通知各位快遞小哥過來拿各自負責區域(視圖中的表達式或$watch中監聽的方法)的快遞進行派送
    update(){
        // 接收到更新通知時,觸發get方法獲取改表達式最新的值
        const value = this.get();
        // vue源碼中:如果value是數組/對象時,我們通過$watch((newVal,oldVal)=>{})獲取到的newVal和oldVal其實是始終相等的,因爲他們都是東一個對象的引用
        if(this.value!==value||isObject(value)){

            const oldVal = this.value;
            this.value = value;
            // 將新舊值傳遞給回調函數,即完成$watch('xxxxx',function(newVal,oldVal){})的通知
            this.cb.call(this.$vm,value,oldVal);
        }

        // console.log(`屬性${this.expOrFn}發生了變化`);
    }

    /**
     * 添加依賴並經自己訂閱到依賴當中
     * @param dep
     */
    addDep(dep){
        const depId = dep.id;
        // 判斷依賴是否已經在依賴列表中,若不存在,則添加依賴
        if(!this.depIds.has(depId)){
            this.deps.push(dep);
            this.depIds.add(depId);
            // 爲添加的依賴訂閱觀察者
            dep.addSub(this);
        }

    }

    /**
     * 取消觀察,移除依賴列表中所有的當前觀察者
     */
    unWatch(){
        let len = this.deps.length;
        while (len--){
            this.deps[len].removeSub(this);
        }
    }

}

export default Watcher;
// Traverse.js 通過traverse遞歸訪問指定對象,通過觸發getter的方式實現依賴收集
import {isA,isObject,hasOb} from "./utils.js";

// 用於存儲依賴id
const depIds = new Set();

// 通過這個方法訪問一下給定目標對象的子對象,從而觸發依賴通知
export const traverse = (val) => {
    _traverse(val,depIds);
    depIds.clear();
};

function _traverse(val,depIds){
    let len,keys;
    // 所傳對象如果類型不是非凍結對象或數組,就直接終止
    if((!isA(val) && !isObject(val)) || Object.isFrozen(val)){
        return;
    }
    // 判斷當前對象是否已經是響應化對象
    if(hasOb(val)){
        const  depId = val.__ob__.dep.id;
        if(depIds.has(depId)){//已經訪問過了,直接終止
            return;
        }
        //若未訪問過,則將依賴id加入到depIds中
        depIds.add(depId);
    }

    if(isA(val)){//如果是數組,則循環訪問其子項並遞歸訪問
        len = val.length;
        while (len--) _traverse(val,depIds);
    }else{//循環對象下的所有屬性並遞歸訪問
        keys = Object.keys(val);
        len = keys.length;
        while (len--) _traverse(val,depIds);
    }
}

 

以上代碼已實現功能:

  1. 數據響應化-Observe.js(Array.js-數組響應化的一些相關處理)
  2. 數據觀察者-Watcher.js(Traverse.js-通過traverse遞歸訪問指定對象,通過觸發getter的方式實現依賴收集)
  3. 依賴管理者-Dep.js
  4. 工具方法$watch-觀察屬性變化的方法、$set-爲對象添加屬性或者爲數組添加子項,並通知依賴更新、$delete-刪除對象屬性或刪除數組子項並通知依賴更新

以後將陸續會嘗試實現:

  1. 虛擬Dom(VNode)
  2. 編譯器
  3. Vue生命週期鉤子、工具方法、全局Api實現
  4. 指令的解析
  5. 過濾器的實現
  6. 行業最佳實踐學習(Vue-Router、Vuex、Element-UI、Element-Admin、Vant)

文章將根據本人對Vue及其相關最佳實踐的學習進度不斷更新,如有不對,歡迎指正,謝謝!

PS:如果對Vue3(即vue-next)感興趣的同學,可以看一下本人撰寫的另一篇文章 Vue3(Vue-next)響應化實現剖析,這裏簡單的實現了使用es6元編程能力(proxy和reflect)實現的數據響應化原理。

 

 

發佈了24 篇原創文章 · 獲贊 12 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章