基於 Proxy 實現簡易版 Vue
阿新 • • 發佈:2020-06-29
分解剖析
- 實現
new Vue()
例項化 - 實現
{{ prop }}
繫結值 - 實現
v-model
雙向繫結值
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Vue</title>
</head>
<body>
<div id="app">
<input v-model="text" />
{{text}}
<span>{{text}}</span>
</div>
<script src="./vue.js"></script>
<script>
var app = new Vue({
el: "#app",data: {
text: "hello world",},});
</script>
</body >
</html>
複製程式碼
實現 class Vue
初始化
// 這裡繼承 EventTarget 提供 Vue 可以接收事件、並且可以建立偵聽器的功能
class Vue extends EventTarget {
constructor(options) {
this.options = options;
this.$el = document.querySelector(options.$el);
// 資料雙向繫結
this.data = this.observerData(options.data);
// 資料模板渲染
this .compileTemplate(this.$el);
}
}
複製程式碼
渲染模板
- 遍歷子元素,拆解文字節點,拆解文字中符合
{{**}}
特徵資料值,繫結data
中的值; - 元素節點中涵蓋
v-model
屬性的對該屬性值進行資料data
繫結;
compileTemplate(node) {
// 子節點
const children = node.childNodes;
children.forEach((it) => {
if (it.nodeType === 3) {
// text 文字節點
// 正則匹配 {{}} 特徵的繫結值
const regexp = /\{\{\s*([^\s\{\}]+)\s*\}\}/gi;
const textContent = it.textContent;
if (textContent.match(regexp)) {
const prop = RegExp.$1;
it.textContent = textContent.replace(regexp,this.data[prop]);
// 節點事件響應監聽
// 用於接收屬性 set 後的事件響應
this.addEventListener(
prop,function (event) {
it.textContent = textContent.replace(regexp,event.detail);
},false
);
}
} else if (it.nodeType === 1) {
// node 元素節點
this.compileTemplate(it);
// check v-model
const attrs = it.attributes;
if (attrs.hasOwnProperty("v-model")) {
const _this = this;
const prop = attrs["v-model"].nodeValue;
it.value = this.data[prop];
// 監聽輸入 change
it.addEventListener(
"input",function (event) {
// TODO 入口需要做XSS校驗
_this.data[prop] = event.target.value;
},false
);
}
}
});
}
複製程式碼
資料雙向繫結
// 雙向繫結
observerData(data) {
const _this = this;
return new Proxy(data,{
set: function (target,prop,newValue) {
// 建立 set 屬性事件
const event = new CustomEvent(prop,{ detail: newValue });
// 廣播該 set 屬性事件
_this.dispatchEvent(event);
return Reflect.set(...arguments);
},});
}
複製程式碼
相關物件
EventTarget
EventTarget 是一個 DOM 介面,由可以接收事件、並且可以建立偵聽器的物件實現。
- EventTarget.addEventListener(type,listener,options/useCapture):在 EventTarget 上註冊特定事件型別的事件處理程式。
- EventTarget.removeEventListener(type,options/useCapture):EventTarget 中刪除事件偵聽器。
- EventTarget.dispatchEvent(event,target):將事件分派到此 EventTarget。
Reflect
Reflect 是一個內建的物件,它提供攔截 JavaScript 操作的方法。這些方法與 proxy handlers 的方法相同。Reflect 不是一個函式物件,因此它是不可構造的。
與大多數全域性物件不同,Reflect 不是一個建構函式。你不能將其與一個 new 運算子一起使用,或者將 Reflect 物件作為一個函式來呼叫。Reflect 的所有屬性和方法都是靜態的(就像 Math 物件)。
-
Reflect.get()
: 獲取物件身上某個屬性的值,類似於 target[name]。 -
Reflect.set()
: 將值分配給屬性的函式。返回一個 Boolean,如果更新成功,則返回 true。
Proxy
Proxy 物件用於定義基本操作的自定義行為(如屬性查詢、賦值、列舉、函式呼叫等)。
-
target
:要使用 Proxy 包裝的目標物件(可以是任何型別的物件,包括原生陣列,函式,甚至另一個代理)。 -
handler
:一個通常以函式作為屬性的物件,各屬性中的函式分別定義了在執行各種操作時代理 p 的行為。-
handler.get():屬性讀取操作的捕捉器。
該方法會攔截目標物件的以下操作:
- 訪問屬性:
proxy[foo]
和proxy.bar
- 訪問原型鏈上的屬性:
Object.create(proxy)[foo]
- Reflect.get():Reflect.get()方法與從 物件 (target[propertyKey]) 中讀取屬性類似,但它是通過一個函式執行來操作的。
- 訪問屬性:
-
handler.set():屬性設定操作的捕捉器。
該方法會攔截目標物件的以下操作:
- 指定屬性值:
proxy[foo] = bar
和proxy.foo = bar
- 指定繼承者的屬性值:
Object.create(proxy)[foo] = bar
- Reflect.set():靜態方法 Reflect.set() 工作方式就像在一個物件上設定一個屬性。
- 指定屬性值:
-
handler.get():屬性讀取操作的捕捉器。
const handler = {
get: function(target,receiver){
// 攔截讀取
return Reflect.get(...arguments);
},set: function(target,newValue,receiver){
// 攔截設定
return Reflect.set(...arguments);
}
};
const p = new Proxy(target,handler);
複製程式碼
CustomEvent
CustomEvent 事件是由程式建立的,可以有任意自定義功能的事件。
-
CustomEvent.detail
: 只讀,任何時間初始化時傳入的資料
完整程式碼
class Vue extends EventTarget {
constructor(options) {
super();
this.options = options;
this.$el = document.querySelector(options.el);
this.data = this.observerData(options.data);
this.compileTemplate(this.$el);
}
// 雙向繫結
observerData(data) {
const _this = this;
return new Proxy(data,newValue) {
// 事件釋出
const event = new CustomEvent(prop,{ detail: newValue });
_this.dispatchEvent(event);
return Reflect.set(...arguments);
},});
}
// 模板編譯
compileTemplate(node) {
const children = node.childNodes;
children.forEach((it) => {
if (it.nodeType === 3) {
// text 文字節點
const regexp = /\{\{\s*([^\s\{\}]+)\s*\}\}/gi;
const textContent = it.textContent;
if (textContent.match(regexp)) {
const prop = RegExp.$1;
it.textContent = textContent.replace(regexp,this.data[prop]);
// 事件接收
this.addEventListener(
prop,function (event) {
it.textContent = textContent.replace(regexp,event.detail);
},false
);
}
} else if (it.nodeType === 1) {
// node 元素節點
this.compileTemplate(it);
// check v-model
const attrs = it.attributes;
if (attrs.hasOwnProperty("v-model")) {
const _this = this;
const prop = attrs["v-model"].nodeValue;
it.value = this.data[prop];
it.addEventListener(
"input",function (event) {
// TODO 入口需要做XSS校驗
_this.data[prop] = event.target.value;
},false
);
}
}
});
}
}
複製程式碼