簡單實現(xiàn)Vue響應式原理@郝晨光

前言

Vue的數(shù)據(jù)雙向綁定须揣,響應式原理,其實就是通過Object.defineProperty()結合發(fā)布者訂閱者模式來實現(xiàn)的。
我們可以先試著拆分一下Vue的核心模塊伏穆。

  1. Vue構造函數(shù),集中以下模塊實現(xiàn)MVVM纷纫。
  2. Observer 通過Object.definePropty進行數(shù)據(jù)劫持
  3. Dep 發(fā)布訂閱者枕扫,添加觀察者者以及在數(shù)據(jù)發(fā)生改變的時候通知觀察者
  4. Watcher 觀察者,對數(shù)據(jù)進行觀察以及保存數(shù)據(jù)修改需要觸發(fā)的回調
  5. Compiler 模板編譯器辱魁,對HTML模板進行編譯烟瞧,提取其中的變量并轉化為數(shù)據(jù)。
Vue數(shù)據(jù)劫持結合發(fā)布者訂閱者模式工作流程

對于整個Vue響應式的簡單實現(xiàn)來說染簇,在文中并不能做過多的介紹参滴,只能依靠讀者自己去試,按照注釋來進行理解锻弓。推薦的學習方式就是通過本文的實現(xiàn)代碼一步一步的自己實現(xiàn)一下砾赔,然后再自己實現(xiàn)的基礎上自己編寫注釋。



正文

先看一下我們最終實現(xiàn)的效果吧

HTML

<div id="app">
    <p>哈哈</p>
    <h1 @click="setName">{{msg}}</h1>
    <h1>{{a.b}}</h1>
    <input type="text" v-model="a.b">
</div>

JavaScript

new Vue({
    data() {
        return {
            msg: '呃呃呃呃呃',
            name: '郝晨光',
            a: {
                b: 'bbbbb'
            }
        }
    },
    methods: {
        setName() {
            this.msg = '哈哈';
        }
    },
    created() {
        this.msg = '郝晨光哈哈';
        console.log('實例初始化完成')
    },
    mounted() {
        console.log('DOM掛載完成')
    }
}).$mount('#app'); // 此處通過el屬性綁定也是沒有任何問題的
Vue簡單實現(xiàn)

Vue構造函數(shù)

// Vue構造函數(shù)
function Vue(options) {
    // 如果當前Vue不是通過new 關鍵字調用,就進行報錯
    if(!(this instanceof arguments.callee)) {
        error('Vue是一個構造函數(shù)过蹂,必須通過new關鍵字調用十绑!');
    }
    // 如果是的話,就接著執(zhí)行_init方法
    this._init(options);
}
// 實例化Vue的方法
Vue.prototype._init = function(options) {
    // 先將options保存在Vue的this.$options上
    this.$options = options;
    // 再拿到對應的data中的值酷勺,沒有默認為空對象
    this.$data = initData(this.$options) || {};
    // 拿到對應的方法本橙,沒有默認為空對象
    this.$methods = this.$options.methods || {};
    // 進行數(shù)據(jù)劫持
    new Observer(this.$data);
    // 對數(shù)據(jù)和方法進行代理
    proxyData(this, this.$data);
    proxyData(this, this.$methods);
    // 生命周期created函數(shù)
    this.$options.created.apply(this);
    // 如果有el屬性的話,自動調用$mount方法脆诉,掛載到DOM節(jié)點中
    if(this.$options.el) {
        this.$mount(this.$options.el);
    }
};

// $mount方法甚亭,將Vue實例掛載到DOM節(jié)點上
Vue.prototype.$mount = function(el) {
    // 拿到對應的DOM節(jié)點
    let $el = typeof el === 'string'
            ?
            document.querySelector(el)
            : el.nodeType === 1
                ?
                el
                :
                error('el必須是一個選擇器或者是一個DOM節(jié)點!');
    // 將DOM保存在$el屬性上
    this.$el = $el;
    // 通過Compiler編譯器進行編譯
    new Compiler(this.$el, this);
    // 調用mounted生命周期鉤子函數(shù)
    this.$options.mounted.apply(this);
    // 返回當前的Vue實例击胜,保證外部能夠拿到正確的Vue實例
    return this;
};

// 初始化Vue實例的data
function initData(options) {
    // 拿到data的數(shù)據(jù)類型
    const type = typeof options.data;
    // 如果是function的話亏狰,調用函數(shù)拿到對象,否則直接返回對象
    return type === 'function' ? options.data() : options.data;
}

