根據對vue源碼的了解,對vue的資料響應式做一個簡單的實作。
定義myvue,使用方式仿造vue,簡單實作插值表達式、資料雙向綁定、事件及指令。
直接上代碼
建立index.html,代碼如下:
<!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></title>
</head>
<body>
<div id="app">
<!-- 插值 -->
<p>{{title}}</p>
<!-- 雙向綁定 -->
<input type="text" my-model="name" />
<!-- 指令 -->
<p my-text="name"></p>
<!-- 事件 -->
<button @click="clear">清空</button>
<!-- html -->
<div my-html="html"></div>
</div>
<script src='./myvue.js'></script>
<script src='./compile.js'></script>
<script>
const myvue = new MyVue({
el: '#app',
data: {
title: "vue響應式",
name: "_BuzzLy",
html: "<button>按鈕</button>"
},
methods: {
clear() {
this.name = "";
}
},
})
</script>
</body>
</html>
建立myvue.js,主要作用是資料的響應化,代碼如下:
class MyVue {
constructor(options) {
this.$options = options;
this.$data = options.data;
this.observe(this.$data);
// 在compile.js中實作
new Compile(options.el, this);
}
observe(data) {
if (!data || typeof data !== 'object')
return;
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
this.proxyData(key);
})
}
// 資料響應化
defineReactive(data, key, value) {
this.observe(value);
var dep = new Dep();
Object.defineProperty(data, key, {
get() {
Dep.target && dep.addDep(Dep.target);
return value;
},
set(newValue) {
if (value === newValue) return;
value = newValue;
dep.notify();
}
})
}
// 代理data中的屬性到vue執行個體上
proxyData(key) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newValue) {
this.$data[key] = newValue;
}
})
}
}
// 依賴收集器,管理Watcher
class Dep {
constructor() {
// 用來存放watcher
this.deps = [];
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach(dep => dep.update())
}
}
// Watcher 訂閱者
class Watcher {
constructor(vm, key, callback) {
this.vm = vm;
this.key = key;
this.callback = callback;
// 将目前執行個體指向Dep類的靜态屬性target
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
update() {
this.callback.call(this.vm, this.vm[this.key]);
}
}
建立compile.js,主要作用是編譯,代碼如下:
class Compile {
constructor(el, vm) {
this.$el = document.querySelector(el);
this.$vm = vm;
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el);
// 核心——編譯(處理html模闆,解析指令事件等,以及收集相關依賴)
this.compile(this.$fragment);
this.$el.appendChild(this.$fragment);
}
}
// 将dom結構轉換為fragment片段進行操作,提升性能
node2Fragment(el) {
const frag = document.createDocumentFragment();
let child;
while (child = el.firstChild) {
frag.appendChild(child);
}
return frag;
}
compile(el) {
const childNodes = el.childNodes;
// 周遊所有節點
Array.from(childNodes).forEach(node => {
// 元素節點
if (this.isElement(node)) {
const nodeAttrs = node.attributes;
// 周遊所有屬性
Array.from(nodeAttrs).forEach(attr => {
const attrName = attr.name;
const attrVal = attr.value;
// 指令
if (this.isDirective(attrName)) {
const dirName = attrName.substring(3);
this[dirName] && this[dirName](node, this.$vm, attrVal);
} else if (this.isEvent(attrName)) { // 事件
const eventName = attrName.substring(1);
this.eventHandler(node, this.$vm, attrVal, eventName);
}
})
} else if (this.isText(node)) { // 文本節點
this.compileText(node);
}
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
})
}
// 調用指令方式初始化,并收集依賴
update(node, vm, val, dir) {
const updateFn = this[dir + 'Updater'];
updateFn && updateFn(node, vm[val]);
new Watcher(vm, val, function (value) {
updateFn && updateFn(node, value);
});
}
// 處理插值表達式
compileText(node) {
this.update(node, this.$vm, RegExp.$1, "text");
}
// text指令方法
text(node, vm, attrVal) {
this.update(node, vm, attrVal, "text");
}
// text指令更新函數
textUpdater(node, value) {
console.log('set:' + value)
node.textContent = value;
}
// model指令方法
model(node, vm, attrVal) {
// 指定input的value屬性,模型對視圖的響應
this.update(node, vm, attrVal, "model");
// 視圖對模型響應
node.addEventListener('input', function (e) {
vm[attrVal] = e.target.value;
})
}
// 雙向綁定更新函數
modelUpdater(node, value) {
node.value = value;
}
// html指令方法
html(node, vm, attrVal) {
this.update(node, vm, attrVal, "html");
}
// html更新函數
htmlUpdater(node, value) {
node.innerHTML = value;
}
// 事件處理函數
eventHandler(node, vm, val, event) {
// 在vue執行個體的methods中找到對應的方法
let fn = vm.$options.methods && vm.$options.methods[val];
if (event && fn)
node.addEventListener(event, fn.bind(vm))
}
// 判斷屬性是否為指令
isDirective(attr) {
return attr.indexOf('my-') === 0;
}
// 判斷屬性是否為自定義事件
isEvent(attr) {
return attr.indexOf('@') === 0;
}
// 判斷節點為元素節點
isElement(node) {
return node.nodeType === 1;
}
// 判斷節點是文本節點并且為插值表達式
isText(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
}
代碼到這就結束了,代碼都有簡單的注釋,就不再用文字說明了,看一張圖就好。(本來想着用文字描述的,但是文字不太容易了解,索性就畫了個圖,覺得比文字容易了解)
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM38FdsYkRGZkRG9lcvx2bjxiNx8VZ6l2cs0TP35UMJpnT10kaNBDOsJGcohVYsR2MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL2UTN0MTNwgTM4ATOwkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
内容有點多,字有點小,可以儲存下來看。