Vue學(xué)習(xí)系列一 —— MVVM響應(yīng)式系統(tǒng)的基本實現(xiàn)原理

MVVM是什么

MVVM是Model-View-ViewModel的簡寫捧存。它模式是MVC—>MVP—>MVVM的進化版愁铺。
Model負(fù)責(zé)用JavaScript對象表示,View負(fù)責(zé)UI界面顯示,兩者做到了最大限度的分離远搪。
而把Model和View關(guān)聯(lián)起來的就是ViewModel峡捡。ViewModel負(fù)責(zé)把Model的數(shù)據(jù)同步到View顯示出來击碗,還負(fù)責(zé)把View的界面修改同步回Model更新數(shù)據(jù)。

主流MVVM框架和實現(xiàn)做法

  • 臟值檢查(angular.js)
  • 發(fā)布者-訂閱者模式+數(shù)據(jù)劫持(vue.js)

臟值檢查: angular.js 是通過臟值檢測的方式來比對數(shù)據(jù)是否有變更而決定是否更新視圖棋返。
原理是延都,拷貝一份copy_viewModel在內(nèi)存中,用戶操作導(dǎo)致viewModel發(fā)生改變的行為時,框架都會把copy_viewModel和最新的viewModel進行深度比較睛竣,一旦發(fā)現(xiàn)有屬性發(fā)生變化晰房,則重新渲染與之綁定的DOM節(jié)點。
最簡單的方式就是通過setInterval()定時輪詢檢測數(shù)據(jù)變動射沟,angular觸發(fā)時進入臟值檢測殊者。但只限 指定的事件 (如:用戶點擊,輸入操作验夯,ajax請求猖吴,setInterval,setTimeout等...)挥转,否則需手動調(diào)用apply函數(shù)去強制執(zhí)行一次臟檢查海蔽。

數(shù)據(jù)劫持: vue.js 則是采用數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式共屈,通過Object.defineProperty()來劫持各個屬性的settergetter在數(shù)據(jù)變動時發(fā)布消息給訂閱者党窜,觸發(fā)相應(yīng)的監(jiān)聽回調(diào)拗引,而產(chǎn)生更新數(shù)據(jù)和視圖。

vue數(shù)據(jù)雙向綁定原理

官網(wǎng)數(shù)據(jù)綁定說明圖

原理圖告訴我們幌衣,data屬性定義了getter矾削、setter對屬性進行劫持,當(dāng)屬性值改變是就會notify通知watch對象豁护,而watch對象則會重新觸發(fā)組件呈現(xiàn)功能哼凯,繼而更新view上的DOM節(jié)點樹。
反之楚里,view上輸入數(shù)據(jù)時断部,也會觸發(fā)data變更,也會觸發(fā)訂閱者watch更新,這樣子model數(shù)據(jù)就可以實時更新view上的數(shù)據(jù)變化腻豌。這樣一個過程就是vue的數(shù)據(jù)雙向綁定了家坎。

vue是通過數(shù)據(jù)劫持的方式來做數(shù)據(jù)綁定的,其中最核心的方法便是通過Object.defineProperty()來實現(xiàn)對屬性的劫持吝梅,達(dá)到監(jiān)聽數(shù)據(jù)變動的目的虱疏。

Object.defineProperty

Object.defineProperty是ES5一個方法,可以直接在一個對象上定義一個新屬性苏携,或者修改一個已經(jīng)存在的屬性做瞪,并返回這個對象,對象里目前存在的屬性描述符有兩種主要形式:數(shù)據(jù)描述符存取描述符右冻。
數(shù)據(jù)描述符是一個擁有可寫或不可寫值的屬性装蓬。
存取描述符是由一對getter-setter函數(shù)功能來描述的屬性。
描述符必須是兩種形式之一牍帚;不能同時是兩者。即:有值和可寫乳蛾,或者可get和set
屬性描述符包括:

  • Configurable(可配置性相當(dāng)于屬性的總開關(guān)暗赶,只有為true時才能設(shè)置,而且不可逆)肃叶、
  • Enumerable(是否可枚舉蹂随,為false時for..in以及Object.keys()將不能枚舉出該屬性)、
  • Writable(是否可寫因惭,為false時將不能夠修改屬性的值)岳锁、
  • Value(屬性的值,默認(rèn)為undefined)、
  • Get(一個給屬性提供getter的方法)蹦魔、
  • Set(一個給屬性提供setter的方法)激率、
