使用 ES6 的 Proxy
結合觀察者模式來實現數據雙向綁定,具體實現步驟大體分爲下面四個步驟html
本文代碼用 TypeScript 寫的,第一次用 Webpack + TS 本身搭項目,很爛,能看就將就看吧。。。node
觀察數據更新,而後通知訂閱者更新視圖從而達到數據雙向綁定效果,Vue 使用了 Object.defineProperty()
這個方法來劫持了 VM 實例對象的屬性的讀寫,我這裏主要採用了 ES6
Proxy
,實例化的時候 _initSet()
把 data
的數據掛載到 this
上面, 再在把 Biub
傳給 Proxy
實例化一個對象掛在本身身上這樣後面的方法執行時直接 .call(this.proxy)
,這樣在傳進去的方法裏 this
的指向就是 Biub
自身了,接着執行了 Compile
模板解析完成了渲染。git
class Biub {
$options: options;
proxy: Biub;
deps: {
[name: string]: Dep
}
[propName: string]: any;
constructor(options: options) {
this.$options = options;
this._initSet(); // 把數據掛到 this 上面
this.deps = {};
this.proxy = this.defineProxy();
this.$compile = new Compile(options.el, this); // 解析模板
}
$watch(key: string, cb: Function) {
new Watcher(this, key, cb);
}
defineProxy() {
const deps = this.deps;
return new Proxy(this, {
get: function (target: Biub, key: string | number) {
if (target[key]) {
if (!deps[key]) {
deps[key] = new Dep();
} else {
if (Dep.target) {
deps[key].depend();
}
}
return target[key];
}
return Reflect.get(target, key);
},
set: function (target, key: any, value, receiver) {
const keys = key.split('.');
if (keys.length > 1) {
key = keys[0];
}
const dep = deps[key];
dep && dep.notify(value);
return Reflect.set(target, key, value);
}
})
}
_initSet() {
const { methods, data } = this.$options
Object.keys(data).forEach((key) => {
this[key] = data[key];
});
Object.keys(methods).forEach((key) => {
this[key] = methods[key];
});
}
_initComputed() {
// this.$options.computed && this.$options.computed.call(this.proxy);
}
}
複製代碼
Compile
主要作的事情是解析模板變量和指令,經過 compileElement
遞歸 DOM 將模板中的變量替換成數據,並對 DOM 相應的指令函數,添加訂閱者,下面代碼不全,全文請戳 compile,這裏要感謝 DMQ 大神提供 compile
subscribe
兩個模塊,我把大佬的 es5 改爲了 TypeScript 並簡化了模板編譯指令,由好幾個簡化成了兩個,哈哈哈!github
class Compile {
// 省略。。。
// 遞歸文檔節點
compileElement(el: DocumentFragment | Node) {
var childNodes = el.childNodes;
childNodes.forEach((node: any) => {
const text = node.textContent;
const reg = /\{\{(.*)\}\}/;
if (this.isElementNode(node)) {
this.compile(node); // 解析指令
} else if (this.isTextNode(node) && reg.test(text)) {
this.compileText(node, RegExp.$1.trim()); // 解析模板變量
}
if (node.childNodes && node.childNodes.length) {
this.compileElement(node);// 遞歸文檔節點
}
});
}
compile(node: any) {
const nodeAttrs = node.attributes;
for (const attr of nodeAttrs) {
const attrName = attr.name;
if (this.isDirective(attrName)) {
const exp = attr.value;
const dir = attrName.substring(2);
if (this.isEventDirective(dir)) {
// 綁定事件指令
directives.eventHandler(node, this.$vm, exp, dir);
} else {
// model指令
directives.model(node, this.$vm, exp);
}
node.removeAttribute(attrName);
}
}
}
// 省略。。。
}
複製代碼
Dep
經過 subs
數組收集訂閱者,當 proxy
發生數據變動時經過 notify()
通知訂閱更新視圖npm
let depid = 0;
class Dep {
static target: Watcher | null = null;
id: number;
subs: Array<Watcher>;
constructor() {
this.id = depid++;
this.subs = [];
}
addSub(watcher: Watcher) {
this.subs.push(watcher);
}
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
// 通知 Watcher 觸發回調
notify(vm: Biub) {
this.subs.forEach((watcher: Watcher) => {
watcher.update(vm);
});
}
};
複製代碼
Compile
模板解析到指令或者變量的時候實例化一個 Watcher
往 Biub
的 deps
對應 Dep
裏添加本身並訂閱了一個回調函數 updater()
,這樣 proxy
觸發 set
時候就能根據 deps
對應 Dep
發送 notify()
並經過 forEach
遍歷全部訂閱者觸發自身的 update()
並而觸發 Compile
中綁定的回調函數 updater()
更新視圖,這裏很差理解的應該就是 updater()
這個回調方法和添加訂閱 getter
這個方法觸發的閉包在 Biub
產生 get
完成把本身添加進 Dep
subs
操做把!數組
export default class Watcher {
updater: Function;
expOrFn: string | Function;
depIds: { [key: number]: Dep };
value: string | number | symbol;
vm: Biub;
getter: any;
constructor(vm: Biub, expOrFn: string | Function, updater: Function) {
this.updater = updater;
this.vm = vm;
this.expOrFn = expOrFn;
this.depIds = {};
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = this.parseGetter(expOrFn.trim());
}
this.value = this.get();
}
update(val: any) {
this.run(val);
}
run(val: any) {
var oldVal = this.value;
if (val !== oldVal) {
this.value = val;
// 觸發更新 compile 傳進來的
this.updater.call(this.vm, val, oldVal);
}
}
addDep(dep: Dep) {
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this);
this.depIds[dep.id] = dep;
}
}
get() {
Dep.target = this;
// 觸發 proxy get,添加訂閱
const value = this.getter.call(this.vm, this.vm);
Dep.target = null;
return value;
}
parseGetter(exp: string ) {
const exps = exp.split('.');
return function () {
var val = this.proxy;
exps.forEach((k: any) => {
val = val[k]
});
return val;
}
}
};
複製代碼
npm run dev
複製代碼
<div id="app">
<input type="text" v-model="name">
<input type="text" v-model="obj.kk.childName">
<p>名字:{{ name }}</p>
<p>Salary:{{salary}}</p>
<p>深對象:{{obj.kk.childName}}</p>
<button v-on:click="click">Add Salary</button>
</div>
複製代碼
let index = 10000;
const vm = new Biub({
el: '#app',
data: {
salary: 10000,
name: 'nancy',
obj: {
kk: {
childName: 'pony',
}
}
},
methods: {
click(e: Event) {
this.salary = index += 1000;
}
}
});
vm.$watch('salary', function () {
console.log(this.salary);
});
複製代碼