MVVM原理- 1 - 原理分析

MVVM-介紹和演示

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-響應式原理-Object.defineProperty()-基本使用

接下里,咱們來重點研究MVVM的原理及實現方式,Vuejs官網給出了MVVM的原理方式正則表達式

Vue文檔說明算法

經過上面的文檔咱們能夠發現, Vue的響應式原理(MVVM)實際上就是下面這段話:數組

當你把一個普通的 JavaScript 對象傳入 Vue 實例做爲 data 選項,Vue 將遍歷此對象全部的屬性,並使用 Object.defineProperty 把這些屬性所有轉爲 getter/setterObject.defineProperty 是 ES5 中一個沒法 shim 的特性,這也就是 Vue 不支持 IE8 以及更低版本瀏覽器的緣由。瀏覽器

從上面的表述中,咱們發現了幾個關鍵詞, Object.defineProperty getter/setterapp

什麼是 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)

接下來進行詳細分析

Object.defineProperty()-數據描述符模式

數據描述符有哪些屬性?

  • value =>該屬性對應的值。能夠是任何有效的 JavaScript 值(數值,對象,函數等)。默認爲 unfined
  • writable => 當且僅當該屬性的writable爲true時,value才能被賦值運算符改變。默認爲 false。

就這兩個 ? 還有嗎 ?

  • configurable => 當且僅當該屬性的 configurable 爲 true 時,該屬性描述符纔可以被改變,同時該屬性也能從對應的對象上被刪除。默認爲 false
  • enumerable => 當且僅當該屬性的enumerabletrue時,該屬性纔可以出如今對象的枚舉屬性中。默認爲 false

爲何 configurableenumerable 不一樣時 和 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)
     }

Object.defineProperty()-存取描述符模式

上一小節中,數據描述符 獨有的屬性 是 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)

可是,咱們想要遍歷怎麼辦 ? 注意哦 , 存儲描述符的時候 依然擁有 configurableenumerable屬性,

依然能夠配置哦

// 定義一個對象
        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進行控制,不能和存取描述符一塊兒寫

Object.defineProperty()-模擬vm對象

經過兩個小節,學習了 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

MVVM實現-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 => 更新視圖

MVVM實現-實現Vue的構造函數和數據代理

挑戰來了,咱們要手寫 一個簡易的**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)

MVVM實現-數據劫持Observer

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中的數據變化
        }

MVVM實現-編譯模板Compiler-設計結構

代理 : $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 () {}

MVVM實現-編譯模板Compiler實現基本的框架邏輯

咱們已經經過構造函數拿到了$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  // 表示就是元素節點
       }

上述代碼的基本邏輯就是 碰到 文本節點就用文本節點的方法處理 碰到元素節點 用元素節點的方法處理

若是碰到元素節點,就表示**還沒完** 還須要調用下一級的查找

MVVM實現-編譯模板Compiler-處理文本節點

// 處理文本節點 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] )
                  // 獲取屬性的值 而且替換 文本節點中的插值表達式
            }
       }

提示: 實際開發時正則不須要記 可是要能看懂

MVVM實現-編譯模板Compiler-處理元素節點

// 處理元素節點 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-開頭表示 就是指令
            })
       }

MVVM實現-數據驅動視圖-發佈訂閱管理器

目前響應式數據有了, 編譯模板也有了, 咱們須要在數據變化的時候編譯模板

以前講了, 這一步須要 經過發佈訂閱來作 ,因此咱們在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改變
            });
          }
      }

MVVM實現-數據變化時 驅動視圖變化

如今萬事俱備,只欠東風

咱們的數據代理,數據劫持,模板編譯, 事件發佈訂閱通通搞定 如今只須要在數據變化時 ,經過事件發佈,而後

通知 數據進行編譯便可

// 數據劫持
        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-開頭表示 就是指令
            })
       }

而後咱們寫個例子來測試一把

MVVM實現-視圖變化更新數據

最後咱們但願實現雙向綁定,即視圖改變時 數據同時變化

// 處理元素節點 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-開頭表示 就是指令
            })
       }
相關文章
相關標籤/搜索