網上講 vue 原理,mvvm 模式的實現,數據雙向綁定的文章一搜一大堆,無論寫的誰好誰壞,都是寫的本身的理解,我也發一篇文章記錄本身的理解,若是對看官有幫助,那也是我莫大的榮幸,不過看完以後,大家之後若是再被面試官問到 vue 的原理的時候,千萬不要只用一句【經過 javascrit 的 Object.defineProperty 將 data 進行劫持,發生改變的時候改變對應節點的值】這麼籠統的話來應付了。若是有不懂的,能夠問我。話很少說,上效果圖:javascript
以及代碼html
<body> <div id="root"> <h1>{{a}}</h1> <button v-on:click="changeA">changeA</button> <h2 v-html="b"></h2> <input type="text" v-model="b"> </div> </body> <script src="./Watcher.js"></script> <script src="./Compile.js"></script> <script src="./Dep.js"></script> <script src="./Observe.js"></script> <script src="./MVVM.js"></script> <script> var vue = new MVVM({ el: '#root', data: { a: 'hello', b: 'world' }, methods: { changeA () { this.a = 'hi' } } }) </script>
怎麼樣,是否是跟vue的寫法很像,跟着個人思路,大家也能夠的。前端
talk is cheap, show you the picturevue
如圖,實現一個mvvm,須要幾個輔助工具,分別是 Observer, Compile, Dep, Watcher。每一個工具各司其職,再由 MVVM 統一掉配從而實現數據的雙向綁定,下面我分別介紹下接下來出場的幾位菇涼java
哈哈,經過我很(lao)幽(si)默(ji)的講解。大家是否是都想下車了?node
嗯,知道大概是怎麼回事以後,我分別講他們的功能。不過話說前面,mvvm 模式以前有千絲萬縷的聯繫,必需要所有看完,才能真正理解 mvvm 的原理。git
個人 mvvm 模式中 Observe 的功能有兩個。1.對將data中的數據綁定到上下文環境上,2.對數據進行劫持,當數據變化的時候通知 Dep。下面用一個 demo 來看看,如何將數據綁定到環境中,並劫持數據github
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> </body> <script> var data = { a: 'hello', b: 'world' } class Observer { constructor(obj, vm) { this.walk(obj, vm); } walk(obj, vm) { Object.keys(obj).forEach(key => { Object.defineProperty(vm, key, { configurable: true, enumerable: true, get () { console.log('獲取obj的值' + obj[key]) return obj[key]; }, set(newVal) { var val = obj[key]; if (val === newVal) return; console.log(`值更新啦`); obj[key] = newVal; } }) }) } } new Observer(data, window); console.log(window.a); window.a = 'hi'; </script> </html>
能夠看到將 data 數據綁定到 window 上,當數據變化時候,會打印 '值更新啦',那麼 data 變化 是如何通知 Dep 的呢?首先咱們要明白,observe 只執行一遍,將數據綁定到 mvvm 實例上,Dep也只有一個,以前說把全部的 Watcher 抓過來,全放在這個 Dep 裏,仍是看代碼說話把。面試
function observe (obj, vm) { if (!obj || typeof obj !== 'object') return; return new Observer(obj, vm) } class Observer { constructor(obj, vm) { // vm 表明上下文環境,也是指向 mvvm 的實例 (調用的時候會傳入) this.walk(obj, vm); // 實例化一個 Dep; this.dep = new Dep(); } walk (obj, vm) { var self = this; Object.keys(obj).forEach(key => { Object.defineProperty(vm, key, { configurable: true, enumerable: true, get () { // 當獲取 vm 的值的時候,若是 Dep 有 target 時執行,目的是將 Watcher 抓過來,後面還會說明 if (Dep.target) { self.dep.depend(); } return obj[key]; }, set (newVal) { var val = obj.key; if (val === newVal) return; obj[key] = newVal; // 當 劫持的值發生變化時候觸發,通知 Dep self.dep.notify(); } }) }) } }
接下來說講 Dep 的實現,Dep 功能很簡單,難點是如何將 watcher 聯繫起來,先看代碼吧。segmentfault
class Dep { constructor (props) { this.subs = []; this.uid = 0; } addSub (sub) { this.subs.push(sub); this.uid++; } notify () { this.subs.forEach(sub => { sub.update(); }) } depend (sub) { Dep.target.addDep(this, sub); } } Dep.target = null;
subs 是一個數組,用來存儲 Watcher 的,當數據更新時候(由Observer告知),會觸發 Dep 的 notify 方法,調用 subs 裏全部 Watcher 的 update 方法。
接下來是否是火燒眉毛的想知道 Dep 是如何將 Watcher 抓過來的吧(污污污),彆着急咱們先看看 Watcher 是如何誕生的。
我以爲 Compile 是 mvvm 中最勞苦功高的一個了,它的任務是頁面過來時候,初始化視圖,將頁面中的{{.*}}解析成對應的值,還有指令解析,如綁定值的 v-text、v-html 還有綁定的事件 v-on,還有創造 Watcher 去監聽值的變化,當值變化的時候又要更新節點的視圖。
咱們先看看 Compile 是如何初始化視圖的
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <div id="root"> <h1>{{a}}</h1> <div v-html="b"></div> </div> </body> <script> var data = { a: 'hello', b: 'world' } class Compile { constructor(el, vm) { this.$el = this.isElementNode(el) ? el : document.querySelector(el); this.$vm = vm; if (this.$el) { this.compileElement(this.$el); } } compileElement(el) { // 將全部的最小節點拿過來,循環判斷是文本節點就看看是否是 {{}} 包裹的字符串,是元素節點就看看是否是v-html喝v-text var childNodes = Array.from(el.childNodes); if (childNodes.length > 0) { childNodes.forEach(child => { var childArr = Array.from(child.childNodes); // 匹配{{}}裏面的內容 var reg = /\{\{((?:.)+?)\}\}/; if (childArr.length > 0) { this.compileElement(child) } if (this.isTextNode(child)) { var text = child.textContent.trim(); var matchTextArr = reg.exec(text); var matchText; if (matchTextArr && matchTextArr.length > 1) { matchText = matchTextArr[1]; this.compileText(child, matchText); } } else if (this.isElementNode(child)) { this.compileNode(child); } }) } } compileText(node, exp) { this.bind(node, this.$vm, exp, 'text'); } compileNode(node) { var attrs = Array.from(node.attributes); attrs.forEach(attr => { if (this.isDirective(attr.name)) { var directiveName = attr.name.substr(2); if (directiveName.includes('on')) { // 綁定事件 node.removeAttribute(attr.name); var eventName = directiveName.split(':')[1]; this.addEvent(node, eventName, attr.value); } else { // v-text v-html 綁定值 node.removeAttribute(attr.name); this.bind(node, this.$vm, attr.value, directiveName); } } }) } addEvent(node, eventName, exp) { node.addEventListener(eventName, this.$vm.$options.methods[exp].bind(this.$vm)); } bind(node, vm, exp, dir) { if (dir === 'text') { node.textContent = vm[exp]; } else if (dir === 'html') { node.innerHTML = vm[exp]; } else if (dir === 'value') { node.value = vm[exp]; } } // 是不是指令 isDirective(attr) { if (typeof attr !== 'string') return; return attr.includes('v-'); } // 元素節點 isElementNode(node) { return node.nodeType === 1; } // 文本節點 isTextNode(node) { return node.nodeType === 3; } } new Compile('#root', data); </script> </html>
額,感受還好理解吧,這裏只是講了 Compile 是如何將data中的值渲染到視圖上,買了個關子,沒有說如何建立 Watcher 的,思考一下,若是要建立 Watcher ,應該在哪一個位置建立比較好呢?
答案是渲染值的同時,同時創造一個 Watcher 來監聽,上代碼:
class Compile { constructor (el, vm) { this.$el = this.isElementNode(el) ? el : document.querySelector(el); this.$vm = vm; if (this.$el) { this.$fragment = this.nodeFragment(this.$el); this.compileElement(this.$fragment); this.$el.appendChild(this.$fragment); } } nodeFragment (el) { let fragment = document.createDocumentFragment(); let child; while (child = el.firstChild) { fragment.appendChild(child); } return fragment; } compileElement (el) { var childNodes = Array.from(el.childNodes); if (childNodes.length > 0) { childNodes.forEach(child => { var childArr = Array.from(child.childNodes); // 匹配{{}}裏面的內容 var reg = /\{\{((?:.)+?)\}\}/; if (childArr.length > 0) { this.compileElement(child) } if (this.isTextNode(child)) { var text = child.textContent.trim(); var matchTextArr = reg.exec(text); var matchText; if (matchTextArr && matchTextArr.length > 1) { matchText = matchTextArr[1]; this.compileText(child, matchText); } } else if (this.isElementNode(child)) { this.compileNode(child); } }) } } compileText(node, exp) { this.bind(node, this.$vm, exp, 'text'); } compileNode (node) { var attrs = Array.from(node.attributes); attrs.forEach(attr => { if (this.isDirective(attr.name)) { var directiveName = attr.name.substr(2); if (directiveName.includes('on')) { node.removeAttribute(attr.name); var eventName = directiveName.split(':')[1]; this.addEvent(node, eventName, attr.value); } else if (directiveName.includes('model')) { // v-model this.bind(node, this.$vm, attr.value, 'value'); node.addEventListener('input', (e) => { this.$vm[attr.value] = e.target.value; }) }else{ // v-text v-html node.removeAttribute(attr.name); this.bind(node, this.$vm, attr.value, directiveName); } } }) } addEvent(node, eventName, exp) { node.addEventListener(eventName, this.$vm.$options.methods[exp].bind(this.$vm)); } bind (node, vm, exp, dir) { if (dir === 'text') { node.textContent = vm[exp]; } else if (dir === 'html') { node.innerHTML = vm[exp]; } else if (dir === 'value') { node.value = vm[exp]; } new Watcher(exp, vm, function () { if (dir === 'text') { node.textContent = vm[exp]; } else if (dir === 'html') { node.innerHTML = vm[exp]; } }) } hasChildNode (node) { return node.children && node.children.length > 0; } // 是不是指令 isDirective (attr) { if (typeof attr !== 'string') return; return attr.includes('v-'); } // 元素節點 isElementNode (node) { return node.nodeType === 1; } // 文本節點 isTextNode (node) { return node.nodeType === 3; } }
這裏比上面演示的demo多建立一個文檔碎片,能夠加快解析速度,另外在 80 行建立了 Watcher,當數據變化時,執行回調函數,從而更新視圖。
期待已久的 Watcher 終於出來了,咱們先看看它長什麼樣:
class Watcher { constructor (exp, vm, cb) { this.$vm = vm; this.$exp = exp; this.depIds = {}; this.getter = this.parseGetter(exp); this.value = this.get(); this.cb = cb; } update () { let newVal = this.get(); let oldVal = this.value; if (oldVal === newVal) return; this.cb.call(this.vm, newVal); this.value = newVal; } get () { Dep.target = this; var value = this.getter.call(this.$vm, this.$vm); Dep.target = null; return value; } parseGetter (exp) { if (/[^\w.$]/.test(exp)) return; return function (obj) { if (!obj) return; obj = obj[exp]; return obj; } } addDep (dep) { if (!this.depIds.hasOwnProperty(dep.id)) { this.depIds[dep.id] = dep; dep.subs.push(this); } } }
也不怎麼樣嘛,只有30多行代碼,接下來睜大眼睛啦,看看它是怎麼被 Dep 抓過來的。
好吧,真想大白了,原來 Watcher 是引誘 Dep 把本身裝進小黑屋的。哈哈~
源碼已放在我本身的git庫裏,點擊這裏獲取源碼
講了半天,正主該出來了,mvvm 是如何將上面四個小夥伴給本身打工的呢,其實很簡單,上代碼
class MVVM { constructor (options) { this.$options = options; var data = this._data = this.$options.data; observe(data, this); new Compile(options.el || document.body, this); } }
就是實例 MVVM 的時候,調用數據劫持,和 Compile 初始化視圖。到此就所有完成了mvvm模式。