簡單說就是在數據和UI之間創建雙向的通訊通道,當用戶經過Function改變了數據,那麼這個改變也會當即反映到UI上;或者說用戶經過UI的操做也會隨之引發對應的數據變更。
Vue是如何實現雙向數據綁定的?數據劫持?什麼意思呢?太籠統了。與其說是數據劫持,更應該說是對象數據對象的setter和Getter實現劫持。可是Object.defineProperty僅僅是實現了對數據的監控,後續實現對UI的從新渲染並非它作的,因此這裏還涉及到發佈-訂閱模式;過程是,當監控的數據對象被更改後,這個變動會被廣播給全部訂閱該數據的watcher,而後由該watcher實現對頁面的從新渲染。
步驟:html
首先要對數據進行劫持,因此咱們須要設置一個監聽器Observer,用來監聽全部的屬性。 若是屬性發生變化,就須要告訴訂閱者Watcher看是否須要更新。 訂閱者是有不少個,因此咱們須要有一個消息訂閱器Dep來專門收集這些訂閱者,而後在監聽器Observer和訂閱者Watcher之間進行統一管理的。 還須要一個指令解析器Compile,對每一個節點元素進行掃描和解析,將相關指令對應初始化成一個訂閱者Watcher,並替換模板數據或者綁定相應的函數,此時當訂閱者Watcher接收到相應屬性的變化,就會執行相應的更新函數,從而更新視圖。 1.實現一個監聽器Observer,用來劫持並監聽全部屬性,若是有變更的,就通知訂閱者。 2.實現一個訂閱者Watcher,能夠收到屬性的變化通知並執行相應的函數,從而更新視圖。 3.實現一個解析器Compile,能夠掃描和解析每一個節點的相關指令,並根據初始化模板數據及初始化相應的訂閱器。
流程圖以下:
node
Observer是一個數據監聽器,其實現核心方法就是Object.defineProperty().若是要對全部屬性都進行監聽的話,那麼能夠經過遞歸方法遍歷全部屬性值,並對其進行Object.defineProperty()處理。緩存
function defineReactive(data,key.val){ observe(val);//遞歸遍歷全部子屬性 Obejct.defineProperty(data,key,{ enumerable:true, configurable:true, get:function(){ return val; }, set:function(newVal){ val = newVal; console.log('屬性'+key+'已經被監聽了') } }) } 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='123'; library.book2='456'
思路分析中,須要建立一個能夠容納訂閱者的消息訂閱器Dep,訂閱器Dep主要負責收集訂閱者,而後再屬性變化的時候執行對應訂閱者的更新函數。因此顯然訂閱者須要有一個容器,這個容器就是list,將上面的Observer稍微改造下,植入消息訂閱者:app
function defineReactive(data,key,val){ observe(val)//遞歸遍歷全部子屬性 var dep = new Dep(); Object.defineProperty(data,key,{ enumerable:true, configurable:true, get:function(){ if(是否須要添加訂閱者){ dep.addSub(watcher);//在這裏添加一個訂閱者 } return val; }, set:function(newVal){ if(val === newVal){ return; } val = newVal; console.log('屬性'+key+'已經被監聽了'); dep.notify();//若是數據變化,通知訂閱者 } }) } function Dep(){ this.subs = []; } Dep.prototype={ addSub:function(sub){ this.subs.push(sub); }, notify:function(){ this.subs.forEach(function(sub){ sub.update(); }) } }
從代碼上看,咱們將訂閱器Dep添加一個訂閱者設計在getter裏面,這是爲了讓Watcher初始化進行觸發,所以須要判斷是否須要添加訂閱者。在setter函數裏面,若是數據變化,就會去通知全部訂閱者,訂閱者們就會去執行對應的更新函數。到此,一個比較完整的Obsever已經實現了,接下來咱們開始設計Watcher。dom
訂閱者Watcher在初始化的時候須要將本身添加進訂閱器Dep中,那該如何添加呢?咱們已經知道監聽器Observer是在get函數執行了添加訂閱者Watcher的操做的,因此咱們只要在訂閱者Watcher初始化的時候觸發對應的get函數去執行添加訂閱者操做便可,那要如何觸發get的函數呢?只要獲取對應的屬性值就能夠觸發了,緣由就是咱們使用了Obejct.defineProperty()進行數據監聽。這裏還有一個細節點須要處理,咱們只要在訂閱者Watcher初始化的時候才須要添加訂閱者,因此須要作一個判斷操做,所以能夠在訂閱器上作一下手腳:在Dep.target上緩存下訂閱者,添加成功後再將其去掉就能夠了。訂閱者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關聯起來,就能夠實現一個簡單的雙向數據綁定了。
簡單的例子:性能
<!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> function observe(data) { if (!data || typeof data !== 'object') { return; } Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); }); }; function Dep () { this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach(function(sub) { sub.update(); }); } }; function Watcher(vm, exp, cb) { this.cb = cb; this.vm = vm; this.exp = exp; this.value = this.get(); // 將本身添加到訂閱器的操做 } Watcher.prototype = { update: 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; } }; function defineReactive(data, key, val) { observe(val); // 遞歸遍歷全部子屬性 var dep = new Dep(); console.log(dep) Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { if (Dep.target) { // 判斷是否須要添加訂閱者 dep.addSub(Dep.target); // 在這裏添加一個訂閱者 } return val; }, set: function(newVal) { if (val === newVal) { return; } val = newVal; console.log('屬性' + key + '已經被監聽了,如今值爲:「' + newVal.toString() + '」'); dep.notify(); // 若是數據變化,通知全部訂閱者 } }); } Dep.target = null; //在new SelfVue作一個代理,讓訪問selfVue的屬性代理爲訪問selfVue.data的屬性,實現原理仍是使用Object.defineProperty()對屬性值再包一層。 function SelfVue (data, el, exp) { var self = this; this.data = data; Object.keys(data).forEach(function(key) { self.proxyKeys(key); // 綁定代理屬性 }); observe(data); el.innerHTML = this.data[exp]; // 初始化模板數據的值 new Watcher(this, exp, function (value) { el.innerHTML = value; }); 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; } }); } } var ele = document.querySelector('#name'); var selfVue = new SelfVue({ name: 'hello world' }, ele, 'name'); window.setTimeout(function () { console.log('name值改變了'); selfVue.name = 'canfoo'; }, 2000); </script> </html>
雖然上面已經實現了一個雙向數據綁定的例子,可是整個過程都沒有去解析dom節點,而是直接固定某個節點進行替換數據的,因此接下來須要實現一個解析器Compile來作解析和綁定工做。解析器Compile實現步驟:
1.解析模板指令,並替換模板數據,初始化視圖。
2.將模板指令對應的節點綁定對應的更新函數,初始化相應的訂閱器
爲了解析模板,首先須要獲取dom元素,而後對含有dom元素上含有指令的節點進行處理,所以這個環節須要對dom操做比較頻繁,因此能夠先建一個fragment片斷,將須要解析的dom節點存入fragment片斷裏再進行處理:ui
function nodeToFragment(el){ var fragment = document.createDocumentFragment(); var child = el.firstChild; while(child){ //將Dom元素移入fragment中 fragment.appendChild(child); child = el.firstChild } return fragment; }
接下來須要遍歷各個節點,對含有相關指定的節點進行特殊處理,這裏先處理最簡單的狀況,只對帶有{{變量}}這種形式的指令進行處理。this
function compileElement(el){ var childNodes = el.childNodes; var self = this; [].slice.call(childNodes).forEach(function(node){ var reg = /\{\(.*)\}\}/; var text = node.textContent; if(self.isTextNode(node)&®.test(text)){ self.compileText(node,reg.exec(text)[1]) } if(node.childNodes&&node.childNodes.length){ self.compileElement(node);//繼續遞歸遍歷子節點。 } }) } function compileText(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); }) } function (node,value){ node.tetxContent = typeof value =='undefined'?'':value; }
獲取到最外層節點後,調用compileElement函數,對全部子節點進行判斷,若是節點是文本節點且匹配{{}}這種形式指令的節點就開始進行變異處理,編譯處理首先須要初始化視圖數據,對應上面所說的步驟1,接下來須要生成一個並綁定更新函數的訂閱器,對應上面所說的步驟2.這樣就完成指令的解析、初始化、編譯三個過程,一個解析器Compile也就能夠正常的工做了。爲了將解析器Compile與監聽器Obsever和訂閱者Watcher關聯起來,咱們須要再修改一下類SelfVue函數:spa
function SelfVue(options){ var self = this; this.vm = this; this.data = options; Object.keys(this.data).forEach(function(key){ self.proxyKeys(key); }) observe(this.data); new Compile(options,this.vm); return this; }
createDocumentFragment:
建立一個新的空白的文檔片斷。
語法:
let fragment = document.createDocumentFragment();
fragment是一個指向空DocumentFragment對象的引用。DocumentFragment是DOM節點。它們不是主dom數的一部分。一般的用例是建立文檔片斷,將元素附加到文檔片斷,而後將文檔片斷附加到DOM樹。在DOM樹中,文檔片斷被其全部的子元素所代替。
由於文檔片斷存在於內存中,並不在DOM樹中,因此將子元素插入到文檔片斷時不會引發頁面迴流。所以使用文檔片斷一般會帶來更好的性能。
<!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> function Observer(data) { this.data = data; this.walk(data); } Observer.prototype = { walk: function(data) { var self = this; Object.keys(data).forEach(function(key) { self.defineReactive(data, key, data[key]); }); }, defineReactive: function(data, key, val) { var dep = new Dep(); var childObj = observe(val); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { console.log(6) if (Dep.target) { dep.addSub(Dep.target); } return val; }, set: function(newVal) { console.log(5) if (newVal === val) { return; } val = newVal; dep.notify(); } }); } }; function observe(value, vm) { if (!value || typeof value !== 'object') { return; } return new Observer(value); }; function Dep () { this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach(function(sub) { sub.update(); }); } }; Dep.target = null; 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() { console.log(4) Dep.target = this; // 緩存本身 var value = this.vm.data[this.exp] // 強制執行監聽器裏的get函數 Dep.target = null; // 釋放本身 return value; } }; 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; } }); } } 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) { console.log(1) 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) { console.log(2) node.textContent = typeof value == 'undefined' ? '' : value; }, isTextNode: function(node) { return node.nodeType == 3; } } var selfVue = new SelfVue({ el: '#app', data: { title: 'hello world', name: '13' } }); // window.setTimeout(function () { // selfVue.title = '你好'; // }, 2000); // window.setTimeout(function () { // selfVue.name = 'canfoo'; // }, 2500); </script> </html> <script> </script>
代碼分析:
1.new SelfVue() 作了三件事:1.使用Object.definePrototype代理讓訪問selfVue的屬性代理爲訪問selfVue.data的屬性。2.一樣使用Object.definePrototype數據劫持。3.new Compile()。