var Book = {}
Object.defineProperty(Book, 'name', {
  get: function () {
    return '《' + name + '》'
  },
  set: function (value) {
    name = value;
    console.log('你取了一個書名叫做' + value);
  }
})

console.log(Book.name);  // 《》
Book.name = 'vue權(quán)威指南';  // 你取了一個書名叫做vue權(quán)威指南
console.log(Book.name);  // 《vue權(quán)威指南》

實現(xiàn)過程

我們已經(jīng)知道怎么實現(xiàn)數(shù)據(jù)的雙向綁定咳燕,首先要對數(shù)據(jù)進行劫持監(jiān)聽,所以我們需要設(shè)置一個監(jiān)聽器Observer柱搜,用來監(jiān)聽所有屬性迟郎。如果屬性發(fā)上變化了,就需要告訴訂閱者Watcher看是否需要更新聪蘸。因為訂閱者是有很多個,所以我們需要有一個消息訂閱器Dep來專門收集這些訂閱者表制,然后在監(jiān)聽器Observer和訂閱者Watcher之間進行統(tǒng)一管理的健爬。接著,我們還需要有一個指令解析器Compile么介,對每個節(jié)點元素進行掃描和解析娜遵,將相關(guān)指令對應(yīng)初始化成一個訂閱者Watcher,并替換模板數(shù)據(jù)或者綁定相應(yīng)的函數(shù)壤短,此時當(dāng)訂閱者Watcher接收到相應(yīng)屬性的變化设拟,就會執(zhí)行對應(yīng)的更新函數(shù),從而更新視圖久脯。
因此接下去我們執(zhí)行以下4個步驟纳胧,實現(xiàn)數(shù)據(jù)的雙向綁定:

  1. 實現(xiàn)一個監(jiān)聽器Observer,用來劫持并監(jiān)聽所有屬性帘撰,如果有變動的跑慕,就拿到最新值并通知訂閱者。
  2. 實現(xiàn)一個訂閱者Watcher摧找,連接ObserverCompile核行。可以訂閱并收到每個屬性的變化通知并執(zhí)行指令綁定的相應(yīng)函數(shù)蹬耘,從而更新視圖芝雪。
  3. 實現(xiàn)一個解析器Compile,可以掃描和解析每個節(jié)點的相關(guān)指令综苔,并根據(jù)初始化模板替換數(shù)據(jù)惩系,以及綁定相應(yīng)的更新函數(shù)。
  4. mvvm入口函數(shù)休里,整合以上三者蛆挫。
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <div id="app">
        <h2>{{title}}</h2>
        <input v-model="name">
        <h1>{{name}}</h1>
        <button v-on:click="clickMe">click me!</button>
        <p>aaaa{{xxx}}zzzz</p>
    </div>
    <!-- <h1 id="name">{{name}}</h1> -->
</body>

