面試過程當中,面試官必定會問你 描述一下 你所知道的MVVM?html
MVVM 在Vue中是用什麼來實現的?vue
OK,咱們來攻克這個題目node
首先第一個M,指的是 Model, 也就是**數據模型
,其實就是數據, 換到Vue裏面,其實指的就是 Vue組件實例中的data
**, 可是這個data 咱們從一開始就定義了 它叫 響應式數據
react
第二個V,指的是View, 也就是**頁面視圖
, 換到Vue中也就是 咱們的template
轉化成的DOM對象
**面試
第三個 VM, 指的是**ViewModel
, 也就是 視圖和數據的管理者, 它管理着咱們的數據 到 視圖變化的工做,換到Vue中 ,它指的就是咱們的當前的Vue實例
, Model數據 和 View 視圖通訊的一個橋樑
**數組
數據驅動視圖
, 數據變化 =>視圖更新 雙向 綁定 視圖更新 => 數據變化Vue ==>MVVM => 雙向數據綁定 => this.name = '張三 '瀏覽器
React => MVVM => 單向數據綁定 => 只能從數據 => 視圖 => this.setState({ name: '張三' })app
<!-- 視圖 --> <template> <div>{{ message }}</div> </template> <script> // Model 普通數據對象 export default { data () { return { message: 'Hello World' } } } </script> <style> </style>
接下里,咱們來重點研究MVVM的原理及實現方式,Vuejs官網給出了MVVM的原理方式框架
Vue文檔說明dom
經過上面的文檔咱們能夠發現, Vue的響應式原理(MVVM)實際上就是下面這段話:
當你把一個普通的 JavaScript 對象傳入 Vue 實例做爲
data
選項,Vue 將遍歷此對象全部的屬性,並使用Object.defineProperty
把這些屬性所有轉爲 getter/setter。Object.defineProperty
是 ES5 中一個沒法 shim 的特性,這也就是 Vue 不支持 IE8 以及更低版本瀏覽器的緣由。
從上面的表述中,咱們發現了幾個關鍵詞, Object.defineProperty
getter/setter
什麼是 Object.defineProperty?
定義:Object.defineProperty()
方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。
語法: Object.defineProperty(obj, prop, descriptor)
參數: obj => 要在其上定義屬性的對象。
prop => 要新增或者修改的屬性名
descriptor => 將被定義或修改的屬性描述符。
返回值 : 被傳遞給函數的對象。 也就是 傳入的obj對象
經過上面的筆記 咱們來看下 有哪些參數 須要學習
obj 就是一個對象 能夠 new Object() 也能夠 { }
prop 就是屬性名 也就是一個字符串
descriptor 描述符是什麼 ? 有哪些屬性
對象裏目前存在的屬性描述符有兩種主要形式:數據描述符
和存取描述符
。數據描述符
是一個具備值的屬性,該值多是可寫的,也可能不是可寫的。存取描述符
是由getter-setter函數對描述的屬性。描述符必須是這兩種形式之一;不能同時是二者
。
上面是官方描述 ,它告訴咱們 defineProterty設計上有**
兩種模式
存在,一種數據描述
, 一種存取描述
**描述符必須是這兩個中的一個 ,不能同時是二者, 也就是
一山不容二虎
, 也不能一山兩虎都無
咱們寫一個最簡單的 **數據描述符
**的例子
// Object.defineProperty(obj,prop, desciptor) // desciptor => 數據描述符 存取描述符 var obj = { name: '曹揚' } var o = Object.defineProperty(obj, 'weight', { // 描述符是一個對象 // 數據描述 存取描述 value: '280kg' // 數據描述符 value }) console.log(o)
接下來進行詳細分析
數據描述符
模式數據描述符有哪些屬性?
value
=>該屬性對應的值。能夠是任何有效的 JavaScript 值(數值,對象,函數等)。默認爲 unfinedwritable
=> 當且僅當該屬性的writable爲true時,value才能被賦值運算符改變。默認爲 false。就這兩個 ? 還有嗎 ?
configurable
=> 當且僅當該屬性的 configurable 爲 true 時,該屬性描述符
纔可以被改變,同時該屬性也能從對應的對象上被刪除。默認爲 false。決定writable可不可改enumerable
=> 當且僅當該屬性的enumerable
爲true
時,該屬性纔可以出如今對象的枚舉屬性中。默認爲 false。爲何
configurable
和enumerable
不一樣時 和 value 還有 writable一塊兒寫呢 ?
由於這兩個屬性不但能夠在數據描述符裏出現 還能夠在 存取描述符裏出現
咱們經過writeable 和 value屬性來寫一個 可寫的屬性 和不寫的屬性
var obj = { name: '曹揚' } Object.defineProperty(obj, 'money', { value: "10k" // 薪水 此時薪水是不可改的 }) Object.defineProperty(obj, 'weight', { value: '150斤', // 給一萬根頭髮 writable: true }) obj.money = '20k' obj.hair = '200斤' console.log(obj)
接下來 ,咱們但願 去讓一個不可變的屬性變成可變的
var obj = { name: '曹揚' } Object.defineProperty(obj, 'money', { value: '10k', // 薪水 此時薪水是不可改的 configurable: true // 只有這裏爲true時 才能去改writeable屬性 }) Object.defineProperty(obj, 'weight', { value: '150斤', // 給一萬根頭髮 writable: true }) obj.money = "20k" obj.weight = '200斤' console.log(obj) Object.defineProperty(obj, 'money', { writable: true }) obj.money = '20k' console.log(obj)
接下來,咱們但願能夠在遍歷的時候 遍歷到新添加的兩個屬性
var obj = { name: '曹揚' } Object.defineProperty(obj, 'money', { value: '10k', // 薪水 此時薪水是不可改的 configurable: true, enumerable: true }) Object.defineProperty(obj, 'weight', { value: '150斤', // 給一萬根頭髮 writable: true, enumerable: true }) obj.money = "20k" obj.weight = '200斤' console.log(obj) Object.defineProperty(obj, 'money', { writable: true }) obj.money = '20k' console.log(obj) for(var item in obj) { console.log(item) }
存取描述符
模式上一小節中,數據描述符 獨有的屬性 是 value 和 writable , 這也就意味着, 在存取描述模式中
value 和 writable屬性不能出現
那麼 存儲描述符有啥屬性 ?
get
一個給屬性提供 getter 的方法,若是沒有 getter 則爲 undefined
。當訪問該屬性時,該方法會被執行,方法執行時沒有參數傳入,可是會傳入this
對象(因爲繼承關係,這裏的this
並不必定是定義該屬性的對象)。set
一個給屬性提供 setter 的方法,若是沒有 setter 則爲 undefined
。當屬性值修改時,觸發執行該方法。該方法將接受惟一參數,即該屬性新的參數值。get/set 其實就是咱們最多見的 讀取值 和設置值得方法
讀取值得時候 調用 get方法
設置值得時候調用 set方法
咱們作一個 能夠 經過 get 和 set 讀取設置的方法
var obj = { name: '曹操' } var wife = '小喬' Object.defineProperty(obj, 'wife',{ get () { return wife }, set (value) { wife = value } }) console.log(obj.wife) obj.wife= '大喬' console.log(obj.wife)
可是,咱們想要遍歷怎麼辦 ? 注意哦 , 存儲描述符的時候 依然擁有 configurable 和 enumerable屬性,
依然能夠配置哦
var obj = { name: '曹操' } var wife = '小喬' Object.defineProperty(obj, 'wife',{ configurable:true, enumerable: true, get () { return wife }, set (value) { wife = value } }) console.log(obj.wife) obj.wife= '大喬' console.log(obj.wife) for(var item in obj) { console.log(item) }
經過兩個小節,學習了 defineProperty的基本使用, 接下里咱們要經過defineProperty模擬 Vue實例化的效果
Vue實例化的時候, 咱們明明給data賦值了數據,可是卻能夠經過 **vm實例.屬性
**進行訪問和設置
怎麼作的 ?
實際上這就是 經過 Object.defineProperty實現的
var data = { name: "張三" }; var vm = {}; Object.defineProperty(vm, "name", { set(value) { data.name = value; }, get() { return data.name; } }); console.log(vm.name); vm.name = "李四"; console.log(vm.name);
上面代碼中,咱們實現了 vm中的數據代理了 data中的name 直接改vm就是改data
總結
: 咱們在 set和get的存取描述符中 代理了 data中的數據,
MVVM =>數據代理 =>object.defineProperty =>存取描述符get/set =>代理數據
MVVM不但要獲取這些數據,而且將這些數據 進行 響應式的更新到DOM中, 也就是 數據變化時,咱們要把數據**反映
**到視圖上
經過調試咱們發現,咱們是能夠在set函數裏面監聽到數據的變化的,只須要在數據變化的時候, 通知對應的視圖來更新就能夠了
那麼 怎麼通知 ? 用什麼技術來作 ? 下一小節中咱們將帶來發布訂閱模式
發佈訂閱模式爲什麼物?
其實咱們早已用過不少遍, 發佈 /訂閱 即 有人**發佈消息
**, 有人 訂閱消息
,到了 數據層面 就是 多 => 多
即 A程序 能夠觸發多個消息 也能夠訂閱 多個消息
在黑馬頭條項目1 和項目2 中咱們 曾經 用過一個eventBus 就是發佈訂閱模式的體現
這個模式咱們拿來作什麼?
上個小節,咱們已經可以捕捉數據的變化,接下來,咱們就要嘗試在數據變化的時候經過 發佈訂閱這個模式 來改變咱們的視圖
咱們先寫出這個發佈訂閱核心代碼的幾個要素
首先,咱們但願 能夠經過實例化 獲得 發佈訂閱對象
發佈消息 $emit
訂閱消息 $on
根據上述思想,咱們獲得以下代碼
// 建立一個構造函數 function Events (){} // 訂閱消息 Events.prototype.$on = function(){} // 發佈消息 Events.prototype.$emit = function(){}
function Events () { this.subs = {} } Events.prototype.$on = function (eventName, fn) { this.subs[eventName] = this.subs[eventName] || [] this.subs[eventName].push(fn) } Events.prototype.$emit = function (eventName, ...params) { if(this.subs[eventName]) { this.subs[eventName].forEach(fn => { //fn.apply(this, [...params]) //fn.call(this, ...params) fn.bind(this,...params)() }); } } var test = new Events() test.$on("updateABC", function(a,b,c){ console.log(a+'-'+b+'-'+c) console.log(this) }) var go = function(){ test.$emit("updateABC", 1,2,3) }
這裏用到了call/apply/bind方法修改函數內部的this指向
利用發佈訂閱模式能夠實現當事件觸發時會通知到不少人去作事情,Vue中作的事情是更新DOM
咱們學習了 Object.defineProperty 和 發佈訂閱模式, 幾乎擁有了手寫一個MVVM的能力,
可是在實現MVVM以前,咱們仍是複習一下 View中也就是 Dom中的含義及結構
DOM是什麼?
文檔對象模型 document
Dom的做用是什麼?
能夠經過**
對象
**去操做頁面元素
Dom中的對象節點都有什麼類型
能夠經過下面的一個小例子檢查
<div id="app"> <h1>衆志成城,共抗疫情</h1> <div> <span style='color:red;font-weight: bold;'>老高:</span> <span>祝全部同窗前程似錦</span> </div> </div> <script> var app = document.getElementById("app") console.dir(app) </script>
經過上面的輸出查看, 咱們能夠發現
元素類型的節點類型 nodeType 爲1 文本類型爲 3, document對象裏面的每一個內容都是**節點
**
childNodes 是全部的節點 childer 值的是 全部的元素 =>nodeType =>節點類型
全部的子節點都放在 childNodes 這個屬性下,childNodes是僞數組 => 僞數組不具備數組方法,有length屬性
全部標籤的屬性集合是什麼?
attributes
分析DOM對象作什麼呢? 咱們前面準備的數據捕獲和 發佈訂閱就是爲了來更新DOM的
接下來咱們開始手寫一個MVVM示例
挑戰來了,咱們要手寫 一個簡易的**
vuejs
**, 提高咱們自身的技術實力.
咱們要實現mvvm的構造函數
構造函數 模仿vuejs 分別有 data /el
data最終被代理給當前的vm實例, 便可以經過 vm訪問,也能夠經過 this.$data訪問
// 首先實現一個構造函數 function Vue (options) { this.$options = options // 全部屬性都給this的一個屬性 this.$data = options.data || {} this.$el = typeof options.el ==="string" ? document.querySelector(options.el) : options.el // 把全部data的數據 代理給 當前實例 this.$proxyData() } // 代理數據 Vue.prototype.$proxyData = function () { Object.keys(this.$data).forEach(key => { Object.defineProperty(this, key, { get () { return this.$data[key] }, set (value) { if(this.$data[value] === value) return this.$data[key] = value } }) }) } var vm = new Vue({ el: '#app', data: { name: '曹揚', company: '攬月一顆' } }) console.log(vm.company) vm.company = '九天攬月' console.log(vm.$data.company) vm.$data.company = '下海捉鱉' console.log(vm.company)
OK,接下來這一步很是關鍵,咱們要作**
數據劫持
**, 劫持誰? 爲何要劫持?
上小節代碼中, 咱們能夠經過 vm.company = '值' 也能夠經過 vm.$data.name = '值', 那麼在哪裏捕捉數據的變化呢?
不管是 this.data 仍是 this.$data 改的都是data的數據,因此咱們須要對 data的數據進行**
劫持
**, 也就是監聽它的set
// 監聽數據 Vue.prototype.observer = function () { Object.keys(this.$data).forEach(key => { let value = this.$data[key] Object.defineProperty(this.$data, key, { get () { return value }, set (newValue) { if(newValue === value) return; value = newValue // 若是數據變化了 咱們須要 去改變視圖 } }) }) }
在構造函數中完成對數據的劫持
// 首先實現一個構造函數 function Vue (options) { this.$options = options // 全部屬性都給this的一個屬性 this.$data = options.data || {} this.$el = typeof options.el ==="string" ? document.querySelector(options.el) : options.el // 把全部data的數據 代理給 當前實例 this.$proxyData() this.observer() // 開啓監聽數據 }
如今咱們基本實現了 實例化數據,而且完成了對數據的劫持,接下來咱們須要實現幾個方法
數據變化時 => 根據最新數據把模板轉化成最新的對象
判斷是不是文本節點
判斷是不是 元素節點
判斷是不是指令
處理元素節點
處理文本節點
因此咱們定義下面幾個方法
// 編譯模板 Vue.prototype.compile = function () {} // 處理文本節點 Vue.prototype.compileTextNode = function (node){} // 處理元素節點 Vue.prototype.compileElementNode = function (node) {} // 判斷是不是文本節點 Vue.prototype.isTextNode = function(node) {}; // 判斷是不是元素節點 Vue.prototype.isElementNode = function(node) {}; // 判斷屬性是不是指令 Vue.prototype.isDirective = function(attr) {};
咱們已經經過構造函數拿到了$el,也就是頁面的dom元素,接下來咱們能夠實現 一下編譯的基本邏輯
// 編譯模板 Vue.prototype.compile = function (rootnode) { let nodes = Array.from(rootnode.childNodes) // 先把僞數組轉成數組 nodes.forEach(item => { if(this.isTextNode(item)) { this.compileTextNode(node) } if(this.isElementNode(item)) { this.compileElementNode(node) this.compile(node) // 遞歸的思路 } }) } // 處理文本節點 Vue.prototype.compileTextNode = function (node){} // 處理元素節點 Vue.prototype.compileElementNode = function (node) {} // 判斷是不是文本節點 Vue.prototype.isTextNode = function(node) { return node.nodeType === 3; }; // 判斷是不是元素節點 Vue.prototype.isElementNode = function(node) { return node.nodeType === 1; }; // 判斷屬性是不是執行 Vue.prototype.isDirective = function(attr) { return attr.startsWith("v-"); };
上述代碼的基本邏輯就是 碰到 文本節點就用文本節點的方法處理 碰到元素節點 用元素節點的方法處理
// 處理文本節點 Vue.prototype.compileTextNode = function (node){ const text = node.textContent const reg = /\{\{(.+?)\}\}/g if(reg.test(text)) { // 若是知足雙大括號 const key = RegExp.$1.trim() this.$on(key, () => { node.textContent = text.replace(reg, this[key]) // 若是找到大括號 就替換對應的數據 }) node.textContent = text.replace(reg, this[key]) // 若是找到大括號 就替換對應的數據 } }
提示: 實際開發時正則不須要記 可是要能看懂
// 處理元素節點 Vue.prototype.compileElementNode = function (node) { let atts = Array.from(node.attributes) attrs.forEach(attr => { if(this.isDirective(attr.name)) { // 判斷是不是指令 if(attr.name === 'v-text') { node.textContent = this[attr.value] // 等於當前屬性的值 } if(attr.name === 'v-model') { // v-model綁定的是表單的value屬性 node.value = this[attr.value] } } }) }
目前響應式數據有了, 編譯模板也有了, 咱們須要在數據變化的時候編譯模板
以前講了, 這一步須要 經過發佈訂閱來作 ,因此咱們在Vue的基礎上實現發佈訂閱
// 首先實現一個構造函數 function Vue (options) { this.subs = {} //發佈訂閱管理器 this.$options = options // 全部屬性都給this的一個屬性 this.$data = options.data || {} this.$el = typeof options.el ==="string" ? document.querySelector(options.el) : options.el // 把全部data的數據 代理給 當前實例 this.$proxyData() this.observer() // 開啓監聽數據 } Vue.prototype.$on= function (eventName, fn) { this.subs[eventName] = this.subs[eventName] || [] this.subs[eventName].push(fn) } Vue.prototype.$emit = function (eventName, ...params) { if(this.subs[eventName]) { this.subs[eventName].forEach(fn => { //fn.apply(this, [...params]) //fn.call(this, ...params) fn.bind(this,...params)() }); } }
如今萬事俱備,只欠東風
咱們的數據代理,數據劫持,模板編譯, 事件發佈訂閱通通搞定 如今只須要在數據變化時 ,經過事件發佈,而後
通知 數據進行編譯便可
// 監聽數據 Vue.prototype.observer = function () { Object.keys(this.$data).forEach(key => { let value = this.$data[key] Object.defineProperty(this.$data, key, { get () { return value }, set: (newValue) => { if(newValue === value) return; value = newValue // 若是數據變化了 咱們須要 去改變視圖 this.$emit(key) //觸發數據改變 } }) }) }
監聽數據改變
// 處理文本節點 Vue.prototype.compileTextNode = function (node){ const text = node.textContent if(/\{\{(.+)\}\}/.test(text)) { // 若是知足雙大括號 const key = RegExp.$1.trim() this.$on(key, () => { node.textContent = text.replace(reg, this[key]) // 若是找到大括號 就替換對應的數據 }) node.textContent = text.replace(reg, this[key]) // 若是找到大括號 就替換對應的數據 } } // 處理元素節點 Vue.prototype.compileElementNode = function (node) { let atts = Array.from(node.attributes) attrs.forEach(attr => { if(this.isDirective(attr.name)) { // 判斷是不是指令 if(attr.name === 'v-text') { node.textContent = this[attr.value] // 等於當前屬性的值 this.$on(attr.value, () => { node.textContent = this[attr.value] // 若是找到大括號 就替換對應的數據 }) } if(attr.name === 'v-model') { // v-model綁定的是表單的value屬性 node.value = this[attr.value] this.$on(attr.value, () => { node.value = this[attr.value] // 若是找到大括號 就替換對應的數據 }) } } }) }
而後咱們寫個例子來測試一把
最後咱們但願實現雙向綁定,即視圖改變時 數據同時變化
// 處理元素節點 Vue.prototype.compileElementNode = function (node) { let attrs = Array.from(node.attributes) attrs.forEach(attr => { if(this.isDirective(attr.name)) { // 判斷是不是指令 if(attr.name === 'v-text') { node.textContent = this[attr.value] // 等於當前屬性的值 this.$on(key, () => { node.textContent = this[attr.value] // 若是找到大括號 就替換對應的數據 }) } if(attr.name === 'v-model') { // v-model綁定的是表單的value屬性 node.value = this[attr.value] this.$on(key, () => { node.value = this[attr.value] // 若是找到大括號 就替換對應的數據 }) node.oninput = () => { this[attr.value] = node.value } } } }) }