VUE MVVM-介紹和演示

MVVM-介紹和演示

  • easyUI
  • knockout
  • smartclient
  • exit.js

MVVM

面試過程當中,面試官必定會問你 描述一下 你所知道的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-響應式原理-Object.defineProperty()-基本使用

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

Vue文檔說明dom

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

當你把一個普通的 JavaScript 對象傳入 Vue 實例做爲 data 選項,Vue 將遍歷此對象全部的屬性,並使用 Object.defineProperty 把這些屬性所有轉爲 getter/setterObject.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)

接下來進行詳細分析

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

數據描述符有哪些屬性?

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

就這兩個 ? 還有嗎 ?

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

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

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

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

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

依然能夠配置哦

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)
      }

Object.defineProperty()-模擬vm對象

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

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 是全部的節點 childer 值的是 全部的元素 =>nodeType =>節點類型

全部的子節點都放在 childNodes 這個屬性下,childNodes是僞數組 => 僞數組不具備數組方法,有length屬性

全部標籤的屬性集合是什麼?

attributes

分析DOM對象作什麼呢? 咱們前面準備的數據捕獲和 發佈訂閱就是爲了來更新DOM的

接下來咱們開始手寫一個MVVM示例

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

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

MVVM實現-數據劫持Observer

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() // 開啓監聽數據
        }

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

如今咱們基本實現了 實例化數據,而且完成了對數據的劫持,接下來咱們須要實現幾個方法

數據變化時 => 根據最新數據把模板轉化成最新的對象

判斷是不是文本節點

判斷是不是 元素節點

判斷是不是指令

處理元素節點

處理文本節點

因此咱們定義下面幾個方法

// 編譯模板
        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) {};

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

咱們已經經過構造函數拿到了$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-");
        };

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

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

// 處理文本節點
      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]) // 若是找到大括號 就替換對應的數據
            }
        }

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

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

// 處理元素節點
        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]
                }
              }
          })
        }

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

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

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

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

如今萬事俱備,只欠東風

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

通知 數據進行編譯便可

// 監聽數據
        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] // 若是找到大括號 就替換對應的數據
                    })
                }
              }
          })
        }

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

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

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

// 處理元素節點
        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
                    }
                }
              }
          })
        }
相關文章
相關標籤/搜索