摘要:原理如圖,實現一個,需要幾個輔助工具,分別是。我的模式中的功能有兩個。對將中的數據綁定到上下文環境上,對數據進行劫持,當數據變化的時候通知。到此就全部完成了模式。
前言
網上講 vue 原理,mvvm 模式的實現,數據雙向綁定的文章一搜一大堆,不管寫的誰好誰壞,都是寫的自己的理解,我也發一篇文章記錄自己的理解,如果對看官有幫助,那也是我莫大的榮幸,不過看完之后,你們以后如果再被面試官問到 vue 的原理的時候,千萬不要只用一句【通過 javascrit 的 Object.defineProperty 將 data 進行劫持,發生改變的時候改變對應節點的值】這么籠統的話來應付了。如果有不懂的,可以問我。話不多說,上效果圖:
效果以及代碼
{{a}}
怎么樣,是不是跟vue的寫法很像,跟著我的思路,你們也可以的。
原理talk is cheap, show you the picture
如圖,實現一個mvvm,需要幾個輔助工具,分別是 Observer, Compile, Dep, Watcher。每個工具各司其職,再由 MVVM 統一掉配從而實現數據的雙向綁定,下面我分別介紹下接下來出場的幾位菇涼
Compile 能夠將頁面中的頁面初始化,對指令進行解析,把 data 對應的值渲染上去的同時,new 一個 Watcher,并告訴它,當渲染的這個數據發生改變時告訴我,我好更新視圖。
Observer 能夠實現將 data 中的數據通過Object.defineProperty進行劫持,當獲取 data 中的值的時候,觸發get里方法,把 Compile 新建的 Watcher 抓過來,關到 Dep(發布訂閱者模式)的小黑屋里狂...,當值修改的時候,觸發 set 里的方法,通知小黑屋(Dep)里所有 Watcher 菇涼們,你們解放啦。
Dep 就是傳說中的小黑屋了,其內在原理是發布訂閱者模式,不了解發布訂閱者模式的話可以看我 這篇文章
Watcher 們從小黑屋里逃出來之后就趕緊跑到對應的 Compile 那,告訴他開始更新視圖吧,看,我是愛你的。
哈哈,通過我很(lao)幽(si)默(ji)的講解。你們是不是都想下車了?
嗯,知道大概是怎么回事之后,我分別講他們的功能。不過話說前面,mvvm 模式之前有千絲萬縷的聯系,必須要全部看完,才能真正理解 mvvm 的原理。
Observe我的 mvvm 模式中 Observe 的功能有兩個。1.對將data中的數據綁定到上下文環境上,2.對數據進行劫持,當數據變化的時候通知 Dep。下面用一個 demo 來看看,如何將數據綁定到環境中,并劫持數據
Document
可以看到將 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 的實現,Dep 功能很簡單,難點是如何將 watcher 聯系起來,先看代碼吧。
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 是如何初始化視圖的
Document {{a}}
額,感覺還好理解吧,這里只是講了 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期待已久的 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 抓過來的。
當 Compile 創建 Watcher 出來的時候,也將 Dep.target 指向了 Watcher。同時獲取了該節點要渲染的值,觸發了 Observer 中的 get 方法,Dep.target 有值了,就執行 self.dep.depend();
depend 方法里執行 Dep.target.addDep(this); 而現在 Dep.target 指向 Watcher,所以執行的是 Watcher 里的 addDep 方法 同時把 Dep 實例傳過去。
Watcher 里的 addDep 方法是將 Watcher 放在的 Dep實例的 subs 數組里。
當vm里的值放生變化時,觸發 Observer 的 set 方法,觸發所有 subs 里的 Watcher 執行 Watcher 里的 update 方法。
update 方法里有 Compile 的回調,從而更新視圖。
好吧,真想大白了,原來 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模式。
參考合格前端系列第三彈-實現一個屬于我們自己的簡易MVVM庫
vue.js 權威指南
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/54553.html
摘要:原理如圖,實現一個,需要幾個輔助工具,分別是。我的模式中的功能有兩個。對將中的數據綁定到上下文環境上,對數據進行劫持,當數據變化的時候通知。到此就全部完成了模式。 前言 網上講 vue 原理,mvvm 模式的實現,數據雙向綁定的文章一搜一大堆,不管寫的誰好誰壞,都是寫的自己的理解,我也發一篇文章記錄自己的理解,如果對看官有幫助,那也是我莫大的榮幸,不過看完之后,你們以后如果再被面試官問...
摘要:在模式中一般把層算在層中,只有在理想的雙向綁定模式下,才會完全的消失。層將通過特定的展示出來,并在控件上綁定視圖交互事件,一般由框架自動生成在瀏覽器中。三大框架的異同三大框架都是數據驅動型的框架及是雙向數據綁定是單向數據綁定。 MVVM相關概念 1) MVVM典型特點是有四個概念:Model、View、ViewModel、綁定器。MVVM可以是單向綁定也可以是雙向綁定甚至是不綁...
摘要:具體代碼執行方式進入到的目錄下,命令行運行即可。確保為一個對象如果對象下有則不需要再次生成函數返回該對象的實例,這里判斷了如果該對象下已經有實例,則直接返回,不再去生產實例。這就確保了一個對象下的實例僅被實例化一次。 看這篇之前,如果沒有看過之前的文章,可拉到文章末尾查看之前的文章。 回顧 在 step4 中,我們大致實現了一個 MVVM 的框架,由3個部分組成: defineRe...
閱讀 1971·2021-09-09 09:33
閱讀 1112·2019-08-30 15:43
閱讀 2657·2019-08-30 13:45
閱讀 3304·2019-08-29 11:00
閱讀 853·2019-08-26 14:01
閱讀 3568·2019-08-26 13:24
閱讀 477·2019-08-26 11:56
閱讀 2686·2019-08-26 10:27