騰訊DeepOcean原創文章:dopro.io/vue-mvvm-re…javascript
var a = {};
Object.defineProperty(a, 'b', { value: 123, // 設置的屬性值 writable: false, // 是否只讀 enumerable: false, // 是否可枚舉 configurable: false //
});
console.log(a.b); //123複製代碼
方法使用很簡單,它接受三個參數,並且都是必填的html
// 經常使用定義
var obj = {};
Object.defineProperty(obj, 'school', { enumerable: true, get: function() {
// 獲取屬性值時會調用get方法 }, set: function(newVal) {
// 設置屬性值時會調用set方法 return newVal } });複製代碼
咱們經過這個Object.defineProperty這個方法,能夠實現對定義的引用數據類型的實現監聽,被方法監聽後的對象,裏面定義的值發生被獲取和設置操做的時候,會分別觸發Object.defineProperty裏面參數三的get和set方法。前端
function MyVue(options = {}) {
// 將全部的屬性掛載到$options身上 this.$options = options;
// 獲取到data數據(Model) var data = this._data = this.$options.data;
// 劫持數據 observe(data) }
// 給須要觀察的對象都添加 Object.defineProperty 的監聽
function Observe(data) {
for (let key in data) {
let val = data[key];
// 遞歸 =》來實現深層的數據監聽 observe(val)
Object.defineProperty(data, key, { enumerable: true, get() {
return val }, set(newval) { if (val === newval) { //設置的值是否和之前是同樣的,若是是就什麼都不作 return } val = newval // 這裏要把新設置的值也在添加一次數據劫持來實現深度響應, observe(newval); } }) } }
function observe(data) {
// 這裏作一下數據類型的判斷,只有引用數據類型纔去作數據劫持 if (typeof data != 'object') return return new Observe(data) }複製代碼
1)以上代碼作了這些事情,先定義了初始換構造函數MyVue咱們經過它來獲取到咱們傳進來的數據data和咱們定義的DOM節點範圍,而後把data傳進定好的數據劫持方法observevue
2)Observe實現了對數據的監聽總體邏輯,這裏有個細節點,沒有直接用構造函數Observe去劫持咱們的數據,而是寫多了一個observe的小方法用來new Observe,而且在裏面作了引用數據類型的判斷。這樣作的目的是爲了方便遞歸來實現數據結構的深層監聽,由於咱們的data結構確定是複雜多樣的,例以下面代碼java
// 這裏數據結構嵌套不少,要實現深層的數據監聽採用了遞歸的實現方式
data: { a: {b:2} , c:{q:12,k:{name:'binhemori'}} , f:'mvvvm',o:[12,5,9,8]}複製代碼
3)這裏還須要注意的是咱們在set方法裏面有再一次把設置的新值,執行了一遍observe方法,是爲了實現深度響應,由於在賦值的時候可能會賦值一個引用數據類型的值,咱們知道vue有個特色,是不能新增不存在的屬性和不能存在屬性沒有get和set方法的,若是賦值進來的新屬性值是引用數據類型,就會把咱們原先執行過數據劫持方法的對象地址給替換掉,而新對象是沒有通過數據劫持的就是沒有get和set的方法,因此咱們在設置新值的時候須要在從新給他執行一遍observe數據劫持,確保開發者無論怎樣去設置值的時候都能被劫持到node
說了這麼多,咱們來使用一下看看有沒有實現對數據的劫持(數據監聽)吧設計模式
數組
<div id="app">
<div> <div>這裏的數據1======<span style="color: red;">{{a.b}}</span></div> <div>這裏是數據2======<span style="color: green;">{{c}}</span></div> </div> <input type="text" v-model="a.b" value=""> </div> <!-- 引入本身定義的mvvm模塊 --> <script src="./mvvm.js"></script> 複製代碼<script type="text/javascript"> var myvue = new MyVue({ el: '#app', data: { a: { b: '你好' }, c: 12, o: [12, 5, 9, 8] } })
</script>複製代碼
能夠看到對咱們所定義的data中的數據都已經有了get和set方法了,到這裏咱們對data中數據的變化都是能夠監聽的到了微信
數據代理,咱們用過vue的都知道,在實際使用中是能直接經過實例+屬性(vm.a)直接獲取到數據的,而咱們上面的代碼要獲取到數據還須要這樣myvue._data.a這樣來獲取到數據,中間多了一個 _data 的環節,這樣使用起來不是很方便的,下面咱們來實現讓咱們的實例this來代理( _data)數據,從而實現 myvue.a 這樣的操做能夠直接獲取到數據數據結構
function MyVue(options = {}) {
// 將全部的屬性掛載到$options身上 this.$options = options;
// 獲取到data數據(Model) var data = this._data = this.$options.data; observe(data);
// this 就代理數據 this._data for (const key in data) {
Object.defineProperty(this, key, { enumerable: true, get() {
// this.a 這裏取值的時候 其實是去_data中的值 return this._data[key] }, set(newVal) { // 設置值的時候其實也是去改this._data.a的值 this._data[key] = newVal } }) } }複製代碼
以上代碼實現了咱們的數據代理,就是在構建實例的時候,把data中的數據遍歷一次出來,依次加到咱們this上,加的過程當中也不要忘記添加Object.defineProperty,只要是數據咱們都須要添加監聽。以下圖咱們已經實現了對數據的代理
咱們已經完成對數據劫持也實現了this對數據的代理,那麼接下來要作的就是怎樣把數據編譯到咱們的DOM節點上面,也就是讓視圖層(view)要展現咱們的數據了
// 將數據和節點掛載在一塊兒
function Compile(el, vm) {
// el表示替換的範圍 vm.$el = document.querySelector(el);
// 這裏注意咱們沒有去直接操做DOM,而是把這個步驟移到內存中來操做,這裏的操做是不會引起DOM節點的迴流 let fragment = document.createDocumentFragment(); // 文檔碎片 let child;
while (child = vm.$el.firstChild) {
// 將app的內容移入內存中 fragment.appendChild(child); }
replace(fragment)
function replace(fragment) {
Array.from(fragment.childNodes).forEach(function (node) { //循環每一層 let text = node.textContent;
let reg = /\{\{(.*)\}\}/g;
// 這裏作了判斷只有文本節點纔去匹配,並且還要帶{{***}}的字符串 if (node.nodeType === 3 && reg.test(text)) {
// 把匹配到的內容拆分紅數組
let arr = RegExp.$1.split('.'); let val = vm;
// 這裏對咱們匹配到的定義數組,會依次去遍歷它,來實現對實例的深度賦值 arr.forEach(function (k) { // this.a.b this.c val = val[k] })
// 用字符串的replace方法替換掉咱們獲取到的數據val node.textContent = text.replace(/\{\{(.*)\}\}/, val) }
// 這裏作了判斷,若是有子節點的話 使用遞歸 if (node.childNodes) { replace(node) } }) }
// 最後把編譯完的DOM加入到app元素中 vm.$el.appendChild(fragment) }複製代碼
以上代碼實現咱們對數據的編譯Compile以下圖,能夠看到咱們把獲取到el下面全部的子節點都存儲到了文檔碎片 fragment 中暫時存儲了起來(放到內存中),由於這裏要去頻繁的操做DOM和查找DOM,因此移到內存中操做
在成功的將咱們的數據綁定到了DOM節點以後,要實現咱們的視圖層(view)跟數據層(model)的關聯,如今實際上尚未關聯起來,由於沒法經過改數據值來引起視圖的變化,實現這步以前先聊一下JS中比較經常使用的設計模式發佈訂閱模式也是vue實現雙向數據綁定的很關鍵的一步
咱們先簡單手動實現一個(就是一個數組關係)
// 發佈訂閱
function Dep() {
this.subs = [] }
// 訂閱
Dep.prototype.addSub = function (sub) {
this.subs.push(sub) }
// 通知
Dep.prototype.notify = function (sub) {
this.subs.forEach(item => item.update()) }
// watcher是一個類,經過這個類建立的函數都會有update的方法
function Watcher(fn) {
this.fn = fn; } Watcher.prototype.update = function () {
this.fn() }複製代碼
這裏用Dep方法來實現訂閱和通知,在這個類中有addSub(添加)和notify(通知)兩個方法,咱們把將要作的事情(函數)經過addSub添加進數組裏,等時機一到就notify通知裏面全部的方法執行
你們會發現爲何要另外定義一個建立函數的方法watcher,而不是直接把方法扔到addSub中好,這樣不是畫蛇添足嘛?其實這樣作的有它的目的,其中一個好處就是咱們經過watcher建立的函數都會有一個update執行的方法能夠方便咱們調用。而另一個用處我下面會講到,先把它運用起來吧
function replace(fragment) {
Array.from(fragment.childNodes).forEach(function (node) { let text = node.textContent;
let reg = /\{\{(.*)\}\}/g;
if (node.nodeType === 3 && reg.test(text)) {
let arr = RegExp.$1.split('.');
let val = vm; arr.forEach(function (k) { val = val[k] })
// 在這裏運用了Watcher函數來新增要操做的事情 new Watcher(vm, RegExp.$1, function (newVal) { node.textContent = text.replace(/\{\{(.*)\}\}/, newVal) }) 複製代碼 node.textContent = text.replace(/{{(.*)}}/, val) }
if (node.childNodes) { replace(node) } }) }複製代碼
能夠看到咱們把定義函數的方法watcher加到了replace方法裏面,可是這裏的watcher更剛寫編寫的多了兩個形參vm、RegExp.$1,並且寫法也新增了一些內容,由於當new Watcher的時候會引起發生幾個操做,來看代碼:
// vm作數據代理的地方
function MyVue(options = {}) {
this.$options = options;
var data = this._data = this.$options.data; observe(data);
for (const key in data) {
Object.defineProperty(this, key, { enumerable: true, get() {
return this._data[key] }, set(newVal) { this._data[key] = newVal } }) } }
// 數據劫持函數
function Observe(data) { let dep = new Dep();
for (let key in data) {
let val = data[key]; observe(val)
Object.defineProperty(data, key, { enumerable: true, get() {
/* 獲取值的時候 Dep.target
對於着 watcher的實例,把他建立的實例加到訂閱隊列中
*/ Dep.target && dep.addSub(Dep.target); return val }, set(newval) { if (val === newval) {
return } val = newval; observe(newval);
// 設置值的時候讓全部的watcher.update方法執行便可觸發全部數據更新 dep.notify() } }) } }
function Watcher(vm, exp, fn) {
this.fn = fn;
// 這裏咱們新增了一些內容,用來能夠獲取對於的數據 this.vm = vm;
this.exp = exp; Dep.target = this let val = vm;
let arr = exp.split('.');
/* 執行這一步的時候操做的是vm.a,
而這一步操做其實就是操做的vm._data.a的操做,
會觸發this代理的數據和_data上面的數據
*/ arr.forEach(function (k) { val = val[k] }) Dep.target = null; }
// 這裏是設置值操做
Watcher.prototype.update = function () {
let val = this.vm;
let arr = this.exp.split('.'); arr.forEach(function (k) { val = val[k] })
this.fn(val) //這裏面要傳一個新值
}複製代碼
這裏開始會有點繞,必定要理解好操做數據的時候會觸發的那個實例上面數據的get和set,操做的是那個數據這個思惟
1)首先看在Watcher構造函數中新增了一些私有屬性分別表明:
arr.forEach(function (k) {
// arr = [a,b] val = val[k] })複製代碼
get() {
// 走到這裏的時候 Dep.target 已經存儲了 watcher的當前實例實例,把他建立的實例加到訂閱隊列中 Dep.target && dep.addSub(Dep.target); return val },
// 把要作的更新視圖層的操做方法用Watcher定義好,裏面已經定義好了要操做的對象
new Watcher(vm, RegExp.$1, function (newVal) { node.textContent = text.replace(/\{\{(.*)\}\}/, newVal) }) 複製代碼Watcher.prototype.update = function () {
let val = this.vm;
let arr = this.exp.split('.'); arr.forEach(function (k) { val = val[k] }) this.fn(val) // 把對於的新值傳遞到方法裏面
}複製代碼
這裏由於加多了一層 vm.a 這樣的數據代理,因此邏輯有點繞,記住這句話就好理解操做 vm.a 代理數據上面值的時候,其實就是操做的vm._data中的數據因此會觸發兩個地方的get和set方法,好說這麼多,咱們來看是否實現數據變更觸發視圖層的變化吧
這裏就實現了數據的變動觸發視圖層的更新操做了
最後一步就來實現視圖層的變動觸發數據結構的變動操做,上面咱們已經把視圖與數據關聯最核心的代碼講解了,剩下視圖變動觸發數據變動就比較好實現了
<div id="app">
<div> <div>這裏的數據1======<span style="color: red;">{{a.b}}</span></div> <div>這裏是數據2======<span style="color: green;">{{c}}</span></div> </div> <input type="text" v-model="a.b" value="">
</div>
<!-- 引入本身定義的mvvm模塊 -->
<script src="./mvvm.js"></script>
<script type="text/javascript"> var myvue = new MyVue({ el: '#app', data: { a: { b: '你好' }, c: 12, o: [12, 5, 9, 8] } })
</script>複製代碼
// 獲取全部元素節點
if (node.nodeType === 1) {
let nodeAttr = node.attributes
Array.from(nodeAttr).forEach(function (attr) {
let name = attr.name; // v-model="a.b" let exp = attr.value; // a.b 複製代碼 if (name.indexOf('v-') >= 0) {
let val = vm;
let arr = exp.split('.'); arr.forEach(function (n) { val = val[n] })
// 這個還好處理,取到對應的值設置給input.value就好 node.value = val; }
// 這裏也要定義一個Watcher,由於數據變動的時候也要變動帶有v-model屬性名的值 new Watcher(vm, exp, function (newVal) { node.value = newVal })
// 這裏是視圖變化的時候,變動數據結構上面的值 node.addEventListener('input', function (e) {
let newVal = e.target.value
if (name.indexOf('v-') >= 0) {
let val = vm;
let arr = exp.split('.'); arr.forEach(function (k,index) { if (typeof val[k] === 'object') { val = val[k] } else{
if (index === arr.length-1) { val[k] = newVal } } }) } }) }) }複製代碼
上面代碼對數據變動觸發視圖層變動的邏輯更上一節同樣便可,主要是node.addEventListener('input')這裏設置數據的問題,其實原理跟第六節關聯視圖(view)與數據(model)的邏輯同樣,有必定須要注意的是這邊加了一個引用數據類型的判斷,否則他的循環會到最底層的數據類型值(也就是基礎數據類型) 1)這裏判斷到取到的不是對象數據類型,不作替換操做 (val = val[k]) 2)判斷是否是已經最後一個層級了index === arr.length-1,若是是的話直接把input中的值賦值進當前數據中便可
arr.forEach(function (k,index) { if (typeof val[k] === 'object') {
// 若是有嵌套的話就繼續查找 val = val[k] } else{
if (index === arr.length-1) {
// 查找到最後一個後直接賦值 val[k] = newVal } } })複製代碼
以上是整個mvvm雙向數據綁定的簡單實現原理,內容有些哪裏解釋不通順的地方或有更好的意見歡迎留言:)