MVVM原理- 2 -簡單版vue實現

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <p>{{ name }}</p>
        <div v-text="name"></div>
        <input  v-model="name" type="text">

    </div>
    <script>
        // 手寫一個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.prototype.$proxyData = function () {
            // this 指的就是 當前的實例
            // key 就是 data數據中的每個key
            Object.keys(this.$data).forEach(key => {
                // 代理數據 讓vm實例代理$data中的數據
                Object.defineProperty(this, key, {
                    // 存取描述符
                    get () {
                       return this.$data[key]  // 返回$data中的數據
                    },
                    // 設置數據時 須要 將 值設置給 $data的值 並且要判斷設置以前數據是否相等
                    set (value) {
                        // value是新值 若是新值等於舊值 就不必再設置了
                        if (this.$data[key] === value ) return
                        this.$data[key] = value // 若是不等再設置值
                    }
                })
            })
            
        }
        // 數據劫持
        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]   //此時數據已經更新
                      })
                      node.oninput = () => {
                        //   須要把當前最新的節點的值 賦值給 自己的數據
                        this[attr.value] =  node.value  // 視圖 發生 => 數據發生變化
                      }  // 若是一個元素綁定了v-model指令 應該監聽這個元素的值改變事件
                    }

               } // 若是以 v-開頭表示 就是指令
            })
       }
    
      //  判斷一個節點是不是文本節點 nodeType ===3
       Vue.prototype.$isTextNode = function (node) {
          return node.nodeType === 3  // 表示就是文本節點
       }
      // 判斷 一個節點是不是元素節點

       Vue.prototype.$isElementNode = function (node) {
        return node.nodeType === 1  // 表示就是元素節點
       }
       
      // 判斷一個屬性是不是指令 全部的指令都以 v-爲開頭 
      Vue.prototype.$isDirective = function (attrname) {
         return attrname.startsWith('v-')
      }
    
      // 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改變
            });
          }
      }

        var vm =  new Vue({
            el: '#app', // 還有多是其餘選擇器 還有多是dom對象
            data: {
                name: '呂布',
                wife: '貂蟬'
            }
        })
    </script>
</body>
</html>
相關文章
相關標籤/搜索