// 對data內的數(shù)據(jù)進行代理
function proxyData(target, proxy) {
    // 拿到對象上的所有key值組成的數(shù)組偶摔,并進行遍歷
    Object.keys(proxy).forEach(key => {
        // 通過Object.defineProperty方法對數(shù)據(jù)進行代理
        Object.defineProperty(target, key, {
            get() {
                return proxy[key];
            },
            set(newValue) {
                proxy[key] = newValue;
            }
        })
    });
}

// 錯誤信息
function error(info) {
    throw new Error(info);
}

Observer數(shù)據(jù)劫持

// 數(shù)據(jù)劫持
function Observer(data) {
    // Observer必須是一個構造函數(shù)暇唾,如果不是通過new關鍵字調用的話,
    // 在內部使用new關鍵字辰斋。
    if(!(this instanceof arguments.callee)) {
        return new arguments.callee(data);
    }
    // 如果data不是一個對象的話策州,提示錯誤,
    // 因為只有對象才能調用Object.defineProperty
    if(!data || typeof data !== 'object') {
        error('代理的data必須是一個對象')
    }
    // 調用observe方法
    this.observe(data);
}

Observer.prototype.observe = function(data) {
    if(!data || typeof data !== 'object') {
        return;
    }
    // 獲取對象上的鍵值數(shù)組并對它進行遍歷
    Object.keys(data).forEach(key => {
        // 調用數(shù)據(jù)劫持方法
        this.defineReactive(data, key, data[key]);
        // 判斷如果當前的值還是對象的話宫仗,遞歸劫持
        if(typeof data[key] === 'object') {
            this.observe(data[key]); // 遞歸劫持所有的值
        }
    })
};

Observer.prototype.defineReactive = function(data, key, value) {
    // 保存this
    const _this = this;
    // 添加觀察者
    let dep = new Dep();
    // 數(shù)據(jù)劫持
    Object.defineProperty(data, key, {
        enumerable: true, // 可枚舉的
        configurable: true, // 可刪除的
        // 代理get
        get() {
            // 當前Dep.target是指的Watcher(訂閱者)實例够挂,
            // 向dep實例中添加Watcher實例
            Dep.target && dep.addSub(Dep.target);
            return value;
        },
        // 代理set
        set(newValue) {
            // 如果新的值和舊的值不相等的情況下
            if(newValue !== value) {
                // 重新調用observe劫持數(shù)據(jù)
                _this.observe(newValue);
                // 設置新的值
                value = newValue;
                // dep實例通知訂閱者進行修改
                dep.notify();
            }
        }
    })
};

Dep發(fā)布者

// Dep發(fā)布者將要執(zhí)行的函數(shù)統(tǒng)一存儲在一個數(shù)組中管理,
// 當達到某個執(zhí)行條件時藕夫,循環(huán)這個數(shù)組并執(zhí)行每一個成員孽糖。
function Dep() {
    this.subs = [];
}

// 在發(fā)布者Dep實例上添加訂閱者
Dep.prototype.addSub = function(watcher) {
    this.subs.push(watcher);
};

// 通知訂閱者進行修改
Dep.prototype.notify = function() {
    // 遍歷所有的訂閱者,調用訂閱者上的update方法進行修改毅贮。
    this.subs.forEach(watcher => watcher.update());
};

Watcher訂閱者

// 訂閱者
function Watcher(vm, variable, callback) {
    // 保存vm實例
    this.vm = vm;
    // 保存需要修改的屬性
    this.variable = variable;
    // 保存屬性修改時需要觸發(fā)的回調
    this.callback = callback;
    // 保存屬性的初始值办悟,并將當前訂閱者添加到發(fā)布者上
    this.value = this.get();
}

Watcher.prototype.get = function() {
    // 將當前的 watcher 添加到Dep發(fā)布者的靜態(tài)屬性上
    Dep.target = this;
    // 獲取到當前的屬性值
    let value = CompilerUtil.getValue(this.vm, this.variable);
    // 在Dep發(fā)布者的靜態(tài)屬性上清除當前 watcher
    Dep.target = null;
    // 返回拿到的值
    return value;
};

Watcher.prototype.update = function() {
    // 發(fā)生修改的時候,重新獲取值
    let newValue = CompilerUtil.getValue(this.vm, this.variable);
    // 先獲取舊的值
    let oldValue = this.value;
    // 如果兩個值不等的話滩褥,調用修改DOM的回調函數(shù)
    if(newValue !== oldValue) {
        this.callback(newValue);
    }
};

Compiler模板編譯器

