用vue框架的基本原理,簡單實現一個todo-list

前言

最近在學習vue框架的基本原理,看了一些技術博客以及一些對vue源碼的簡單實現,對數據代理、數據劫持、模板解析、變異數組方法、雙向綁定有了更深的理解。因而乎,嘗試着去實踐本身學到的知識,用vue的一些基本原理實現一個簡單的todo-list,完成對深度複雜對象的雙向綁定以及對數組的監聽,加深了對vue基本原理的印象。html

學習連接

前排感謝如下文章,對我理解vue的基本原理有很大的幫助!github

實現效果

todo-list.png

數據代理

1.簡單介紹數據代理

正常狀況下,咱們都會把數據寫在data裏面,以下面所示數組

var vm = new Vue({
    el: '#app',
    data: {
        title: 'hello world'
    }
    methods: {
        changeTitle: function () {
            this.title = 'hello vue'
        }
    }
})
console.log(vm.title) // 'hello world' or 'hello vue'

若是沒有數據代理,而咱們又要修改data裏面的title的話,methods裏面的changeTitle只能這樣修改爲this.data.title = 'hello vue', 下面的console也只能改爲console.log(vm.data.title),數據代理就是這樣的功能。緩存

2. 實現原理

經過遍歷data裏面的屬性,將每一個屬性經過object.defineProperty()設置getter和setter,將data裏面的每一個屬性都複製到與data同級的對象裏。app

(對應上面的示例代碼)框架

clipboard.png

觸發這裏的getter將會觸發data裏面對應屬性的getter,觸發這裏的setter將會觸發data裏面對應屬性的setter,從而實現代理。實現代碼以下:

