實現一個簡易的vue的mvvm(defineProperty)

 

 

這是一個最近一年很火的面試題,不少人看到這個題目從下手,其實查閱一些資料後,簡單的模擬仍是不太難的:html

vue不兼容IE8如下是由於他的實現原理使用了 Object.defineProperty 的get和set方法,首先簡單介紹如下這個方法vue

 

 

咱們看到控制檯打印出了這個對象的 key 和 value:node

 這時候,咱們刪除這個 name :git

        let obj = {};
       Object.defineProperty( obj, 'name', {
           value: 'langkui'
       })
       delete obj.name;
       console.log(obj)    

  查看控制檯,其實並無刪除:github

 

添加 configurable屬性:面試

let obj = {};
Object.defineProperty( obj, 'name', {
    configurable: true,
    value: 'langkui'
})
delete obj.name;
console.log(obj)

 

咱們發現 name 被刪除了: 數組

此時,註釋掉刪除 name 的代碼,繼續添加修改 name 屬性的值app

let obj = {};
Object.defineProperty( obj, 'name', {
    configurable: true,
    value: 'langkui'
})
// delete obj.name;
obj.name = 'xiaoming';
console.log(obj)

打開控制檯,咱們發現 name 的值並無被修改mvvm

咱們添加writable: true 的屬性:函數

let obj = {};
Object.defineProperty( obj, 'name', {
    configurable: true,
    writable: true,
    value: 'langkui'
})
// delete obj.name;
obj.name = 'xiaoming';
console.log(obj)

此時obj.name的值被修改了,

 

咱們試着循環obj: 

let obj = {};
Object.defineProperty( obj, 'name', {
    configurable: true,
    writable: true,
    value: 'langkui'
})
// delete obj.name;
// obj.name = 'xiaoming';
for(let key in obj) {
    console.log(obj[key])
}
console.log(obj)

可是控制檯什麼也沒有輸出;

添加 enumerable: true 屬性後, 控制檯顯示執行了循環

let obj = {};
Object.defineProperty( obj, 'name', {
    configurable: true,
    writable: true,
    enumerable: true,
    value: 'langkui'
})
// delete obj.name;
// obj.name = 'xiaoming';
for(let key in obj) {
    console.log(obj[key])
}
console.log(obj)

 

咱們還能夠給Object.defineProperty 添加 get 和 set 的方法:

let obj = {};
Object.defineProperty( obj, 'name', {
    configurable: true,
    // writable: true,
    enumerable: true,
    get() {
        console.log('正在獲取name的值')
        return 'langming'
    },
    set(newVal) {
        console.log(`正在設置name的值爲${newVal}`)
    }
})
// delete obj.name;
// obj.name = 'xiaoming';
for(let key in obj) {
    console.log(obj[key])
}
console.log(obj)

而後咱們試着在控制檯改變 name 的值爲100

 

 這些就是Object.defineProperty一些經常使用設置。

 

接下來咱們用它來實現一個簡單的mvvm:

 

有以下一個簡單的看似很像vue的東西:

<!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">
        {{a}}
    </div>
    <script src="1.js"></script>
    <script>
    // 數據劫持 Observe
       let vue = new Vue({
           el: 'app',
           data: {
               a: 1,
           }
       });
    </script>
</body>
</html>

首先咱們建立一個Vue的構造函數,並把_data和$options做爲他的屬性,同時咱們但願有個一observe的函數來監聽_data的變化,在_data發生變化的時候咱們修改Vue構造函數上添加一個對應相同key的屬性的值而且同時監聽這個新的key的值的變化:

function Vue( options = {} ) {
    this.$options = options;
    // this._data;
    var data = this._data = this.$options.data;
    // 監聽 data 的變化
    observe(data);
    // 實現代理  this.a 代理到 this._data.a
    for(let name in data) {
        Object.defineProperty( this, name, {
            enumerable: true,
            get() {
                // this.a 獲取的時候返回 this._data.a
                return this._data[name];   
            },
            set(newVal) {
                // 設置 this.a 的時候至關於設置  this._data.a
                this._data[name] = newVal;
            }
        })
    }
}