// Compiler模板編譯器
function Compiler(el, vm) {
    // 先拿到需要編譯的DOM節(jié)點
    this.el = el.nodeType === 1 ? el : document.querySelector(el);
    // 拿到當前的vm實例
    this.vm = vm;
    // 如果當前的el存在誉尖,就開始編譯
    if(this.el) {
        // 將真實的DOM轉換為文檔碎片
        let fragment = this.vNodeFragment(this.el);
        // 調用compile方法進行編譯
        this.compile(fragment);
        // 編譯完成之后再添加到真實DOM中
        this.el.appendChild(fragment);
    }
}

// DOM文檔片段
Compiler.prototype.vNodeFragment = function(el) {
    // 創(chuàng)建文檔片段
    let fragment = document.createDocumentFragment();
    let firstChild;
    // 遍歷當前所有的DOM子節(jié)點
    while (firstChild = el.firstChild) {
        // 將真實DOM節(jié)點添加到文檔片段中
        fragment.appendChild(firstChild);
    }
    // 返回虛擬文檔片段
    return fragment;
};

// 進行編譯
Compiler.prototype.compile = function(fragment) {
    // 拿到文檔片段的所有子節(jié)點
    // 必須通過childNodes拿,因為childNodes不會忽略文本節(jié)點铸题。
    let children = fragment.childNodes;
    // 轉換為真實數(shù)組并進行遍歷
    Array.prototype.slice.call(children).forEach(node => {
        // 如果當前是元素節(jié)點的話,繼續(xù)遞歸遍歷琢感,并編譯元素節(jié)點
        if(node.nodeType === 1) {
            this.compile(node); // 對當前節(jié)點內的子節(jié)點進行遞歸遍歷
            this.compileElement(node); // 編譯元素節(jié)點
        }else {
            // 否則是文本節(jié)點丢间,就開始編譯文本
            this.compileText(node);
        }
    })
};

// 編譯元素節(jié)點
Compiler.prototype.compileElement = function (node) {
    // 獲取到元素所有的屬性
    let attrs = node.attributes;
    // 轉換為真實數(shù)組并進行遍歷
    Array.prototype.slice.call(attrs).forEach(attr => {
        // 獲取到當前的屬性名
        let attrName = attr.name;
        // 判斷當前的屬性是否是指令
        if(attrName.includes('v-')) {
            // 如果是指令的話,拿到當前的屬性值
            let value = attr.value;
            // 拿到當前的指令名
            let [,type] = attrName.split('-');
            // 對當前指令執(zhí)行編譯
            CompilerUtil[type](node, this.vm, value);
            // 判斷當前屬性是否是事件
        }else if(attrName.includes('@')) {
            // 拿到事件名稱
            let event = attrName.slice(1);
            // 拿到事件需要觸發(fā)的方法名稱
            let method = attr.value;
            // 對當前元素添加DOM事件
            CompilerUtil.addEvent(node, event, method, this.vm);
        }
    })
};

// 編譯文本節(jié)點
Compiler.prototype.compileText = function (node) {
    let content = node.textContent; // 獲取文本節(jié)點的內容
    let reg = /\{\{(.+?)\}\}/g; // 匹配模板編譯器的內容
    // 如果能匹配到模板編譯器
    if(reg.test(content)) {
        // 編譯文本節(jié)點
        CompilerUtil.text(node, this.vm, content);
    }
};

模板編譯工具

