MVVM 在Vue中是用什麼來實現的?html
首先第一個M,指的是 Model, 也就是**數據模型
,其實就是數據, 換到Vue裏面,其實指的就是 Vue組件實例中的data
**, 可是這個data 咱們從一開始就定義了 它叫 響應式數據
vue
第二個V,指的是View, 也就是**頁面視圖
, 換到Vue中也就是 咱們的template
轉化成的DOM對象
**node
第三個 VM, 指的是**ViewModel
, 也就是 視圖和數據的管理者, 它管理着咱們的數據 到 視圖變化的工做,換到Vue中 ,它指的就是咱們的當前的Vue實例
, Model數據 和 View 視圖通訊的一個橋樑
**react
數據驅動視圖
, 數據變化 =>視圖更新<!-- 視圖 --> <template> <div>{{ message }}</div> </template> <script> // Model 普通數據對象 export default { data () { return { message: 'Hello World' } } } </script> <style> </style>
接下里,咱們來重點研究MVVM的原理及實現方式,Vuejs官網給出了MVVM的原理方式正則表達式
Vue文檔說明算法
經過上面的文檔咱們能夠發現, Vue的響應式原理(MVVM)實際上就是下面這段話:數組
當你把一個普通的 JavaScript 對象傳入 Vue 實例做爲
data
選項,Vue 將遍歷此對象全部的屬性,並使用Object.defineProperty
把這些屬性所有轉爲 getter/setter。Object.defineProperty
是 ES5 中一個沒法 shim 的特性,這也就是 Vue 不支持 IE8 以及更低版本瀏覽器的緣由。瀏覽器
從上面的表述中,咱們發現了幾個關鍵詞, Object.defineProperty
getter/setter
app
什麼是 Object.defineProperty?框架
定義:Object.defineProperty()
方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。
語法: Object.defineProperty(obj, prop, descriptor)
參數: obj => 要在其上定義屬性的對象。
prop => 要新增或者修改的屬性名
descriptor => 將被定義或修改的屬性描述符。
返回值 : 被傳遞給函數的對象。 也就是 傳入的obj對象
經過上面的筆記 咱們來看下 有哪些參數 須要學習
obj 就是一個對象 能夠 new Object() 也能夠 {}
prop 就是屬性名 也就是一個字符串
descriptor 描述符是什麼 ? 有哪些屬性
對象裏目前存在的屬性描述符有兩種主要形式:數據描述符
和存取描述符
。數據描述符
是一個具備值的屬性,該值多是可寫的,也可能不是可寫的。存取描述符
是由getter-setter函數對描述的屬性。描述符必須是這兩種形式之一;不能同時是二者
。
上面是官方描述 ,它告訴咱們 defineProterty設計上有**
兩種模式
存在,一種數據描述
, 一種存取描述
**描述符必須是這兩個中的一個 ,不能同時是二者, 也就是
一山不容二虎
, 也不能一山兩虎都無
咱們寫一個最簡單的 **數據描述符
**的例子
var obj = { name: '小明' } var o = Object.defineProperty(obj, 'weight', { value: '280kg' }) console.log(o)
接下來進行詳細分析
數據描述符
模式數據描述符有哪些屬性?
value
=>該屬性對應的值。能夠是任何有效的 JavaScript 值(數值,對象,函數等)。默認爲 unfinedwritable
=> 當且僅當該屬性的writable爲true時,value才能被賦值運算符改變。默認爲 false。就這兩個 ? 還有嗎 ?
configurable
=> 當且僅當該屬性的 configurable 爲 true 時,該屬性描述符
纔可以被改變,同時該屬性也能從對應的對象上被刪除。默認爲 false。enumerable
=> 當且僅當該屬性的enumerable
爲true
時,該屬性纔可以出如今對象的枚舉屬性中。默認爲 false。爲何
configurable
和enumerable
不一樣時 和 value 還有 writable一塊兒寫呢 ?
由於這兩個屬性不但能夠在數據描述符裏出現 還能夠在 存取描述符裏出現
咱們經過writeable 和 value屬性來寫一個 可寫的屬性 和不寫的屬性
var obj = { name: '小明' } Object.defineProperty(obj, 'money', { value: "10k" // 薪水 此時薪水是不可改的 }) Object.defineProperty(obj, '小明', { value: '150斤', // 給一萬根頭髮 writable: true }) obj.money = '20k' obj.weight = '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 其實就是咱們最多見的 讀取值 和設置值得方法 this.name 讀取值 this.name = '張三'
讀取值得時候 調用 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 person = { name: '曹揚' } var name = '小喬' Object.defineProperty(person, 'wife', { enumerable: true, //表示能夠遍歷到新增的屬性 // 存取描述符 get (){ return name // 返回 wife的屬性 }, set (value){ name = value } }) console.log(person.wife) person.wife = '大喬' // 存取描述符的時候 不須要 value經過wriable來控制 console.log(person.wife) for(var item in person) { console.log(item) }
數據描述符 wriable 只對 數據描述的時候 value進行控制,不能和存取描述符一塊兒寫
經過兩個小節,學習了 defineProperty的基本使用, 接下里咱們要經過defineProperty模擬 Vue實例化的效果
Vue實例化的時候, 咱們明明給data賦值了數據,可是卻能夠經過 **vm實例.屬性
**進行訪問和設置
怎麼作的 ?
var vm = new Vue({ data: { name: '張三' } }) vm.name = '李四'
實際上這就是 經過 Object.defineProperty實現的
var person = { name: '小芳' } var vm = {} // vm對象 // Object.defineProperty 存取描述符 Object.defineProperty(vm, 'name', { // 存取描述符 get /set get () { return person.name //返回person同屬性的值 }, set (value) { debugger // 也要把person中的值給該了 person.name = value } }) console.log(vm.name) // 獲取值 => get方法 vm.name = '曲泡麪' // 調用了set方法 console.log(vm.name)
上面代碼中,咱們實現了 vm中的數據代理了 person中的name 直接改vm就是改person
總結
: 咱們在 set和get的存取描述符中 代理了 person中的數據,
MVVM => 數據代理 => Object.defineProperty =>存取描述符get/set => 代理數據
MVVM不但要獲取這些數據,而且將這些數據 進行 響應式的更新到DOM中, 也就是 數據變化時,咱們要把數據**反映
**到視圖上
經過調試咱們發現,咱們是能夠在set函數裏面監聽到數據的變化的,只須要在數據變化的時候, 通知對應的視圖來更新就能夠了
那麼 怎麼通知 ? 用什麼技術來作 ? 下一小節中咱們將帶來發布訂閱模式
發佈訂閱模式爲什麼物?
其實咱們早已用過不少遍, 發佈 /訂閱 即 有人**發佈消息
**, 有人 訂閱消息
,到了 數據層面 就是 多 => 多
即 A程序 能夠觸發多個消息 也能夠訂閱 多個消息
在vue項目中, 曾經 用過一個**
eventBus
** 就是發佈訂閱模式的體現
這個模式咱們拿來作什麼?
上個小節,咱們已經可以捕捉數據的變化,接下來,咱們就要嘗試在數據變化的時候經過 發佈訂閱這個模式 來改變咱們的視圖
咱們先寫出這個發佈訂閱核心代碼的幾個要素
首先,咱們但願 能夠經過實例化 獲得 發佈訂閱對象
發佈消息 $emit
訂閱消息 $on
根據上述思想,咱們獲得以下代碼
// 建立一個構造函數 function Events () {} // 訂閱消息 監聽消息 Events.prototype.$on = function() {} // 發佈消息 Events.prototype.$emit = function (){}
<button onclick="emitEvent()">觸發事件</button> <script> // 建立一個構造函數 function Events () { // 構造函數 // 開闢一個空間 只對當前實例有效 this.subs = {} // 用來存儲 監聽的事件名和回調函數 { 鍵(事件名): [回調函數1, 回調函數2 ...] 值(回調函數) } } // 訂閱消息 監聽消息 eventName事件名, fn 是該事件觸發時 應該觸發的回調函數 Events.prototype.$on = function(eventName,fn) { // 事件名 => 回調函數 => 觸發某個事件的時候 找到這個事件對應的回調函數 而且執行 // if(this.subs[eventName]) { // this.subs[eventName].push(fn) // }else { // this.subs[eventName] = [fn] // } this.subs[eventName] = this.subs[eventName] || [] this.subs[eventName].push(fn) } // 發佈消息 第一個參數必定是eventName(要觸發的事件名) ...params 表明 eventName以後 全部的參數 Events.prototype.$emit = function (eventName, ...params ) { // 拿到了事件名 應該去咱們的開闢的空間裏面 找有沒有回調函數 if(this.subs[eventName]) { // 有人監聽你的事件 // 調用別人的回調函數 this.subs[eventName].forEach(fn => { // 改變this指向 // fn(...params) // 調用該回調函數 而且傳遞參數 // 三種方式 改變回調函數裏的this指向 // fn.apply(this, [...params]) // apply 參數 [參數列表] // fn.call(this, ...params) // 若干參數 fn.bind(this, ...params)() // bind用法 bind並不會執行函數 而是直接將函數this改變 }); } } var event = new Events() // 實例化 // 開啓一個監聽 event.$on("changeName", function(a,b,c, d, e) { console.log(this) alert(a + '-' +b +'-'+ c + '-'+ d +'-'+ e) }) // 監聽一個事件 // 調用觸發方法 var emitEvent = function () { event.$emit("changeName", 1,2,3,4,5) } </script>
這裏用到了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 是全部的節點 children指的 是全部的元素 => nodeType =1 的節點
全部的子節點都放在 childNodes 這個屬性下,childNodes是僞數組 => 僞數組不具備數組方法. 有length屬性
全部標籤的屬性集合是什麼?
attributes => 放置了全部的屬性
分析DOM對象作什麼呢? 咱們前面準備的數據捕獲和 發佈訂閱就是爲了來更新DOM的
接下來咱們開始手寫一個MVVM示例
手寫一個vuejs 的簡易版 Object.defineProperty => 新增屬性 .修改屬性 數據代理
發佈訂閱 => 發佈事件 訂閱事件
Dom => 更新視圖
挑戰來了,咱們要手寫 一個簡易的**
vuejs
**, 提高咱們自身的技術實力.
咱們要實現mvvm的構造函數
構造函數 模仿vuejs 分別有 data /el
data最終被代理給當前的vm實例, 便可以經過 vm訪問,也能夠經過 this.$data訪問
// 手寫一個mvvm 簡易版的vuejs // options就是選項 全部vue屬性都帶$ function Vue (options) { this.$options = options // 放置選項 this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el // 將dom對象賦值給$el 和官方vuejs保持一致 this.$data = options.data || {} // 數據代理 但願 vm可以代理 $data的數據 // 但願 vm.name 就是$data.name this.$proxyData() // 代理數據 } // 數據代理好的方法 Vue.prototype.$proxyData = function () { // this 指的就是 當前的實例 // key 就是 data數據中的每個key Object.keys(this.$data).forEach(key => { Object.defineProperty(this, key, { // 存取描述符 get () { return this.$data[key] // 返回$data中的數據 }, // 設置數據時 須要 將 值設置給 $data的值 並且要判斷設置以前數據是否相等 set (value) { // value是新值 若是新值等於舊值 就不必再設置了 if (this.$data[key] === value ) return this.$data[key] = value // 若是不等再設置值 } }) }) } var vm = new Vue({ el: '#app', // 還有多是其餘選擇器 還有多是dom對象 data: { name: '呂布', wife: '貂蟬' } }) vm.wife = '西施' vm.name = '小明' console.log(vm.name)
OK,接下來這一步很是關鍵,咱們要作**
數據劫持
**, 劫持誰? 爲何要劫持?
上小節代碼中, 咱們能夠經過 vm.name= '值' 也能夠經過 vm.$data.name = '值', 那麼在哪裏捕捉數據的變化呢?
不管是 this.data 仍是 this.$data 改的都是$data的數據,因此咱們須要對 $data的數據進行**
劫持
**, 也就是監聽它的set
數據劫持意味着 : 咱們要監控MVVM中的 Model的數據層的變化
// 數據劫持 Vue.prototype.$observer = function () { // 要劫持誰 ? $data // 遍歷 $data中的全部key Object.keys(this.$data).forEach(key => { // 劫持 =>劫持數據的變化 -> 監聽 data中的數據的變化 => set方法 // obj / prop / desciptor let value = this.$data[key] // 從新開闢一個空間 value的空間 Object.defineProperty(this.$data, key, { // 描述 => 描述符有幾種 ? 數據描述符(value,writable) 存取描述符 (get/set) get () { return value }, set (newValue) { if(newValue === value) return value = newValue // 一旦進入set方法 表示 MVVM中的 M 發生了變化 data變化了 // MVVVM => Model => 發佈訂閱模式 => 更新Dom視圖 } }) }) }
在構造函數中完成對數據的劫持
function Vue (options) { this.$options = options // 放置選項 this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el // 將dom對象賦值給$el 和官方vuejs保持一致 this.$data = options.data || {} // 數據代理 但願 vm可以代理 $data的數據 // 但願 vm.name 就是$data.name this.$proxyData() // 代理數據 把$data中數據 代理給vm實例 this.$observer() // 數據劫持 劫持 $data中的數據變化 }
代理 : $data中的全部數據都代理給了 this
劫持 : $data中的數據變化
如今咱們基本實現了 實例化數據,而且完成了對數據的代理和劫持,接下來咱們須要實現幾個方法
數據變化時 => 根據最新數據把模板轉化成最新的對象
判斷節點是不是文本節點
判斷節點是不是 元素節點
判斷是不是指令 v-model / v-text
處理元素節點
處理文本節點
因此咱們定義下面幾個方法
// 編譯模板的一個總方法 構造函數執行時執行 Vue.prototype.$compile = function () { } // 處理文本節點 nodeType =3 Vue.prototype.$compileTextNode = function () {} // 處理元素節點 nodeType = 1的時候是元素節點 Vue.prototype.$compileElementNode = function () {} // 判斷一個節點是不是文本節點 Vue.prototype.$isTextNode = function () {} // 判斷 一個節點是不是元素節點 Vue.prototype.$isElementNode = function () {} // 判斷一個屬性是不是指令 全部的指令都以 v-爲開頭 Vue.prototype.$isDirective = function () {}
咱們已經經過構造函數拿到了$el,也就是頁面的dom元素,接下來咱們能夠實現 一下編譯的基本邏輯
注意: 文本節點就再也不有子節點了 由於文本就是最終的體現
元素節點 必定還有子節點
// 編譯模板 // 編譯模板的一個總方法 構造函數執行時執行 // rootnode是傳入本次循環的根節點 => 找rootnode下全部的子節點 => 子節點 => 子節點=> 子節點 > 子節點 ... 找到沒有子節點爲止 Vue.prototype.$compile = function (rootnode) { let nodes = Array.from(rootnode.childNodes) // 是一個僞數組 將僞數組轉成真數組 nodes.forEach(node => { // 循環每一個節點 判斷節點類型 若是你是文本節點 就要用文本節點的處理方式 若是元素節點就要元素節點的處理方式 if(this.$isTextNode(node)) { // 若是是文本節點 this.$compileTextNode(node) // 處理文本節點 當前的node再也不有 子節點 沒有必要繼續找了 } if(this.$isElementNode(node)) { // 若是是元素節點 this.$compileElementNode(node) // 處理元素節點 // 若是是元素節點 下面必定還有子節點 只有文本節點纔是終點 // 遞歸了 => 自身調用自身 this.$compile(node) // 傳參數 保證一層一層找下去 找到 node.chidNodes的長度爲0的時候 自動中止 // 能夠保證 把 $el下的全部節點都遍歷一遍 } }) } // 處理文本節點 nodeType =3 Vue.prototype.$compileTextNode = function () {} // 處理元素節點 nodeType = 1的時候是元素節點 Vue.prototype.$compileElementNode = function () {} // 判斷一個節點是不是文本節點 nodeType ===3 Vue.prototype.$isTextNode = function (node) { return node.nodeType === 3 // 表示就是文本節點 } // 判斷 一個節點是不是元素節點 Vue.prototype.$isElementNode = function (node) { return node.nodeType === 1 // 表示就是元素節點 }
上述代碼的基本邏輯就是 碰到 文本節點就用文本節點的方法處理 碰到元素節點 用元素節點的方法處理
若是碰到元素節點,就表示**
還沒完
** 還須要調用下一級的查找
// 處理文本節點 nodeType =3 Vue.prototype.$compileTextNode = function (node) { // console.log(node.textContent) // 拿到文本節點內容以後 要作什麼事情 {{ name }} => 真實的值 // 正則表達式 const text = node.textContent // 拿到文本節點的內容 要看一看 有沒有插值表達式 const reg = /\{\{(.+?)\}\}/g // 將匹配全部的 {{ 未知內容 }} if (reg.test(text)) { // 若是能匹配 說明 此時這個文本里有插值表達式 // 表示 上一個匹配的正則表達式的值 const key = RegExp.$1.trim() // name屬性 => 取name的值 $1取的是第一個的key node.textContent = text.replace(reg, this[key] ) // 獲取屬性的值 而且替換 文本節點中的插值表達式 } }
提示: 實際開發時正則不須要記 可是要能看懂
// 處理元素節點 nodeType = 1的時候是元素節點 Vue.prototype.$compileElementNode = function (node) { // 指令 v-text v-model => 數據變化 => 視圖更新 更新數據變化 // v-text = '值' => innerText上 textContent // 拿到該node全部的屬性 let attrs = Array.from(node.attributes) // 把全部的屬性轉化成數組 // 循環每一個屬性 屬性是否帶 v- 若是帶 v- 表示指令 attrs.forEach(attr => { if (this.$isDirective( attr.name)) { // 判斷指令類型 if(attr.name === 'v-text') { // v-text的指令的含義是 v-text後面的表達的值 做用在 元素的innerText或者textContent上 node.textContent = this[attr.value] // 賦值 } if(attr.name === 'v-model') { // 表示我要對當前節點進行雙向綁定 node.value = this[attr.value] // v-model要給value賦值 並非textContent } } // 若是以 v-開頭表示 就是指令 }) }
目前響應式數據有了, 編譯模板也有了, 咱們須要在數據變化的時候編譯模板
以前講了, 這一步須要 經過發佈訂閱來作 ,因此咱們在Vue的基礎上實現發佈訂閱
// 手寫一個mvvm 簡易版的vuejs // options就是選項 全部vue屬性都帶$ function Vue (options) { this.subs = {} // 事件管理器 this.$options = options // 放置選項 this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el // 將dom對象賦值給$el 和官方vuejs保持一致 this.$data = options.data || {} // 數據代理 但願 vm可以代理 $data的數據 // 但願 vm.name 就是$data.name this.$proxyData() // 代理數據 把$data中數據 代理給vm實例 this.$observer() // 數據劫持 劫持 $data中的數據變化 this.$compile(this.$el) // 模板第一次編譯渲染 遞歸的要求 這裏必須傳入參數 // 遞歸是一種簡單的算法 => 通常用在處理樹形數據,嵌套數據 中國/北京/海淀/中關村/知春路/海淀橋/982/人 // 遞歸其實就是函數自身調用自身 => 傳入下一次遞歸的條件 => 兩次遞歸條件同樣 => 死循環了 } // Vue的發佈訂閱管理器 $on $emit // 監聽事件 Vue.prototype.$on = function (eventName, fn) { // 事件名 => 回調函數 => 觸發某個事件的時候 找到這個事件對應的回調函數 而且執行 // if(this.subs[eventName]) { // this.subs[eventName].push(fn) // }else { // this.subs[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 => { // 改變this指向 // fn(...params) // 調用該回調函數 而且傳遞參數 // 三種方式 改變回調函數裏的this指向 // fn.apply(this, [...params]) // apply 參數 [參數列表] // fn.call(this, ...params) // 若干參數 fn.bind(this, ...params)() // bind用法 bind並不會執行函數 而是直接將函數this改變 }); } }
如今萬事俱備,只欠東風
咱們的數據代理,數據劫持,模板編譯, 事件發佈訂閱通通搞定 如今只須要在數據變化時 ,經過事件發佈,而後
通知 數據進行編譯便可
// 數據劫持 Vue.prototype.$observer = function () { // 要劫持誰 ? $data // 遍歷 $data中的全部key Object.keys(this.$data).forEach(key => { // 劫持 =>劫持數據的變化 -> 監聽 data中的數據的變化 => set方法 // obj / prop / desciptor let value = this.$data[key] // 從新開闢一個空間 value的空間 Object.defineProperty(this.$data, key, { // 描述 => 描述符有幾種 ? 數據描述符(value,writable) 存取描述符 (get/set) get () { return value }, set: (newValue) => { if(newValue === value) return value = newValue // 一旦進入set方法 表示 MVVM中的 M 發生了變化 data變化了 // MVVVM => Model => 發佈訂閱模式 => 更新Dom視圖 // 總體編譯只執行一次 經過發佈訂閱模式來作 觸發一個事件 視圖層監聽一個事件 // 觸發一個事件 this.$emit(key) // 把屬性當成事件名 觸發一個事件 } }) }) }
監聽數據改變
// 編譯模板 數據發生變化 => 模板數據更新到最新 // 編譯模板的一個總方法 構造函數執行時執行 // rootnode是傳入本次循環的根節點 => 找rootnode下全部的子節點 => 子節點 => 子節點=> 子節點 > 子節點 ... 找到沒有子節點爲止 Vue.prototype.$compile = function (rootnode) { let nodes = Array.from(rootnode.childNodes) // 是一個僞數組 將僞數組轉成真數組 nodes.forEach(node => { // 循環每一個節點 判斷節點類型 若是你是文本節點 就要用文本節點的處理方式 若是元素節點就要元素節點的處理方式 if(this.$isTextNode(node)) { // 若是是文本節點 this.$compileTextNode(node) // 處理文本節點 當前的node再也不有 子節點 沒有必要繼續找了 } if(this.$isElementNode(node)) { // 若是是元素節點 this.$compileElementNode(node) // 處理元素節點 // 若是是元素節點 下面必定還有子節點 只有文本節點纔是終點 // 遞歸了 => 自身調用自身 this.$compile(node) // 傳參數 保證一層一層找下去 找到 node.chidNodes的長度爲0的時候 自動中止 // 能夠保證 把 $el下的全部節點都遍歷一遍 } }) } // 處理文本節點 nodeType =3 Vue.prototype.$compileTextNode = function (node) { // console.log(node.textContent) // 拿到文本節點內容以後 要作什麼事情 {{ name }} => 真實的值 // 正則表達式 const text = node.textContent // 拿到文本節點的內容 要看一看 有沒有插值表達式 const reg = /\{\{(.+?)\}\}/g // 將匹配全部的 {{ 未知內容 }} if (reg.test(text)) { // 若是能匹配 說明 此時這個文本里有插值表達式 // 表示 上一個匹配的正則表達式的值 const key = RegExp.$1.trim() // name屬性 => 取name的值 $1取的是第一個的key node.textContent = text.replace(reg, this[key] ) // 獲取屬性的值 而且替換 文本節點中的插值表達式 this.$on(key, () => { // 若是 key這個屬性所表明的值發生了變化 回調函數裏更新視圖 node.textContent = text.replace(reg, this[key] ) // 把原來的帶大括號的內容替換成最新值 賦值給textContent }) } } // 處理元素節點 nodeType = 1的時候是元素節點 Vue.prototype.$compileElementNode = function (node) { // 指令 v-text v-model => 數據變化 => 視圖更新 更新數據變化 // v-text = '值' => innerText上 textContent // 拿到該node全部的屬性 let attrs = Array.from(node.attributes) // 把全部的屬性轉化成數組 // 循環每一個屬性 屬性是否帶 v- 若是帶 v- 表示指令 attrs.forEach(attr => { if (this.$isDirective( attr.name)) { // 判斷指令類型 if(attr.name === 'v-text') { // v-text的指令的含義是 v-text後面的表達的值 做用在 元素的innerText或者textContent上 node.textContent = this[attr.value] // 賦值 attr.value => v-text="name" this.$on(attr.value, () => { node.textContent = this[attr.value] //此時數據已經更新 }) } if(attr.name === 'v-model') { // 表示我要對當前節點進行雙向綁定 node.value = this[attr.value] // v-model要給value賦值 並非textContent this.$on(attr.value, () => { node.value = this[attr.value] //此時數據已經更新 }) } } // 若是以 v-開頭表示 就是指令 }) }
而後咱們寫個例子來測試一把
最後咱們但願實現雙向綁定,即視圖改變時 數據同時變化
// 處理元素節點 nodeType = 1的時候是元素節點 Vue.prototype.$compileElementNode = function (node) { // 指令 v-text v-model => 數據變化 => 視圖更新 更新數據變化 // v-text = '值' => innerText上 textContent // 拿到該node全部的屬性 let attrs = Array.from(node.attributes) // 把全部的屬性轉化成數組 // 循環每一個屬性 屬性是否帶 v- 若是帶 v- 表示指令 attrs.forEach(attr => { if (this.$isDirective( attr.name)) { // 判斷指令類型 if(attr.name === 'v-text') { // v-text的指令的含義是 v-text後面的表達的值 做用在 元素的innerText或者textContent上 node.textContent = this[attr.value] // 賦值 attr.value => v-text="name" this.$on(attr.value, () => { node.textContent = this[attr.value] //此時數據已經更新 }) } if(attr.name === 'v-model') { // 表示我要對當前節點進行雙向綁定 node.value = this[attr.value] // v-model要給value賦值 並非textContent this.$on(attr.value, () => { node.value = this[attr.value] //此時數據已經更新 }) node.oninput = () => { // 須要把當前最新的節點的值 賦值給 自己的數據 this[attr.value] = node.value // 視圖 發生 => 數據發生變化 } // 若是一個元素綁定了v-model指令 應該監聽這個元素的值改變事件 } } // 若是以 v-開頭表示 就是指令 }) }