也不知道哪股風潮,鑽研源碼竟成了深刻理解的標配。我只想說一句,說的很對
core 目錄:包含了 Vue.js 的核心代碼,包括內置組件、全局 API 封裝,Vue 實例化、觀察者、虛擬 DOM、工具函數等等。這裏的代碼可謂是 Vue.js 的靈魂javascript
platform目錄:Vue.js 是一個跨平臺的 MVVM 框架,它能夠跑在 web 上,也能夠配合 weex 跑在 natvie 客戶端上。platform 是 Vue.js 的入口,2 個目錄表明 2 個主要入口,分別打包成運行在 web 上和 weex 上的 Vue.js。好比如今比較火熱的mpvue框架其實就是在這個目錄下面多了一個小程序的運行平臺相關內容。html
在vue的生命週期上第一個就是 new Vue() 建立一個vue實例出來,對應到源碼在\vue-dev\src\core\instance\index.jsvue
import { initMixin } from './init' import { stateMixin } from './state' import { renderMixin } from './render' import { eventsMixin } from './events' import { lifecycleMixin } from './lifecycle' import { warn } from '../util/index' function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) export default Vue
能夠經過index.js中的代碼看到,其實就是一個function,在es5中實現class的方式,在function vue中還加入了if判斷,表示vue必須經過new關鍵字進行實例化。這裏有個疑問就是爲何vue中沒有使用es6的方式進行定義?經過看下面的方法能夠獲得解答。java
function vue下定義了許多Mixin這種方法,而且把vue類看成參數傳遞進去,下面來進入initMixin(Vue)下,來自import { initMixin } from './init',選取了部分代碼以下node
export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) }
能夠看到initMixin方法就是往vue的原型上掛載了一個_init方法,其餘的Mixin也是同理,都是往vue的原型上掛載各類方法,而最開始建立vue類時經過es5 function的方式建立也是爲了後面能夠更加靈活操做,能夠將方法寫入到各個js文件,不用一次寫在一個下面,更加方便代碼後期的維護,這個也是選擇es5建立的緣由。git
當調用new Vue的時候,事實上就調用的Vue原型上的_init方法.es6
vue 初始化主要就幹了幾件事情,合併配置,初始化生命週期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等github
從index.js入口分析後,越往裏發現各個文件之間的引用理不亂剪還亂,因而乎從原來的看源碼變成模仿着寫雛形,這種方式可能會理解的更加深入一些,和你們共勉。web
vue中的雙向數據是經過數據劫持(Object.defineProperty())結合發佈者-訂閱者模式來實現的,Object.defineProperty()方法會直接在一個對象上定義一個新屬性,或者修改一個已經存在的屬性, 並返回這個對象。小程序
Object.defineProperty()使用
作個小案例,定義一個svue.js文件,定義一個book對象,並賦值輸出
var Book = { name: 'vue權威指南' }; console.log(Book.name); // vue權威指南
獲得的結果就是「vue權威指南」,若是想要在執行console.log(book.name)的同時,直接給書名加個書名號怎麼作?能夠使用Object.defineProperty()來完成,修改後的代碼
var Book = {} var name = ''; Object.defineProperty(Book, 'name', { set: function (value) { name = value; console.log('你取了一個書名叫作' + value); }, get: function () { return '《' + name + '》' } }) Book.name = 'vue權威指南'; // 你取了一個書名叫作vue權威指南 console.log(Book.name); // 《vue權威指南》
經過Object.defineProperty()對Book對象的name屬性的get和set進行了重寫操做,當訪問name屬性時會觸發get執行。
實現mvvm主要包含兩個方面,數據變化更新視圖,視圖變化更新數據。分爲3個步驟來作:
(1) 實現一個監聽器Observer,用來劫持並監聽全部屬性,若是有變更的,就通知訂閱者
Observer是一個數據監聽器,其實現核心方法就是前文所說的Object.defineProperty( )。若是要對全部屬性都進行監聽的話,那麼能夠經過遞歸方法遍歷全部屬性值,並對其進行Object.defineProperty( )處理。
對應到源碼的目錄是:/vue-dev/src/core/observer/index.js
function defineReactive(data, key, val) { observe(val); // 遞歸遍歷全部子屬性 Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { return val; }, set: function(newVal) { val = newVal; console.log('屬性' + key + '已經被監聽了,如今值爲:「' + newVal.toString() + '」'); } }); } function observe(data) { if (!data || typeof data !== 'object') { return; } Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); }); }; var library = { book1: { name: '' }, book2: '' }; observe(library); library.book1.name = 'vue權威指南'; // 屬性name已經被監聽了,如今值爲:「vue權威指南」 library.book2 = '沒有此書籍'; // 屬性book2已經被監聽了,如今值爲:「沒有此書籍」
由於訂閱者是有不少個,因此咱們須要有一個消息訂閱器Dep來專門收集這些訂閱者,而後在監聽器Observer和訂閱者Watcher之間進行統一管理的,因此要修改下代碼
function defineReactive(data, key, val) { observe(val); // 遞歸遍歷全部子屬性 var dep = new Dep(); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { if (Dep.target) { //是否須要添加訂閱者(Dep.target後類中加入) dep.addSub(Dep.target); // 在這裏添加一個訂閱者 } return val; }, set: function(newVal) { if (val === newVal) { return; } val = newVal; console.log('屬性' + key + '已經被監聽了,如今值爲:「' + newVal.toString() + '」'); dep.notify(); // 若是數據變化,通知全部訂閱者 } }); } function observe(data) { if (!data || typeof data !== 'object') { return; } Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); }); }; function Dep () { this.subs = []; //訂閱者的list } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach(function(sub) { sub.update(); }); } };
在setter函數裏面,若是數據變化,就會去通知全部訂閱者,訂閱者們就會去執行對應的更新的函數
(2) 實現一個訂閱者Watcher,能夠收到屬性的變化通知並執行相應的函數,從而更新視圖
function Watcher(vm, exp, cb) { this.cb = cb; this.vm = vm; this.exp = exp; this.value = this.get(); // 將本身添加到訂閱器的操做 } Watcher.prototype = { update: function() { this.run(); }, run: function() { var value = this.vm.data[this.exp]; var oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); } }, get: function() { Dep.target = this; // 緩存本身 var value = this.vm.data[this.exp] // 強制執行監聽器裏的get函數 Dep.target = null; // 釋放本身 return value; } };
簡單版的Watcher設計完畢,這時候只要將Observer和Watcher關聯起來,就能夠實現一個簡單的雙向綁定數據了,定義index.js
function SelfVue (data, el, exp) { this.data = data; //傳遞進來的{}對象數據 observe(data);//監聽 el.innerHTML = this.data[exp]; // 初始化模板數據的值 this.data[name] //訂閱者 function更新會在watcher.js中回調 new Watcher(this, exp, function (value) { el.innerHTML = value; }); return this; }
定義index.html引入以上3個js文件進行測試
<!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>Document</title> </head> <body> <h1 id="name">{{name}}</h1> </body> <script src="observer.js"></script> <script src="watcher.js"></script> <script src="index.js"></script> <script type="text/javascript"> var ele = document.querySelector('#name'); var selfVue = new SelfVue({ name: 'hello world' }, ele, 'name'); window.setTimeout(function () { console.log('name值改變了'); selfVue.data.name = '66666666'; }, 2000); </script> </html>
(3) 實現一個解析器Compile,能夠掃描和解析每一個節點的相關指令,並根據初始化模板數據以及初始化相應的訂閱器
雖然上面已經實現了一個雙向數據綁定的例子,可是整個過程都沒有去解析dom節點,而是直接固定某個節點進行替換數據的,因此接下來須要實現一個解析器Compile來作解析和綁定工做
function Compile(el, vm) { this.vm = vm; this.el = document.querySelector(el); this.fragment = null; this.init(); } Compile.prototype = { init: function () { if (this.el) { this.fragment = this.nodeToFragment(this.el); this.compileElement(this.fragment); this.el.appendChild(this.fragment); } else { console.log('Dom元素不存在'); } }, nodeToFragment: function (el) { var fragment = document.createDocumentFragment(); var child = el.firstChild; while (child) { // 將Dom元素移入fragment中 fragment.appendChild(child); child = el.firstChild } return fragment; }, compileElement: function (el) { var childNodes = el.childNodes; var self = this; [].slice.call(childNodes).forEach(function(node) { var reg = /\{\{(.*)\}\}/; var text = node.textContent; if (self.isTextNode(node) && reg.test(text)) { // 判斷是不是符合這種形式{{}}的指令 self.compileText(node, reg.exec(text)[1]); } if (node.childNodes && node.childNodes.length) { self.compileElement(node); // 繼續遞歸遍歷子節點 } }); }, compileText: function(node, exp) { var self = this; var initText = this.vm[exp]; this.updateText(node, initText); // 將初始化的數據初始化到視圖中 new Watcher(this.vm, exp, function (value) { // 生成訂閱器並綁定更新函數 self.updateText(node, value); }); }, updateText: function (node, value) { node.textContent = typeof value == 'undefined' ? '' : value; }, isTextNode: function(node) { return node.nodeType == 3; } }
修改index.js
function SelfVue (options) { var self = this; this.vm = this; this.data = options.data; Object.keys(this.data).forEach(function(key) { self.proxyKeys(key); }); observe(this.data); new Compile(options.el, this.vm); return this; } SelfVue.prototype = { proxyKeys: function (key) { var self = this; Object.defineProperty(this, key, { enumerable: false, configurable: true, get: function proxyGetter() { return self.data[key]; }, set: function proxySetter(newVal) { self.data[key] = newVal; } }); } }
修改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>Document</title> </head> <body> <div id="app"> <h2>{{title}}</h2> <h1>{{name}}</h1> </div> </body> <script src="observer.js"></script> <script src="watcher.js"></script> <script src="compile.js"></script> <script src="index.js"></script> <script type="text/javascript"> var selfVue = new SelfVue({ el: '#app', data: { title: 'hello world', name: '' } }); window.setTimeout(function () { selfVue.title = '你好'; }, 2000); window.setTimeout(function () { selfVue.name = 'canfoo'; }, 2500); </script> </html>
如今已經能夠解析出{{}}的內容,若是想要支持更多的指令,繼續完善compile.js
function Compile(el, vm) { this.vm = vm; this.el = document.querySelector(el); this.fragment = null; this.init(); } Compile.prototype = { init: function () { if (this.el) { this.fragment = this.nodeToFragment(this.el); this.compileElement(this.fragment); this.el.appendChild(this.fragment); } else { console.log('Dom元素不存在'); } }, nodeToFragment: function (el) { var fragment = document.createDocumentFragment(); var child = el.firstChild; while (child) { // 將Dom元素移入fragment中 fragment.appendChild(child); child = el.firstChild } return fragment; }, compileElement: function (el) { var childNodes = el.childNodes; var self = this; [].slice.call(childNodes).forEach(function(node) { var reg = /\{\{(.*)\}\}/; var text = node.textContent; if (self.isElementNode(node)) { self.compile(node); } else if (self.isTextNode(node) && reg.test(text)) { self.compileText(node, reg.exec(text)[1]); } if (node.childNodes && node.childNodes.length) { self.compileElement(node); } }); }, compile: function(node) { var nodeAttrs = node.attributes; var self = this; Array.prototype.forEach.call(nodeAttrs, function(attr) { var attrName = attr.name; if (self.isDirective(attrName)) { var exp = attr.value; var dir = attrName.substring(2); if (self.isEventDirective(dir)) { // 事件指令 self.compileEvent(node, self.vm, exp, dir); } else { // v-model 指令 self.compileModel(node, self.vm, exp, dir); } node.removeAttribute(attrName); } }); }, compileText: function(node, exp) { var self = this; var initText = this.vm[exp]; this.updateText(node, initText); new Watcher(this.vm, exp, function (value) { self.updateText(node, value); }); }, compileEvent: function (node, vm, exp, dir) { var eventType = dir.split(':')[1]; var cb = vm.methods && vm.methods[exp]; if (eventType && cb) { node.addEventListener(eventType, cb.bind(vm), false); } }, compileModel: function (node, vm, exp, dir) { var self = this; var val = this.vm[exp]; this.modelUpdater(node, val); new Watcher(this.vm, exp, function (value) { self.modelUpdater(node, value); }); node.addEventListener('input', function(e) { var newValue = e.target.value; if (val === newValue) { return; } self.vm[exp] = newValue; val = newValue; }); }, updateText: function (node, value) { node.textContent = typeof value == 'undefined' ? '' : value; }, modelUpdater: function(node, value, oldValue) { node.value = typeof value == 'undefined' ? '' : value; }, isDirective: function(attr) { return attr.indexOf('v-') == 0; }, isEventDirective: function(dir) { return dir.indexOf('on:') === 0; }, isElementNode: function (node) { return node.nodeType == 1; }, isTextNode: function(node) { return node.nodeType == 3; } }
修改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>Document</title> </head> <body> <div id="app"> <h2>{{title}}</h2> <input v-model="name"> <h1>{{name}}</h1> </div> </body> <script src="observer.js"></script> <script src="watcher.js"></script> <script src="compile.js"></script> <script src="index.js"></script> <script type="text/javascript"> var selfVue = new SelfVue({ el: '#app', data: { title: 'hello world', name: '' } }); </script> </html>
就能看到v-model的效果了