function Observe(data) {
    for(let key in data) {
        let val = data[key];
        observe(val)
        Object.defineProperty(data, key, {
            enumerable: true,
            get() {
                return val;
            },
            set(newVal) {
                if(newVal === val) {
                    return;
                }
                // 設置值的時候觸發
                val = newVal;
                // 實現賦值後的對象監測功能
                observe(newVal);
            }
        })
    }
}

// 觀察數據,給data中的數據object.defineProperty
function observe(data) {
    if(typeof data !== 'object') {
        return;
    }
    return new Observe(data);
}

咱們在控制檯查看vue 而且 修改 vue.a 的值爲100 並再次查看 vue:

接下來咱們經過正則匹配頁面上的{{}} 而且獲取 {{}} 裏面的變量 並把 vue上對應的key 替換進去 :

function Vue( options = {} ) {
    this.$options = options;
    // this._data;
    var data = this._data = this.$options.data;
    // 監聽 data 的變化
    observe(data);
    // 實現代理  this.a 代理到 this._data.a
    for(let name in data) {
        Object.defineProperty( this, name, {
            enumerable: true,
            get() {
                // this.a 獲取的時候返回 this._data.a
                return this._data[name];   
            },
            set(newVal) {
                // 設置 this.a 的時候至關於設置  this._data.a
                this._data[name] = newVal;
            }
        })
    }
    // 實現魔板編譯  
    new Compile(this.$options.el, this)
}

// el:當前Vue實例掛載的元素, vm:當前Vue實例上data,已代理到 this._data
function Compile(el, vm) {
    // $el  表示替換的範圍
    vm.$el = document.querySelector(el);
    let fragment = document.createDocumentFragment();
    // 將 $el 中的內容移到內存中去
    while( child = vm.$el.firstChild ) {
        fragment.appendChild(child);
    }
    replace(fragment);
    // 替換{{}}中的內容 
    function replace(fragment) {
        Array.from(fragment.childNodes).forEach( function (node) {
            let text = node.textContent;
            let reg = /\{\{(.*)\}\}/;
            // 當前節點是文本節點而且經過{{}}的正則匹配
            if(node.nodeType === 3 && reg.test(text)) {
                console.log(RegExp.$1);  // a.a b
                let arr = RegExp.$1.split('.');   // [a,a] [b]
                let val = vm;
                arr.forEach( function(k) {
                    // 循環層級
                    val = val[k];
                })
                // 賦值
                node.textContent = text.replace(reg, val);
            }
            vm.$el.appendChild(fragment)
            // 若是當前節點還有子節點,進行遞歸操做
            if(node.childNodes) {
                replace(node);
            }
        })
    }
}
 
function Observe(data) {
    for(let key in data) {
        let val = data[key];
        observe(val)
        Object.defineProperty(data, key, {
            enumerable: true,
            get() {
                return val;
            },
            set(newVal) {
                if(newVal === val) {
                    return;
                }
                // 設置值的時候觸發
                val = newVal;
                // 實現賦值後的對象監測功能
                observe(newVal);
            }
        })
    }
}

// 觀察數據,給data中的數據object.defineProperty
function observe(data) {
    if(typeof data !== 'object') {
        return;
    }
    return new Observe(data);
}

這時咱們剩下要作的就是在data改變的時候進行一次頁面更新, 此時須要提一下訂閱發佈模式:

訂閱模式其實就是就是一個隊列,咱們把須要執行的函數推動一個數組,在須要用的時候依次去執行這個數組中方法:

// 發佈訂閱模式   先訂閱 再有發佈   一個數組的隊列  [fn1, fn2, fn3]

// 約定綁定的每個方法,都有一個update屬性
function Dep() {
    this.subs = [];
}
Dep.prototype.addSub = function (sub) {
    this.subs.push(sub);
}

Dep.prototype.notify = function () {
    this.subs.forEach( sub => sub.update());
}

// Watch是一個類,經過這個類建立的實例都有update的方法ßß
function Watcher (fn) {
    this.fn = fn
}
Watcher.prototype.update = function() {
    this.fn();
}

let watcher = new Watcher( function () {
    console.log('開始了發佈');
})

let dep = new Dep();
dep.addSub(watcher);
dep.addSub(watcher);
console.log(dep.subs);
dep.notify();     //  訂閱發佈模式其實就是一個數組關係,訂閱就是講函數push到數組隊列,發佈就是以此的執行這些函數 

