1. 程式人生 > >實現一個簡易的vue的mvvm(defineProperty)

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

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

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

我們看到控制檯打印出了這個物件的 key 和 value:

 這時候,我們刪除這個 name :

1

2

3

4

5

6

let obj = {};

Object.defineProperty( obj, 'name', {

value: 'langkui'

})

delete obj.name;

console.log(obj)   

  檢視控制檯,其實並沒有刪除:

新增 configurable屬性:

複製程式碼

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

複製程式碼

我們發現 name 被刪除了: 

此時,註釋掉刪除 name 的程式碼,繼續新增修改 name 屬性的值

複製程式碼

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

複製程式碼

開啟控制檯,我們發現 name 的值並沒有被修改

我們新增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~~