通過幾天的研究,發現學習框架的底層技術,收穫頗豐,相比只學習框架的使用要來的合算;若是工做急需,快速上手應用,掌握如何使用短時間內更加高效;若是有較多的時間來系統學習,建議研究一下框架的等層技術、原理。javascript
一、Vuehtml
Vue是尤雨溪編寫的一個構建數據驅動的Web界面的庫,準確來講不是一個框架,它聚焦在V(view)視圖層。前端
它有如下的特性:vue
1.輕量級的框架java
2.雙向數據綁定node
3.指令angularjs
4.插件化api
優勢:瀏覽器
- 簡單:官方文檔很清晰,比 Angular 簡單易學。
- 快速:異步批處理方式更新 DOM。
- 組合:用解耦的、可複用的組件組合你的應用程序。
- 緊湊:~18kb min+gzip,且無依賴。
- 強大:表達式 無需聲明依賴的可推導屬性 (computed properties)。
- 對模塊友好:能夠經過 NPM、Bower 或 Duo 安裝,不強迫你全部的代碼都遵循 Angular 的各類規定,使用場景更加靈活。
缺點:前端框架
- 新生兒:Vue.js是一個新的項目,沒有angular那麼成熟。
- 影響度不是很大:google了一下,有關於Vue.js多樣性或者說豐富性少於其餘一些有名的庫。
- 不支持IE8
二、React
React 起源於 Facebook 的內部項目,用來架設 Instagram 的網站, 並於 2013年 5 月開源。React 擁有較高的性能,代碼邏輯很是簡單,愈來愈多的人已開始關注和使用它。
它有如下的特性:
1.聲明式設計:React採用聲明範式,能夠輕鬆描述應用。
2.高效:React經過對DOM的模擬,最大限度地減小與DOM的交互。
3.靈活:React能夠與已知的庫或框架很好地配合。
優勢:
- 速度快:在UI渲染過程當中,React經過在虛擬DOM中的微操做來實現對實際DOM的局部更新。
- 跨瀏覽器兼容:虛擬DOM幫助咱們解決了跨瀏覽器問題,它爲咱們提供了標準化的API,甚至在IE8中都是沒問題的。
- 模塊化:爲你程序編寫獨立的模塊化UI組件,這樣當某個或某些組件出現問題是,能夠方便地進行隔離。
- 單向數據流:Flux是一個用於在JavaScript應用中建立單向數據層的架構,它隨着React視圖庫的開發而被Facebook概念化。
- 同構、純粹的javascript:由於搜索引擎的爬蟲程序依賴的是服務端響應而不是JavaScript的執行,預渲染你的應用有助於搜索引擎優化。
- 兼容性好:好比使用RequireJS來加載和打包,而Browserify和Webpack適用於構建大型應用。它們使得那些艱難的任務再也不讓人望而生畏。
缺點:
- React自己只是一個V而已,並非一個完整的框架,因此若是是大型項目想要一套完整的框架的話,基本都須要加上ReactRouter和Flux才能寫大型應用。
三、Angular
Angular是一款優秀的前端JS框架,已經被用於Google的多款產品當中。
它有如下的特性:
1.良好的應用程序結構
2.雙向數據綁定
3.指令
4.HTML模板
5.可嵌入、注入和測試
優勢:
- 模板功能強大豐富,自帶了極其豐富的angular指令。
- 是一個比較完善的前端框架,包含服務,模板,數據雙向綁定,模塊化,路由,過濾器,依賴注入等全部功能;
- 自定義指令,自定義指令後能夠在項目中屢次使用。
- ng模塊化比較大膽的引入了Java的一些東西(依賴注入),可以很容易的寫出可複用的代碼,對於敏捷開發的團隊來講很是有幫助。
- angularjs是互聯網巨人谷歌開發,這也意味着他有一個堅實的基礎和社區支持。
缺點:
- angular 入門很容易 但深刻後概念不少, 學習中較難理解.
- 文檔例子很是少, 官方的文檔基本只寫了api, 一個例子都沒有, 不少時候具體怎麼用都是google來的, 或直接問misko,angular的做者.
- 對IE6/7 兼容不算特別好, 就是能夠用jQuery本身手寫代碼解決一些.
- 指令的應用的最佳實踐教程少, angular其實很靈活, 若是不看一些做者的使用原則,很容易寫出 四不像的代碼, 例如js中仍是像jQuery的思想有不少dom操做.
- DI 依賴注入 若是代碼壓縮須要顯示聲明.
經過以上相比較,您更加傾向於學習哪個呢?
一、創建虛擬DOM Tree,經過document.createDocumentFragment(),遍歷指定根節點內部節點,根據{{ prop }}、v-model等規則進行compile;
二、經過Object.defineProperty()進行數據變化攔截;
三、截取到的數據變化,經過發佈者-訂閱者模式,觸發Watcher,從而改變虛擬DOM中的具體數據;
四、經過改變虛擬DOM元素值,從而改變最後渲染dom樹的值,完成雙向綁定
完成數據的雙向綁定在於Object.defineProperty()
一、簡易雙綁
首先,咱們把注意力集中在這個屬性上:Object.defineProperty。
Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。
語法:Object.defineProperty(obj, prop, descriptor)
什麼叫作,定義或修改一個對象的新屬性,並返回這個對象呢?
var obj = {}; Object.defineProperty(obj,'hello',{ get:function(){ //咱們在這裏攔截到了數據 console.log("get方法被調用"); }, set:function(newValue){ //改變數據的值,攔截下來額 console.log("set方法被調用"); } }); obj.hello//輸出爲「get方法被調用」,輸出了值。 obj.hello = 'new Hello';//輸出爲set方法被調用,修改了新值
經過以上方法能夠看出,獲取對象屬性值觸發get、設置對象屬性值觸發set,所以咱們能夠想象到數據模型對象的屬性設置和讀取能夠驅動view層的數據變化,view的數據變化傳遞給數據模型對象,在set裏面能夠作不少事情。
在這基礎上,咱們能夠作到數據的雙向綁定:
let obj = {}; Object.defineProperty(obj, 'name', { set: function(newValue){ console.log('觸發setter'); document.querySelector('.text-box').innerHTML = newValue; document.querySelector('.inp-text').value = newValue; }, get: function(){ console.log('觸發getter'); } }); document.querySelector('.inp-text').addEventListener('keyup', function(e){ obj.name = e.target.value; }, false);
html
<input class="inp-text" type="text"> <div class="text-box"></div>
以上只是vue的核心思想,經過對象底層屬性的set和get進行數據攔截,vue的虛擬dom又是怎麼實現的,且看如下分解。
二、虛擬DOM樹
建立虛擬DOM:
var frag = document.createDocumentFragment();
view層的{{msg}}和v-model的編譯規則以下:
html:
<div id="container"> {{ msg }}<br> <input class="inp-text" type="text" v-model="inpText"> <div class="text-box"> <p class="show-text">{{ msg }}</p> </div> </div>
view層作了多層嵌套,這樣測試更多出現錯誤的可能性。
var container = document.getElementById('container'); //這裏咱們把vue實例中的data提取出來,更加直觀 var data = { msg: 'Hello world!', inpText: 'Input text' }; var fragment = virtualDom(container, data); container.appendChild(fragment); //虛擬dom建立方法 function virtualDom(node, data){ let frag = document.createDocumentFragment(); let child; // 遍歷dom節點 while(child = node.firstChild){ compile(child, data); frag.appendChild(child); } return frag; } //編譯規則 function compile(node, data){ let reg = /\{\{(.*)\}\}/g; if(node.nodeType === 1){ // 標籤 let attr = node.attributes; for(let i = 0, len = attr.length; i < len; i++){ // console.log(attr[i].nodeName, attr[i].nodeValue); if(attr[i].nodeName === 'v-model'){ let name = attr[i].nodeValue; node.value = data[name]; } } if(node.hasChildNodes()){ node.childNodes.forEach((item) => { compile(item, data); // 遞歸 }); } } if(node.nodeType === 3){ // 文本節點 if(reg.test(node.nodeValue)){ let name = RegExp.$1; name = name.trim(); node.nodeValue = data[name]; } } }
解釋:
一、經過virtualDom建立虛擬節點,將目標盒子內全部子節點添加到其內部,注意這裏只是子節點;
二、子節點經過compile進行編譯,a:若是節點爲元素,其nodeType = 1,b:若是節點爲文本,其nodeType = 3,具體能夠查看詳情http://www.w3school.com.cn/js...;
三、若是第二步子節點仍有子節點,經過hasChildNodes()來確認,若是有遞歸調用compile方法。
三、響應式原理
核心思想:Object.defineProperty(obj, key, {set, get})
function defineReact(obj, key, value){ Object.defineProperty(obj, key, { set: function(newValue){ console.log(`觸發setter`); value = newValue; console.log(value); }, get: function(){ console.log(`觸發getter`); return value; } }); }
這裏是針對data數據的屬性的響應式定義,可是如何去實現vue實例vm綁定data每一個屬性,經過如下方法:
function observe(obj, vm){ Object.keys(obj).forEach((key) => { defineReact(vm, key, obj[key]); }) }
vue的構造函數:
function Vue(options){ this.data = options.data; let id = options.el; observe(this.data, this); // 將每一個data屬相綁定到Vue的實例上this }
經過以上咱們能夠實現Vue實例綁定data屬性。
如何去實現Vue,一般咱們實例化Vue是這樣的:
var vm = new Vue({ el: 'container', data: { msg: 'Hello world!', inpText: 'Input text' } }); console.log(vm.msg); // Hello world! console.log(vm.inpText); // Input text
實現以上效果,咱們必須在vue內部初始化虛擬Dom
function Vue(options){ this.data = options.data; let id = options.el; observe(this.data, this); // 將每一個data屬相綁定到Vue的實例上this //------------------------添加如下代碼 let container = document.getElementById(id); let fragment = virtualDom(container, this); // 這裏經過vm對象初始化 container.appendChild(fragment); }
這是咱們再對Vue進行實例化,則能夠看到如下頁面:
至此咱們實現了dom的初始化,下一步咱們在v-model元素添加監聽事件,這樣就能夠經過view層的操做來修改vm對應的屬性值。在compile編譯的時候,能夠準確的找到v-model屬相元素,所以咱們把監聽事件添加到compile內部。
function compile(node, data){ let reg = /\{\{(.*)\}\}/g; if(node.nodeType === 1){ // 標籤 let attr = node.attributes; for(let i = 0, len = attr.length; i < len; i++){ // console.log(attr[i].nodeName, attr[i].nodeValue); if(attr[i].nodeName === 'v-model'){ let name = attr[i].nodeValue; node.value = data[name]; // ------------------------添加監聽事件 node.addEventListener('keyup', function(e){ data[name] = e.target.value; }, false); // ----------------------------------- } } if(node.hasChildNodes()){ node.childNodes.forEach((item) => { compile(item, data); }); } } if(node.nodeType === 3){ // 文本節點 if(reg.test(node.nodeValue)){ let name = RegExp.$1; name = name.trim(); node.nodeValue = data[name]; } } }
這一步咱們操做頁面輸入框,能夠看到如下效果,證實監聽事件添加有效。
到這裏咱們已經實現了MVVM的,即Model -> vm -> View || View -> vm -> Model 中間橋樑就是vm實例對象。
四、觀察者模式原理
觀察者模式也稱爲發佈者-訂閱者模式,這樣說應該會更容易理解,更加形象。
訂閱者:
var subscribe_1 = { update: function(){ console.log('This is subscribe_1'); } }; var subscribe_2 = { update: function(){ console.log('This is subscribe_2'); } }; var subscribe_3 = { update: function(){ console.log('This is subscribe_3'); } };
三個訂閱者都有update方法。
發佈者:
function Publisher(){ this.subs = [subscribe_1, subscribe_2, subscribe_3]; // 添加訂閱者 } Publisher.prototype = { constructor: Publisher, notify: function(){ this.subs.forEach(function(sub){ sub.update(); }) } };
發佈者經過notify方法對訂閱者廣播,訂閱者經過update來接受信息。
實例化publisher:
var publisher = new Publisher(); publisher.notify();
這裏咱們能夠作一箇中間件來處理髮布者-訂閱者模式:
var publisher = new Publisher(); var middleware = { publish: function(){ publisher.notify(); } }; middleware.publish();
五、觀察者模式嵌入
到這一步,咱們已經實現了:
一、修改v-model屬性元素 -> 觸發修改vm的屬性值 -> 觸發set
二、發佈者添加訂閱 -> notify分發訂閱 -> 訂閱者update數據
接下來咱們要實現:更新視圖,同時把訂閱——發佈者模式嵌入。
發佈者:
function Publisher(){ this.subs = []; // 訂閱者容器 } Publisher.prototype = { constructor: Publisher, add: function(sub){ this.subs.push(sub); // 添加訂閱者 }, notify: function(){ this.subs.forEach(function(sub){ sub.update(); // 發佈訂閱 }); } };
訂閱者:
考慮到要把訂閱者綁定data的每一個屬性,來觀察屬性的變化,參數:name參數能夠有compile中獲取的name傳參。因爲傳入的node節點類型分爲兩種,咱們能夠分爲兩訂閱者來處理,同時也能夠對node節點類型進行判斷,經過switch分別處理。
function Subscriber(node, vm, name){ this.node = node; this.vm = vm; this.name = name; } Subscriber.prototype = { constructor: Subscriber, update: function(){ let vm = this.vm; let node = this.node; let name = this.name; switch(this.node.nodeType){ case 1: node.value = vm[name]; break; case 3: node.nodeValue = vm[name]; break; default: break; } } };
咱們要把訂閱者添加到compile進行虛擬dom的初始化,替換掉原來的賦值:
function compile(node, data){ let reg = /\{\{(.*)\}\}/g; if(node.nodeType === 1){ // 標籤 let attr = node.attributes; for(let i = 0, len = attr.length; i < len; i++){ // console.log(attr[i].nodeName, attr[i].nodeValue); if(attr[i].nodeName === 'v-model'){ let name = attr[i].nodeValue; // --------------------這裏被替換掉 // node.value = data[name]; new Subscriber(node, data, name); // ------------------------添加監聽事件 node.addEventListener('keyup', function(e){ data[name] = e.target.value; }, false); } } if(node.hasChildNodes()){ node.childNodes.forEach((item) => { compile(item, data); }); } } if(node.nodeType === 3){ // 文本節點 if(reg.test(node.nodeValue)){ let name = RegExp.$1; name = name.trim(); // ---------------------這裏被替換掉 // node.nodeValue = data[name]; new Subscriber(node, data, name); } } }
既然是對虛擬dom編譯初始化,Subscriber要初始化,即Subscriber.update,所以要對Subscriber做進一步的處理:
function Subscriber(node, vm, name){ this.node = node; this.vm = vm; this.name = name; this.update(); } Subscriber.prototype = { constructor: Subscriber, update: function(){ let vm = this.vm; let node = this.node; let name = this.name; switch(this.node.nodeType){ case 1: node.value = vm[name]; break; case 3: node.nodeValue = vm[name]; break; default: break; } } };
發佈者添加到defineReact,來觀察數據的變化:
function defineReact(data, key, value){ let publisher = new Publisher(); Object.defineProperty(data, key, { set: function(newValue){ console.log(`觸發setter`); value = newValue; console.log(value); publisher.notify(); // 發佈訂閱 }, get: function(){ console.log(`觸發getter`); if(Publisher.global){ //這裏爲何來添加判斷條件,主要是讓publisher.add只執行一次,初始化虛擬dom編譯的時候來執行 publisher.add(Publisher.global); // 添加訂閱者 } return value; } }); }
這一步將訂閱者添加到發佈者容器內,對訂閱者改造:
function Subscriber(node, vm, name){ Publisher.global = this; this.node = node; this.vm = vm; this.name = name; this.update(); Publisher.global = null; } Subscriber.prototype = { constructor: Subscriber, update: function(){ let vm = this.vm; let node = this.node; let name = this.name; switch(this.node.nodeType){ case 1: node.value = vm[name]; break; case 3: node.nodeValue = vm[name]; break; default: break; } } };
六、完整效果
html:
<div id="container"> {{ msg }}<br> <input class="inp-text" type="text" v-model="inpText"> <p>{{ inpText }}</p> <div class="text-box"> <p class="show-text">{{ msg }}</p> </div> </div>
javascript:
function Publisher(){ this.subs = []; } Publisher.prototype = { constructor: Publisher, add: function(sub){ this.subs.push(sub); }, notify: function(){ this.subs.forEach(function(sub){ sub.update(); }); } }; function Subscriber(node, vm, name){ Publisher.global = this; this.node = node; this.vm = vm; this.name = name; this.update(); Publisher.global = null; // 清空 } Subscriber.prototype = { constructor: Subscriber, update: function(){ let vm = this.vm; let node = this.node; let name = this.name; switch(this.node.nodeType){ case 1: node.value = vm[name]; break; case 3: node.nodeValue = vm[name]; break; default: break; } } }; function virtualDom(node, data){ let frag = document.createDocumentFragment(); let child; // 遍歷dom節點 while(child = node.firstChild){ compile(child, data); frag.appendChild(child); } return frag; } function compile(node, data){ let reg = /\{\{(.*)\}\}/g; if(node.nodeType === 1){ // 標籤 let attr = node.attributes; for(let i = 0, len = attr.length; i < len; i++){ // console.log(attr[i].nodeName, attr[i].nodeValue); if(attr[i].nodeName === 'v-model'){ let name = attr[i].nodeValue; // node.value = data[name]; // ------------------------添加監聽事件 node.addEventListener('keyup', function(e){ data[name] = e.target.value; }, false); new Subscriber(node, data, name); } } if(node.hasChildNodes()){ node.childNodes.forEach((item) => { compile(item, data); }); } } if(node.nodeType === 3){ // 文本節點 if(reg.test(node.nodeValue)){ let name = RegExp.$1; name = name.trim(); // node.nodeValue = data[name]; new Subscriber(node, data, name); } } } function defineReact(data, key, value){ let publisher = new Publisher(); Object.defineProperty(data, key, { set: function(newValue){ console.log(`觸發setter`); value = newValue; console.log(value); publisher.notify(); // 發佈訂閱 }, get: function(){ console.log(`觸發getter`); if(Publisher.global){ publisher.add(Publisher.global); // 添加訂閱者 } return value; } }); } // 將data中數據綁定到vm實例對象上 function observe(data, vm){ Object.keys(data).forEach((key) => { defineReact(vm, key, data[key]); }) } function Vue(options){ this.data = options.data; let id = options.el; observe(this.data, this); // 將每一個data屬相綁定到Vue的實例vm上 //------------------------ let container = document.getElementById(id); let fragment = virtualDom(container, this); // 這裏經過vm對象初始化 container.appendChild(fragment); } var vm = new Vue({ el: 'container', data: { msg: 'Hello world!', inpText: 'Input text' } });
未完待續......