這是一個最近一年很火的面試題,不少人看到這個題目從下手,其實查閱一些資料後,簡單的模擬仍是不太難的: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~~