var self = this;   // this爲vue實例, 即vm
Object.keys(this.data).forEach(function(key) {
    Object.defineProperty(this, key, {    // this.title, 即vm.title
        enumerable: false,
        configurable: true,
        get: function getter () {
            return self.data[key];   //觸發對應data[key]的getter
        },
        set: function setter (newVal) {
            self.data[key] = newVal;  //觸發對應data[key]的setter
        }
    });
}

對object.defineProperty不熟悉的小夥伴能夠在MDN的文檔(連接)學習一下

雙向綁定

  • 數據變更 ---> 視圖更新

  • 視圖更新(input、textarea) --> 數據變更

視圖更新 --> 數據變更這個方向的綁定比較簡單,主要經過事件監聽來改變數據,好比input能夠監聽input事件,一旦觸發input事件就改變data。下面主要來理解一下數據變更--->視圖更新這個方向的綁定。

1. 數據劫持

不妨讓咱們本身思考一下,如何實現數據變更,對應綁定數據的視圖就更新呢?

答案仍是object.defineProperty,經過object.defineProperty遍歷設置this.data裏面全部屬性,在每一個屬性的setter裏面去通知對應的回調函數,這裏的回調函數包括dom視圖從新渲染的函數、使用$watch添加的回調函數等,這樣咱們就經過object.defineProperty劫持了數據,當咱們對數據從新賦值時,如this.title = 'hello vue',就會觸發setter函數,從而觸發dom視圖從新渲染的函數,實現數據變更,對應視圖更新。

2. 發佈-訂閱模式

那麼問題來了,咱們如何在setter裏面觸發全部綁定該數據的回調函數呢?

既然綁定該數據的回調函數不止一個,咱們就把全部的回調函數放在一個數組裏面,一旦觸發該數據的setter,就遍歷數組觸發裏面全部的回調函數,咱們把這些回調函數稱爲訂閱者。數組最好就定義在setter函數的最近的上級做用域中,以下面實例代碼所示。

Object.keys(this.data).forEach(function(key) {
    var subs = [];  // 在這裏放置添加全部訂閱者的數組
    Object.defineProperty(this.data, key, {    // this.data.title
        enumerable: false,
        configurable: true,
        get: function getter () {
            console.log('訪問數據啦啦啦')
            return this.data[key];   //返回對應數據的值
        },
        set: function setter (newVal) {
            if (newVal === this.data[key]) {   
                return;    // 若是數據沒有變更,函數結束,不執行下面的代碼
            }
            this.data[key] = newVal;  //數據從新賦值
            
            subs.forEach(function () {
                // 通知subs裏面的全部的訂閱者
            })
        }
    });
}

那麼問題又來了,怎麼把綁定數據的全部回調函數放到一個數組裏面呢?

咱們能夠在getter裏面作作手腳,咱們知道只要訪問數據就會觸發對應數據的getter,那咱們能夠先設置一個全局變量target,若是咱們要在data裏面title屬性添加一個訂閱者(changeTitle函數),咱們能夠先設置target = changeTitle,把changeTitle函數緩存在target中,而後訪問this.title去觸發title的getter,在getter裏面把target這個全局變量的值添加到subs數組裏面,添加完成後再把全局變量target設置爲null,以便添加其餘訂閱者。實例代碼以下:

Object.keys(this.data).forEach(function(key) {
    var subs = [];  // 在這裏放置添加全部訂閱者的數組
    Object.defineProperty(this.data, key, {    // this.data.title
        enumerable: false,
        configurable: true,
        get: function getter () {
            console.log('訪問數據啦啦啦')
            if (target) {
                subs.push(target);                
            }
            return this.data[key];   //返回對應數據的值
        },
        set: function setter (newVal) {
            if (newVal === this.data[key]) {   
                return;    // 若是數據沒有變更,函數結束,不執行下面的代碼
            }
            this.data[key] = newVal;  //數據從新賦值
            
            subs.forEach(function () {
                // 通知subs裏面的全部的訂閱者
            })
        }
    });
}

上面的代碼爲了方便理解都是經過簡化的,實際上咱們把訂閱者寫成一個構造函數watcher,在實例化訂閱者的時候去訪問對應的數據,觸發相應的getter,詳細的代碼能夠閱讀DMQ的本身動手實現MVVM

3. 模板解析

經過上面的兩個步驟咱們已經實現一旦數據變更,就會通知對應綁定數據的訂閱者,接下來咱們來簡單介紹一個特殊的訂閱者,也就是視圖更新函數,幾乎每一個數據都會添加對應的視圖更新函數,因此咱們就來簡單瞭解一下視圖更新函數。

假如說有下面這一段代碼,咱們怎麼把它解析成對應的html呢?

<input v-model="title">
<h1>{{title}}</h1>
<button v-on:click="changeTitle">change title<button>

先簡單介紹視圖更新函數的用途,
好比解析指令v-model="title",v-on:click="changeTitle",還有把{{title}}替換爲對應的數據等。

回到上面那個問題,如何解析模板?咱們只要去遍歷全部dom節點包括其子節點,

  • 若是節點屬性含有v-model,視圖更新函數就爲把input的value設置爲title的值

  • 若是節點爲文本節點,視圖更新函數就爲先用正則表達式取出大括號裏面的值'title',再設置文本節點的值爲data['title']

  • 若是節點屬性含有v-on:xxxx,視圖更新函數就爲先用正則獲取事件類型爲click,而後獲取該屬性的值爲changeTitle,則事件的回調函數爲this.methods['changeTitle'],接着用addEventListener監聽節點click事件。

咱們要知道視圖更新函數也是data對應屬性的訂閱者,若是不知道如何觸發視圖更新函數,能夠把上面的發佈-訂閱模式再看一遍。

可能有的小夥伴可能還有個疑問,如何實現input節點的值變化後,下面的h1節點的title值也發生變化?在遍歷全部節點後,若是節點含有屬性v-model,就用addEventListener監聽input事件,一旦觸發input事件,改變data['title']的值,就會觸發title的setter,從而通知全部的訂閱者。

監聽數組變化

沒法監控每一個數組元素

若是讓咱們本身實現監聽數組的變化,咱們可能會想到用object.defineProperty去遍歷數組每一個元素並設置setter,可是vue源碼裏面卻不是這樣寫的,由於對每個數組元素defineProperty帶來代碼自己的複雜度增長和代碼執行效率的下降。

感謝Ma63d這篇文章下面的的評論,對此解釋得很詳細,這裏也就再也不贅述。

變異數組方法

既然沒法經過defineProperty監控數組的每一個元素,咱們能夠重寫數組的方法(push, pop, shift, unshift, splice, sort, reverse)來改變數組。

vue文檔中是這樣寫的:

Vue 包含一組觀察數組的變異方法,因此它們也將會觸發視圖更新。這些方法以下:

  • push()

  • pop()

  • shift()

  • unshift()

  • splice()

  • sort()

  • reverse()

下面是 vue早期源碼學習系列之二:如何監聽一個數組的變化 中的實例代碼

const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
const arrayAugmentations = [];

aryMethods.forEach((method)=> {

    // 這裏是原生Array的原型方法
    let original = Array.prototype[method];

   // 將push, pop等封裝好的方法定義在對象arrayAugmentations的屬性上
   // 注意:是屬性而非原型屬性
    arrayAugmentations[method] = function () {
        console.log('我被改變啦!');

        // 調用對應的原生方法並返回結果
        return original.apply(this, arguments);
    };

});

let list = ['a', 'b', 'c'];
// 將咱們要監聽的數組的原型指針指向上面定義的空數組對象
// 別忘了這個空數組的屬性上定義了咱們封裝好的push等方法
list.__proto__ = arrayAugmentations;
list.push('d');  // 我被改變啦! 4

// 這裏的list2沒有被從新定義原型指針,因此就正常輸出
let list2 = ['a', 'b', 'c'];
list2.push('d');  // 4

對__proto__不熟悉的小夥伴能夠去看一下王福明的博客,寫的很好。

變異數組方法的缺陷

vue文檔中變異數組方法的缺陷

因爲 JavaScript 的限制, Vue 不能檢測如下變更的數組:

  1. 當你利用索引直接設置一個項時,例如: vm.items[indexOfItem] = newValue

  2. 當你修改數組的長度時,例如: vm.items.length = newLength

同時文檔中也介紹瞭如何解決上面這兩個問題。

最後

以上是本身對vue一些基本原理的理解,固然還有不少不足的地方,歡迎指正。原本本身也是爲了應付面試纔去學習vue框架的基本原理,可是簡單學習了這些vue基本的原理後,讓我明白經過深刻學習框架原理,能夠有效避開一些本身之後會遇到的坑,因此,有時間的話本身之後仍是會去看看框架的基本原理。

相關文章
相關標籤/搜索