做者:殷榮檜@騰訊javascript
建議你先把倉庫中的代碼clone下來跑一遍,執行git checkout aec6a75 切換到倉庫的第一個commit,本身運行運行,嘗試着去看一看代碼,本身先理解理解,斷點調試調試。而後再來看這篇文章代碼中寫的註釋,遇到不太理解的再來看看這篇文章,看看是否是可以更易於理解些。代碼真的已經簡化到不能再簡化,總計才150行左右,仔細看你必定能理解。第一個commit實現了雙向綁定功能,後面每一個commit都只實現一個完整的細小的功能(如v-model、computed、watch。method等),並且代碼量都儘量的少,你若是想看必定能看懂。html
Vue最精華的部分就是雙向綁定,在此基礎上,又添加了computed,watch, methods等方法。因此要看懂Vue內核,那第一步確定就是要了解Vue雙向綁定的原理,可是說實話,看了網上不少,好多代碼都是通過重構優化後的代碼,失去了代碼原始的面貌,不太易於理解。因此決定寫一個原始一點可是又儘量簡潔一點的,可是原理絕對是Vue雙向綁定的原理,確保你看懂這篇文章,就可以瞭解Vue內核。採用最少的代碼,來實現一個個功能。有什麼寫的不妥的地方,煩請在倉庫issue中指出,我好及時修正。 這個項目的github地址爲build-your-own-vue 歡迎starvue
若是你對當前流行的輪子的原理感興趣,下面這些都是我用盡量少的代碼,儘量易於理解的方式實現的框架的原理,這些你也能夠看看,有疑問歡迎在各個倉庫下留言:java
build-your-own-vuexreact
build-your-own-fluxgithub
接下來所講的這些就爲了實現下面這個簡單的雙向綁定:正則表達式
<div id="app">
{{name}}
</div>
<script type="text/javascript">
let vue = new Vue({
el: '#app',
data: {
name: 'jackieyin'
}
})
window.vue = vue;
</script>
複製代碼
在chrome devtools控制檯中經過this.vue.name = 'willen'能夠自動更新頁面中的name爲’willen‘。看看結果:vuex
(1)從最容易的Dependency.js開始說。
先來看代碼:
let Watcher = null; // 用來代表有沒有監視器實例,這會你可能不懂,下面會遇到它,而後講解
class Dep { // 把與一個變量相關的監聽器都存在subs這個變量中
constructor() {
this.subs = []; // 定義一個subs容器
}
notify() {
// 執行全部與變量相關的回調函數,容器中的watcher一個個都執行掉(看不懂watcher不要緊,第二結中就會講解)
this.subs.forEach(sub => sub.update());
}
addSub(watcher) { // 將一個一個的watcher放入到sub的容器中(看不懂watcher不要緊,第二結中就會講解)
// 添加與變量相關的訂閱回調
this.subs.push(watcher);
}
}
複製代碼
從代碼看下來,Dep就是subs容器,是一個數組,將一個個的watcher都放到subs容器中。watcher就是一個個的回調函數,都放在subs的容器中等待觸發。addSub中的this.subs.push(watcher)就是將一個個的watcher回調函數放入到其中。notify就是用來將subs中的watcher都觸發掉。watcher中就是一個一個更新頁面中對應的變量的函數。這個下面會說到。
(2)接下來就看看這個watcher是什麼?
class Watch {
constructor(vue, exp, cb) {
this.vue = vue; // 將vue實例傳入到watcher中
this.exp = exp; // 須要對那個表達式進行監控,好比對上例中的'name'進行監控,那麼這裏的exp就是'name'
this.cb = cb; // 一但監聽到上述exp表達式子的值發生變化,須要通知到的cb(callback)回調函數
this.hasAddedAsSub = false; // 有沒有被添加到Dep中的Subscriber中去,有的話就不須要重複添加
this.value = this.get(); // 獲得當前vue實例上對應表達式exp的最新的值
}
get() {
Watcher = this; // 這邊的Watcher爲何須要放入this,並在下面又置空,你須要繼續向下看,暫且先記着,這邊把如今的watcher實例放到了Watcher中了。
var value = this.vue[this.exp]; // 獲得表達式的值,就是獲得'name'表達式的值爲‘willen’(經過chrome devtools控制檯中經過this.vue.name = 'willen'修改了name爲’willen‘。)
Watcher = null; // 將Watcher置空,讓給下一個值
return value; // 將獲取到的表達式的值返回出去
}
update() {
let value = this.get(); // 經過get()函數獲得當前的watcher監聽的表達式的值,例如上面的‘willen’
let oldVal = this.value; // 獲取舊的值
if(value !== oldVal) { // 對比新舊錶達式‘name’的值,發現修改前爲'jackieyin',修改後爲'willen',說明須要更新頁面
this.value = value; // 把如今的值記錄下來,用於和下次比較。
this.cb.call(this.vue, value); // 用如今的值willen去執行回調函數,其實就是更新一下頁面中的{{name}}從‘jackieyin’ 爲‘willen’
}
}
}
複製代碼
(3) 接下來看一下Observer,這個類是作什麼工做的。
class Observer {
constructor(data) {
this.defineReactive(data); // 將用戶自定義的data中的元素都進行劫持觀察,從而來實現雙向綁定
}
defineReactive(data) { // 開始對用戶定義的數據進行劫持
var dep = new Dep(); //這個就是第一節中說起到的Dependency類。用來收集雙向綁定的各個數據變化時都有的依賴watcher
Object.keys(data).forEach(key => { // 遍歷用戶定義的data,其實如今也就一個‘name’字段
var val = data[key]; // 獲得data['name']的值爲jackieyin
Object.defineProperty(data, key, {
get() { // 使用get對data中的name字段進行劫持
if(Watcher) { // 這個就是第二結中說起的Watcher了,(第二結中Watcher = this賦值後這邊纔會進入if)
if(!Watcher.hasAddedAsSub) { // 對於已經添加到訂閱列表中的監視器則無需再重複添加了,防止將watcher重複添加到subs容器中,沒有意義,由於一下子更新{{name}}從‘jackieyin’到‘willen’,更新兩三次也還仍是一個結果
dep.addSub(Watcher); // 將監視器watcher添加到subs訂閱列表中
Watcher.hasAddedAsSub = true; // 代表這個結果已經添加到subs容器中了
}
}
return val; // 將name中的值返回出去
},
set(newVal) { // 對this.vue.name = 'willen'這個set行爲進行劫持
if(newVal === val) { // 新值(例如仍是this.vue.name = 'jackieyin')與以前的值相同,不作任何修改
return;
}
val = newVal; // 將vue實例上對應的值(name的值)修改成新的值
dep.notify(); // 通知subs中watcher都觸發來對頁面進行更新,將頁面中的{{name}}處的‘jackieyin’更新爲'willen'
}
})
});
}
}
複製代碼
(4) 最後再一塊兒來看看編譯類Compile,這個是用來對{{name}}進行編譯,說白了就是在你的實例的data對象中,找到name: 'jackieyin',而後在頁面上將{{name}}替換爲‘jackieyin’
class Compile {
constructor(el, vue) {
this.$vue = vue; // 拷貝vue實例,之因此加$符號,表示暴露給用戶的,常常在Vue中看到這種帶$標誌的,說明是暴露給用戶使用的。
this.$el = document.querySelector(el); // 獲取到dom對象,其實就是document.querySelector('#app');
if(this.$el) { // 若是存在能夠掛在的實例
// 在$fragment中操做,比this.$el中操做節省不少性能,因此要賦值給fragment
let $fragment = this.node2Fragment(this.$el); // 將獲取到的el的地方使用片斷替代,這是爲了便於在內存中操做,使得更新頁面更加快速
this.compileText($fragment.childNodes[0]); // 將模板中的{{}}替換成對應的變量,如{{name}}替換爲'jackieyin'
this.$el.appendChild($fragment); // 將el獲取到的dom節點使用內存中的片斷進行替換
}
}
node2Fragment(el) { // 用來把dom中的節點賦值到內存fragment變量中去
// 將node節點都放到fragment中去
var fragment = document.createDocumentFragment();
fragment.appendChild(el.firstChild);// 將el中的元素放到fragment中去,並刪除el中原有的,這個是appendChild自帶的功能
return fragment;
}
compileText(node) {
// 對包含可能出現vue標識的部分進行編譯,主要是將{{xxx}}替換成對應的值,這邊是用正則表達式檢測{{}}進行替換
var reg = /\{\{(.*)\}\}/; // 用來判斷有沒有vue的雙括號的
if(reg.test(node.textContent)) {
let matchedName = RegExp.$1;
node.textContent = this.$vue[matchedName];
new Watch(this.$vue, matchedName, function(value) { // 對當前的表達式‘name’添加watcher監聽器,其實後來就是把這個watcher放入到了dep中的subs的數組中了。當'name'更新爲‘willen’後,其實就是執行了這邊的node.textContent = value就把頁面中的jackieyin替換成了willen了。這就是雙向綁定了。node其實就是剛纔存放在內存中的$fragement的節點,因此至關於直接操做了內存,因此更新頁面就比修改DOM更新頁面快多了。
node.textContent = value;
});
}
}
}
複製代碼
(5)這個時候就能夠來組裝出一個咱們本身的小型的Vue了。
class Vue {
constructor(options) {
let data = this._data = options.data || undefined;
this._initData(); // 將data中的數據都掛載到this上去,使得this.name 至關於就是獲得了this._data.name
new Observer(data); // 將data中的數據進行劫持
new Compile(options.el, this); // 將{{name}}用data中的’jackieyin‘數據替換掉
}
_initData() {
// 這個函數的功能很簡單,就是把用戶定義在data中的變量,都掛載到Vue實例(this)上
let that = this;
Object.keys(that._data).forEach((key) => {
Object.defineProperty(that, key, {
get: () => {
return that._data[key];
},
set: (newVal) => {
that._data[key] = newVal;
}
})
});
}
}
複製代碼
(6)大功告成,把咱們所寫的零件組裝在一塊兒試一下咱們的小型的vue是否工做正常。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
{{name}}
</div>
<script src="./js/Dependency.js"></script>
<script src="./js/Observer.js"></script>
<script src="./js/Watch.js"></script>
<script src="./js/Compile.js"></script>
<script src="./js/Vue.js"></script>
<script type="text/javascript">
let vue = new Vue({
el: '#app',
data: {
name: 'jackie'
}
})
window.vue = vue;
</script>
</body>
</html>
複製代碼
怎麼樣,搞定了,其實,這只是Vue的冰山一角(下圖中的綠色框框的部分),在這個倉庫中還實現了一系列vue的功能,若是你有興趣能夠一個commit一個commit的往上看,每一個commit都只實現一個完整的細小的功能,並且代碼量都儘量的少,你若是想看必定能看懂。這倉庫都是沒有使用虛擬DOM來實現,更新顆粒度細,如今的Vue下降了更新的顆粒度,用了虛擬DOM,可是Vue中雙向綁定的原理始終未變,因此這篇文章仍是須要看懂的,老弟。之後有時間我再研究研究虛擬DOM寫個倉庫。