執行這個文件:

這個就是簡單的訂閱發佈模式,咱們把這個應用到們的mvvm中,在數據改變的時候進行實時的更新頁面操做:

function Vue( options = {} ) {
    this.$options = options;
    // this._data;
    var data = this._data = this.$options.data;
    // 監聽 data 的變化
    observe(data);
    // 實現代理  this.a 代理到 this._data.a
    for(let name in data) {
        Object.defineProperty( this, name, {
            enumerable: true,
            get() {
                // this.a 獲取的時候返回 this._data.a
                return this._data[name];   
            },
            set(newVal) {
                // 設置 this.a 的時候至關於設置  this._data.a
                this._data[name] = newVal;
            }
        })
    }
    // 實現魔板編譯  
    new Compile(this.$options.el, this)
}

// el:當前Vue實例掛載的元素, vm:當前Vue實例上data,已代理到 this._data
function Compile(el, vm) {
    // $el  表示替換的範圍
    vm.$el = document.querySelector(el);
    let fragment = document.createDocumentFragment();
    // 將 $el 中的內容移到內存中去
    while( child = vm.$el.firstChild ) {
        fragment.appendChild(child);
    }
    replace(fragment);
    // 替換{{}}中的內容 
    function replace(fragment) {
        Array.from(fragment.childNodes).forEach( function (node) {
            let text = node.textContent;
            let reg = /\{\{(.*)\}\}/;
            // 當前節點是文本節點而且經過{{}}的正則匹配
            if(node.nodeType === 3 && reg.test(text)) {
                // RegExp $1-$9 表示 最後使用的9個正則
                console.log(RegExp.$1);  // a.a b
                let arr = RegExp.$1.split('.');   // [a,a] [b]
                let val = vm;
                arr.forEach( function(k) {
                    // 循環層級
                    val = val[k];
                })
                // 賦值
                new Watcher( vm, RegExp.$1, function(newVal) {
                    node.textContent = text.replace(reg, newVal);
                })
                node.textContent = text.replace(reg, val);
            }
            vm.$el.appendChild(fragment)
            // 若是當前節點還有子節點,進行遞歸操做
            if(node.childNodes) {
                replace(node);
            }
        })
    }
}
 
function Observe(data) {
    // 開啓訂閱發佈模式
    let dep = new Dep();
    for(let key in data) {
        let val = data[key];
        observe(val)
        Object.defineProperty(data, key, {
            enumerable: true,
            get() {
                Dep.target && dep.addSub(Dep.target);
                return val;
            },
            set(newVal) {
                if(newVal === val) {
                    return;
                }
                // 設置值的時候觸發
                val = newVal;
                // 實現賦值後的對象監測功能
                observe(newVal);
                // 讓全部的watch的update方法都執行
                dep.notify();
            }
        })
    }
}

// 觀察數據,給data中的數據object.defineProperty
function observe(data) {
    if(typeof data !== 'object') {
        return;
    }
    return new Observe(data);
}

// 發佈訂閱模式
function Dep() {
    this.subs = [];
}
Dep.prototype.addSub = function (sub) {
    this.subs.push(sub);
}

Dep.prototype.notify = function () {
    this.subs.forEach( sub => sub.update());
}

// watcher
function Watcher (vm, exp, fn) {
    this.vm = vm;
    this.exp = exp;
    this.fn = fn
    // 將watch添加到訂閱中
    Dep.target = this;
    let val = vm;
    let arr = exp.split('.');
    arr.forEach(function (k) {   // 取值,也就是取 this.a.a/this.b 此時會調用 Object.defineProperty的get的方法
        val = val[k];
    });
    Dep.target = null;
}
Watcher.prototype.update = function() {
    let val = this.vm;
    let arr = this.exp.split('.');
    arr.forEach( function (k) {
        val = val[k];
    })
    // 須要傳入newVal
    this.fn(val);
}

在控制檯修改數據頁面出現了更新:

一個簡單的mvvm就實現了。

 

源碼已經放到了個人github: https://github.com/Jasonwang911/vueMVVM   若是對你有幫助,能夠star~~

相關文章
相關標籤/搜索