</html>
<script>
    /**** 
     * Observer
     * 
     * */
    //初始化數(shù)據(jù)監(jiān)聽器
    function observe(data) {
        //驗證傳入的參數(shù)格式
        if (!data || typeof data !== 'object') {
            return;
        }
        // var dep = new Dep(); //創(chuàng)建訂閱器Dep
        // console.log(dep)
        //遍歷所有屬性
        Object.keys(data).forEach(function (key) {
            defineReactive(data, key, data[key])//所有數(shù)據(jù),單個鍵妙黍,單個值
            console.log(data)
            console.log(key)
            console.log(data[key])
        })
        console.log(Object.keys(data))
    }

    //監(jiān)聽所有屬性
    function defineReactive(data, key, val) {
        observe(val); // 遞歸遍歷所有子屬性
        var dep = new Dep();//創(chuàng)建訂閱器Dep
        Object.defineProperty(data, key, {
            enumerable: true, // 可枚舉
            configurable: false, // 可配置
            get: function () {//返回它本身
                console.log(Dep)
                console.log(Dep.target)
                if (Dep.target) { // 判斷是否需要添加訂閱者
                    dep.addSub(Dep.target); // 在這里添加一個訂閱者
                }
                return val;
            },
            set: function (newVal) {//返回更新值
                val = newVal;
                console.log('屬性' + key + '已經(jīng)被監(jiān)聽了悴侵,現(xiàn)在值為:“' + newVal.toString() + '”');
                console.log(dep)
                dep.notify(); // 如果數(shù)據(jù)變化,通知所有訂閱者
            }
        })
    }
    console.log(Dep)
    Dep.target = null;

    //訂閱器容器
    function Dep() {
        this.subs = [];
    }

    //訂閱器原型方法
    Dep.prototype = {
        //添加進訂閱器容器
        addSub: function (sub) {
            this.subs.push(sub);
        },
        //通知所有訂閱者
        notify: function () {
            this.subs.forEach(function (sub) {
                console.log(sub)
                sub.update();
            });
        }
    };
    /**** 
     * Watcher
     * 
     * */
    //初始化Watcher訂閱者
    function Watcher(vm, exp, cb) {//實例本身拭嫁, 模板鍵值可免,模板值重新賦值方法
        console.log(vm)
        console.log(exp)
        console.log(cb)
        this.cb = cb;
        this.vm = vm;
        this.exp = exp;
        this.value = this.get();  // 將自己添加到訂閱器的操作
    }

    Watcher.prototype = {
        update: function () {
            this.run();
        },
        run: function () {
            var value = this.vm.data[this.exp];
            var oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal);//實例的賦值方法call到訂閱者
            }
        },
        //讓實例設(shè)置的屬性強制映射到結(jié)構(gòu)樹上
        get: function () {
            console.log(Dep.target)
            console.log(Dep)
            Dep.target = this;  // 緩存自己
            var value = this.vm.data[this.exp]  // 強制執(zhí)行監(jiān)聽器里的get函數(shù)
            Dep.target = null;  // 釋放自己
            return value;
        }
    };
    /**** 
     * Compile
     * 
     * */
    function Compile(el, vm) {//dom節(jié)點抓于,實例對象
        this.vm = vm;
        this.el = document.querySelector(el);
        this.fragment = null;
        this.init();
    }
    Compile.prototype = {
        // 初始化
        init: function () {
            if (this.el) {
                this.fragment = this.nodeToFragment(this.el);
                this.compileElement(this.fragment);
                this.el.appendChild(this.fragment);//掛載點載入模板碎片
            } else {
                console.log('Dom元素不存在');
            }
        },
        //創(chuàng)建一個fragment片段,用于解析的dom節(jié)點
        nodeToFragment: function (el) {
            var fragment = document.createDocumentFragment();//創(chuàng)建fragment-DOM模板碎片
            var child = el.firstChild;
            while (child) {
                // 將Dom元素移入fragment中
                fragment.appendChild(child);
                child = el.firstChild
            }
            return fragment;
        },
        //獲取起始節(jié)點下所有節(jié)點并且遞歸遍歷所有符合{{}}的指令
        compileElement: function (el) {
            var childNodes = el.childNodes;
            var self = this;
            //數(shù)組分割的方法作用于起始節(jié)點下所有節(jié)點并遍歷每個節(jié)點執(zhí)行對應(yīng)方法
            [].slice.call(childNodes).forEach(function (node) {
                var reg = /\{\{(.*)\}\}/;//{{}}指令的正則
                var text = node.textContent;//節(jié)點的內(nèi)容

                //v-model指令和事件指令的解析編譯
                if (self.isElementNode(node)) {
                    self.compile(node);
                } else if (self.isTextNode(node) && reg.test(text)) {  // 判斷是否是符合這種形式{{}}的指令
                    self.compileText(node, reg.exec(text)[1]);
                }

                if (node.childNodes && node.childNodes.length) {
                    self.compileElement(node);  // 繼續(xù)遞歸遍歷子節(jié)點
                }
            });
        },
        // 執(zhí)行v-model指令和事件指令的解析編譯
        compile: function (node) {
            var nodeAttrs = node.attributes;//獲取該元素上的長度
            var self = this;
            //遍歷該元素上的所有屬性
            Array.prototype.forEach.call(nodeAttrs, function (attr) {
                var attrName = attr.name;
                if (self.isDirective(attrName)) {
                    var exp = attr.value;//指定model的value值
                    var dir = attrName.substring(2);
                    if (self.isEventDirective(dir)) {  // 事件指令
                        self.compileEvent(node, self.vm, exp, dir);
                    } else {  // v-model 指令
                        self.compileModel(node, self.vm, exp, dir);
                    }
                    node.removeAttribute(attrName);
                }
            });
        },
        //執(zhí)行{{}}的節(jié)點的值
        compileText: function (node, exp) {//每個符合{{}}的節(jié)點浇借,{{}}里面的內(nèi)容值
            var self = this;
            var initText = this.vm[exp];
            this.updateText(node, initText);  // 將初始化的數(shù)據(jù)初始化到視圖中
            new Watcher(this.vm, exp, function (value) { // 生成訂閱器并綁定更新函數(shù)
                self.updateText(node, value);
            });
        },
        //執(zhí)行事件的節(jié)點的值
        compileEvent: function (node, vm, exp, dir) {
            var eventType = dir.split(':')[1];
            var cb = vm.methods && vm.methods[exp];

            if (eventType && cb) {
                node.addEventListener(eventType, cb.bind(vm), false);
            }
        },
        //執(zhí)行模塊的節(jié)點的值
        compileModel: function (node, vm, exp, dir) {
            var self = this;
            var val = this.vm[exp];
            this.modelUpdater(node, val);
            new Watcher(this.vm, exp, function (value) {
                self.modelUpdater(node, value);
            });

            node.addEventListener('input', function (e) {
                var newValue = e.target.value;
                if (val === newValue) {
                    return;
                }
                self.vm[exp] = newValue;
                val = newValue;
            });
        },
        //更新文本
        updateText: function (node, value) {
            node.textContent = typeof value == 'undefined' ? '' : value;
        },
        //更新模塊
        modelUpdater: function (node, value, oldValue) {
            node.value = typeof value == 'undefined' ? '' : value;
        },
        // 判斷是是不是v-指令
        isDirective: function (attr) {
            return attr.indexOf('v-') == 0;
        },
        // 判斷是是不是on:事件指令
        isEventDirective: function (dir) {
            return dir.indexOf('on:') === 0;
        },
        // 判斷元素節(jié)點 元素類型等于1
        isElementNode: function (node) {
            return node.nodeType == 1;
        },
        // 判斷文本節(jié)點
        isTextNode: function (node) {
            return node.nodeType == 3;
        }
    }
    /**** 
     * Observer和Watcher
     * 
     * */
    function SelfVue(options) {// 整個實例對象   //data, el, exp 所有數(shù)據(jù)捉撮,選中元素,模板鍵值
        var self = this;
        this.vm = this;
        this.data = options.data;
        this.methods = options.methods;
        //賦值時妇垢,屬性的綁定做一層封裝
        Object.keys(this.data).forEach(function (key) {
            self.proxyKeys(key);  // 綁定代理屬性
        });
        //劫持并監(jiān)聽所有屬性
        observe(this.data);
        //解析器解析掛載點的指令
        new Compile(options.el, this.vm)//掛載點巾遭,實例對象
        options.mounted.call(this); // 所有事情處理好后執(zhí)行mounted函數(shù)

        // el.innerHTML = this.data[exp];  // 初始化模板數(shù)據(jù)的值 // 內(nèi)容為設(shè)置的鍵值
        // console.log(el.innerHTML)
        // console.log(this)
        // new Watcher(this, exp, function (value) {//selfvue本身,模板鍵值闯估,模板值為監(jiān)聽的新值
        //     el.innerHTML = value;
        // });
        return this;
    }
    //讓selfVue的屬性代理為訪問selfVue.data的屬性
    SelfVue.prototype = {
        proxyKeys: function (key) {
            var self = this;
            Object.defineProperty(this, key, {
                enumerable: false,
                configurable: true,
                get: function proxyGetter() {
                    return self.data[key];
                },
                set: function proxySetter(newVal) {
                    self.data[key] = newVal;
                }
            });
        }
    }
    /**** 
     * 實例
     * 
     * */
    var selfVue = new SelfVue({
        el: '#app',
        data: {
            title: 'hello world',
            name: 'null',
            xxx: 'cjh'
        },
        methods: {
            clickMe: function () {
                this.title = 'hello world';
            }
        },
        mounted: function () {
            window.setTimeout(() => {
                this.title = '你好';
            }, 2000);
        }
    });

    // window.setTimeout(function () {
    //     selfVue.title = '你好';
    // }, 2000);
    // window.setTimeout(function () {
    //     selfVue.name = 'canfoo';
    // }, 2500);

    // //實例
    // var ele = document.querySelector('#name');
    // var selfVue = new SelfVue({
    //     name: 'hello world'
    // }, ele, 'name');
    // console.log(ele)
    // console.log('name')

    // window.setTimeout(function () {
    //     console.log('name值改變了');
    //     selfVue.name = 'canfoo';
    // }, 2000);


    // //實例
    // var library = {
    //     book1: {
    //         name: ''
    //     },
    //     book2: ''
    // };
    // observe(library);
    // library.book1.name = 'vue權(quán)威指南'; // 屬性name已經(jīng)被監(jiān)聽了灼舍,現(xiàn)在值為:“vue權(quán)威指南”
    // library.book2 = '沒有此書籍';  // 屬性book2已經(jīng)被監(jiān)聽了,現(xiàn)在值為:“沒有此書籍”
    // console.log(library)
