1. Object.defineProperty,能夠看這篇文章javascript
2. 觀察者模式,能夠看個人筆記html
雙向綁定一個是視圖改變數據,這個簡單,好比input中輸入的文本綁定到數據中,那麼能夠經過監聽input事件實現vue
另外一個是數據改變視圖,這個具體怎麼實現呢?先看如下總結,帶着這些總結看代碼更容易理解java
咱們要實現Watcher Dep Observer Compile,如下是它們的介紹。node
1. Watcher:首先要知道,每一個雙向綁定的屬性(如綁定在v-model中的屬性)都會生成watcher實例,watcher包含一個更新視圖的方法(命名爲update)。git
2. Dep:觀察者系統,用於存放(訂閱)watcher,在屬性改變時(setter)觸發,會執行watcher的update方法從而更新視圖。github
3. Observer:循環data中的屬性,對於每一個屬性都生成一個觀察者系統實例dep,而後設置getter,在getter中包含一個讓dep訂閱該屬性的watcher的操做,這個操做是怎麼執行的呢?實際上是經過在生成該屬性的watcher時,讀取一下該值,那麼就會進入到getter中,從而執行訂閱操做。而後再設置setter,在setter中觸發dep,從而執行watcher中更新dom的方法update,達到屬性值改變時更新視圖的目的。segmentfault
4. Compile:這個用於解析dom,初始化視圖和爲全部雙向綁定的屬性(如v-mode {{}} )生成watcher實例,由3可知,爲屬性建立watcher時,會讀取一下該屬性,讓這個屬性的觀察者dep訂閱該watcher。bash
1. 首先實現下Vue構造函數,由此可知Vue在實例化時會作什麼。dom
function Vue (options) {
this.data = options.data(); // vue的data是一個工廠函數
let dom = document.querySelector(options.el);
observe(this.data); // 爲data 的屬性進行 Object.defineProperty 操做
new Compile(dom, this); // 解析dom,初始化視圖,爲雙向綁定的屬性生成watcher實例
}複製代碼
看看這個Vue構造函數好像有點不妥,好比我要讀取data中的一個name屬性時,我要這樣寫this.data.name,可是想一想咱們平時用vue時是否是直接this.name就能讀取到呢?因此這裏要給屬性作一下代理。
function Vue(options) {
this.data = options.data(); // vue的data是一個工廠函數
let dom = document.querySelector(options.el);
// 代理下data的屬性
for (let key of Object.keys(this.data)) {
this.proxy(key);
}
// 爲data 的屬性進行 Object.defineProperty 操做
observe(this.data);
// 解析dom,初始化視圖,爲雙向綁定的屬性生成watcher實例
new Compile(dom, this);
}
Vue.prototype.proxy = function (key){
Object.defineProperty(this, key, {
configurable: false,
enumerable: true,
get () {
return this.data[key];
},
set (newVal) {
this.data[key] = newVal;
}
});
}複製代碼
2. 由1可知,Vue實例化時會執行observe方法,上面已經介紹過observe主要是設置getter和setter,而且會用到觀察者模式,因此咱們先實現一個觀察者系統,再實現observe方法。
// 觀察者系統 用於訂閱watcher
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function (sub) {
this.subs.push(sub);
},
notify: function () {
this.subs.forEach(function (sub) {
sub.update(); // 執行watcher的更新視圖的方法。
});
}
}
function observe(data) {
if (typeof data !== 'object') return;
for (let key of Object.keys(data)) {
defineReactive(data, key, data[key]);
}
}
function defineReactive(data, key, val) {
observe(val);
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
/*
在getter中包含訂閱watcher的操做,在實例化該屬性的watcher時,
會把watcher綁定到Dep的靜態屬性target上,而後讀取一下該屬性,
從而進入getter這裏執行這個訂閱操做。
*/
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set(newval) {
val = newval;
// 觸發觀察者,從而執行watcher的update方法,更新視圖
dep.notify();
}
})
}複製代碼
3. 由2可知,在getter中有個訂閱watcher的操做,那麼咱們實現下watcher,watcher會包含一個更新視圖的方法update。
function Watcher(vm, exp, cb) {
this.cb = cb; // 一個更新視圖的方法
this.vm = vm;
this.exp = exp;
// 綁定本身到Dep.target
Dep.target = this;
// 就是此處,讀取一下本身,從而進入getter,訂閱本身(Dep.target)
this.value = this.vm[this.exp];
// 釋放Dep.target
Dep.target = null;
}
Watcher.prototype = {
update () {
let newValue = this.vm[this.exp];
let oldValue = this.value;
if (newValue !== oldValue) {
this.cb.call(this.vm, newValue, oldValue)
}
}
}複製代碼
4. 好了 watcher有了,那麼實現下Compile,初始化視圖併爲雙向綁定的屬性生成watcher。
function Compile (el, vm) {
this.el = el;
this.vm = vm;
this.compileElement(el);
}
Compile.prototype = {
compileElement (el) {
let childs = el.childNodes;
Array.from(childs).forEach(node => {
let reg = /\{\{(.*)\}\}/;
let text = node.textContent;
if (this.isElementNode(node)) // 元素節點
this.compile(node)
else if (this.isTextNode(node) && reg.test(text)) { // 文本節點
this.compileText(node, reg.exec(text)[1]);
}
if (node.childNodes && node.childNodes.length) {
this.compileElement(node);
}
})
},
compile (node) {
let nodeAttr = node.attributes;
Array.from(nodeAttr).forEach(attr => {
if (this.isDirective(attr.nodeName)) { // v-model屬性
node.value = this.vm[attr.nodeValue]; // 初始化
// 綁定input事件,達到視圖更新數據目的
node.addEventListener('input', () => {
this.vm[attr.nodeValue] = node.value;
})
new Watcher(this.vm, attr.nodeValue, val => {
node.value = val;
})
}
})
},
compileText (node, exp) {
node.textContent = this.vm[exp]; // 初始化
new Watcher(this.vm, exp, val => {
node.textContent = val;
});
},
isElementNode (node) {
return node.nodeType === 1;
},
isTextNode (node) {
return node.nodeType === 3;
},
isDirective (attr) {
return attr === 'v-model';
}
}複製代碼
大功告成,使用一下看看效果吧。
html:
<div id="demo">
<div>{{text}}</div>
<input v-model="text">
</div>
script:
new Vue({
el: '#demo',
data() {
return {
text: 'hello world'
}
}
})
複製代碼
代碼已提交到 github上