// 模板編譯工具對象
const CompilerUtil =  {
    // 文本編譯的回調函數(shù)
    textUpdater(node, value) {
        node.textContent = value;
    },
    // input編譯的回調函數(shù)
    modelUpdater(node, value) {
        node.value = value;
    },
    // 獲取vm實例中對應的值
    getValue(vm, variable) {
        // 獲取對象的屬性
        variable = variable.split('.');
        // 通過reduce方法遞歸遍歷vm.$data驹针,拿到最終在vm實例中的屬性值
        return variable.reduce((prev, next) => prev[next], vm.$data);
    },
    // 獲取文本中變量對應的內容
    getTextValue(vm, variable) {
        // 通過正則匹配烘挫,拿到屬性名
        let reg = /\{\{([^}]+)\}\}/g;
        return variable.replace(reg, ($0, $1) => {
            // 通過屬性名,調用getValue方法,獲取屬性值
            return this.getValue(vm, $1);
        })
    },
    // 設置Value
    setValue(vm, variable, newValue) {
        // 獲取對象的屬性名
        variable = variable.split('.');
        // 通過reduce方法遍歷
        return variable.reduce((prev, next, index) => {
            // 如果當前是匹配的屬性名的話
            if(index === variable.length - 1) {
                // 給當前的屬性設置值
                return prev[next] = newValue;
            }
            // 如果不是就返回繼續(xù)計算
            return prev[next];
        }, vm.$data);
    },
    // 雙向數(shù)據(jù)綁定 v-model的簡單實現(xiàn)
    model(node, vm, variable) {
        // 獲取到雙向數(shù)據(jù)綁定的修改方法
        let updateFn = this.modelUpdater;
        // 獲取到對應的值
        let value = this.getValue(vm, variable);
        // 添加訂閱者饮六, 給訂閱者添加回調
        new Watcher(vm, variable, newValue => {
            // 當數(shù)據(jù)發(fā)生修改的時候其垄,就觸發(fā)當前回調,修改元素節(jié)點的值
            updateFn && updateFn(node, newValue);
        });
        // 將v-model屬性從DOM節(jié)點上刪除
        node.removeAttribute('v-model');
        // 給當前元素節(jié)點添加input事件
        node.addEventListener('input', e => {
            // 拿到對應的值
            let value = e.target.value;
            // 設置值
            this.setValue(vm, variable, value);
        });
        // 初次渲染的時候卤橄,也要設置一次值
        updateFn && updateFn(node, value);
    },
    // 添加事件
    addEvent(node, event, method, vm) {
        // 給元素刪除事件符
        node.removeAttribute('@'+event);
        // 給元素添加事件
        node.addEventListener(event, (...args) => {
            // 調用vm上的方法绿满,并傳入?yún)?shù)
            vm[method].apply(vm, args);
        })
    },
    // 編譯文本節(jié)點的變量
    text(node, vm, variable) {
        // 文本節(jié)點的修改函數(shù)
        let updateFn = this.textUpdater;
        // 獲取到文本節(jié)點變量的值
        let value = this.getTextValue(vm, variable);
        // 定義正則
        let reg = /\{\{(.+?)\}\}/g;
        // 通過正則匹配變量,給變量添加觀察者
        variable.replace(reg, ($0, $1) => {
            // 當解析模板遇到變量的時候窟扑,應該使用觀察者監(jiān)聽這個變量
            new Watcher(vm, $1, newValue => {
                // 觀察者的回調函數(shù)喇颁,當數(shù)據(jù)發(fā)生改變就觸發(fā)該回調
                updateFn && updateFn(node, newValue);
            })
        });
        // 第一次設置值
        updateFn && updateFn(node, value);
    }
};



結束

參考文章鏈接:
一起學習、手寫MVVM框架
前端 實現(xiàn)一個簡易版的vue嚎货,了解vue的運行機制
JS實現(xiàn)一個簡易版的vue

如果本文對您有幫助橘霎,可以看看本人的其他文章:
前端常見面試題(十六)@郝晨光
前端常見面試題(十五)@郝晨光
前端常見面試題(十四)@郝晨光

結言
感謝您的查閱,本文由郝晨光整理并總結殖属,代碼冗余或者有錯誤的地方望不吝賜教姐叁;菜鳥一枚,請多關照
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末洗显,一起剝皮案震驚了整個濱河市外潜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌墙懂,老刑警劉巖橡卤,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異损搬,居然都是意外死亡碧库,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門巧勤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嵌灰,“玉大人,你說我怎么就攤上這事颅悉」敛t!?“怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵剩瓶,是天一觀的道長驹溃。 經(jīng)常有香客問我,道長延曙,這世上最難降的妖魔是什么豌鹤? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮枝缔,結果婚禮上布疙,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好灵临,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布截型。 她就那樣靜靜地躺著,像睡著了一般儒溉。 火紅的嫁衣襯著肌膚如雪宦焦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天睁搭,我揣著相機與錄音赶诊,去河邊找鬼。 笑死园骆,一個胖子當著我的面吹牛舔痪,可吹牛的內容都是我干的。 我是一名探鬼主播锌唾,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼锄码,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了晌涕?” 一聲冷哼從身側響起滋捶,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎余黎,沒想到半個月后重窟,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡惧财,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年巡扇,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片垮衷。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡厅翔,死狀恐怖,靈堂內的尸體忽然破棺而出搀突,到底是詐尸還是另有隱情刀闷,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布仰迁,位于F島的核電站甸昏,受9級特大地震影響,放射性物質發(fā)生泄漏徐许。R本人自食惡果不足惜施蜜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望绊寻。 院中可真熱鬧,春花似錦、人聲如沸澄步。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽村缸。三九已至祠肥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間梯皿,已是汗流浹背仇箱。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留东羹,地道東北人剂桥。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像属提,于是被迫代替她去往敵國和親权逗。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內容