</script>
參考鏈接:

深入響應(yīng)式原理
剖析Vue原理&實現(xiàn)雙向綁定MVVM
《響應(yīng)式系統(tǒng)的基本原理》.js
JavaScript實現(xiàn)MVVM之我就是想監(jiān)測一個普通對象的變化

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末涨薪,一起剝皮案震驚了整個濱河市骑素,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌刚夺,老刑警劉巖献丑,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異侠姑,居然都是意外死亡创橄,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進店門结借,熙熙樓的掌柜王于貴愁眉苦臉地迎上來筐摘,“玉大人,你說我怎么就攤上這事船老】欤” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵柳畔,是天一觀的道長馍管。 經(jīng)常有香客問我,道長薪韩,這世上最難降的妖魔是什么确沸? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮俘陷,結(jié)果婚禮上罗捎,老公的妹妹穿的比我還像新娘。我一直安慰自己拉盾,他們只是感情好桨菜,可當(dāng)我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般倒得。 火紅的嫁衣襯著肌膚如雪泻红。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天霞掺,我揣著相機與錄音谊路,去河邊找鬼。 笑死菩彬,一個胖子當(dāng)著我的面吹牛缠劝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播骗灶,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼剩彬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了矿卑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤沃饶,失蹤者是張志新(化名)和其女友劉穎母廷,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體糊肤,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡琴昆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了馆揉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片业舍。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖升酣,靈堂內(nèi)的尸體忽然破棺而出舷暮,到底是詐尸還是另有隱情,我是刑警寧澤噩茄,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布下面,位于F島的核電站,受9級特大地震影響绩聘,放射性物質(zhì)發(fā)生泄漏沥割。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一凿菩、第九天 我趴在偏房一處隱蔽的房頂上張望机杜。 院中可真熱鬧,春花似錦衅谷、人聲如沸椒拗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽陡叠。三九已至玩郊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間枉阵,已是汗流浹背译红。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留兴溜,地道東北人侦厚。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像拙徽,于是被迫代替她去往敵國和親刨沦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內(nèi)容