實現一個簡易的vue的mvvm(defineProperty)
這是一個最近一年很火的面試題,很多人看到這個題目從下手,其實查閱一些資料後,簡單的模擬還是不太難的:
vue不相容IE8以下是因為他的實現原理使用了 Object.defineProperty 的get和set方法,首先簡單介紹以下這個方法
我們看到控制檯打印出了這個物件的 key 和 value:
這時候,我們刪除這個 name :
1 2 3 4 5 6 |
|
檢視控制檯,其實並沒有刪除:
新增 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~~