實現一個簡易的Vue雙向數據綁定

Vue數據綁定

主要是經過數據劫持和發佈訂閱一塊兒實現的html

  • 雙向數據綁定 數據更新時,能夠更新視圖 視圖的數據更新是,能夠反向更新模型

組成說明

  • Observe監聽器 劫持數據, 感知數據變化, 發出通知給訂閱者, 在get中將訂閱者添加到訂閱器
  • Dep消息訂閱器 存儲訂閱者, 通知訂閱者調用更新函數
  • 訂閱者Wather取出模型值,更新視圖
  • 解析器Compile 解析指令, 更新模板數據, 初始化視圖, 實例化一個訂閱者, 將更新函數綁定到訂閱者上, 能夠在接收通知二次更新視圖, 對於v-model還須要監聽input事件,實現視圖到模型的數據流動

基本結構

HTML模板

  <div id="app">
    <form>
      <input type="text" v-model="username">
    </form>
    <p v-bind="username"></p>
  </div>
複製代碼
  • 一個根節點#app
  • 表單元素,裏面包含input, 使用v-model指令綁定數據username
  • p元素上使用v-bind綁定數username

MyVue類

簡單的模擬Vue類node

將實例化時的選項options, 數據options.data進行保存 此外,經過options.el獲取dom元素,存儲到$elbash

    class MyVue {
      constructor(options) {
        this.$options = options
        this.$el = document.querySelector(this.$options.el)
        this.$data = options.data
      }
    }
複製代碼

實例化MyVue

實例化一個MyVue,傳遞選項進去,選項中指定綁定的元素el和數據對象dataapp

    const myVm = new MyVue({
      el: '#app',
      data: {
        username: 'LastStarDust'
      }
    })
複製代碼

Observe監聽器實現

劫持數據是爲了修改數據的時候能夠感知, 發出通知, 執行更新視圖操做dom

    class MyVue {
      constructor(options) {
        // ...
        // 監視數據的屬性
        this.observable(this.$data)
      }
      // 遞歸遍歷數據對象的全部屬性, 進行數據屬性的劫持 { username: 'LastStarDust' }
      observable(obj) {
        // obj爲空或者不是對象, 不作任何操做
        const isEmpty = !obj || typeof obj !== 'object'
        if(isEmpty) {
          return
        }

        // ['username']
        const keys = Object.keys(obj)
        keys.forEach(key => {
          // 若是屬性值是對象,遞歸調用
          let val = obj[key]
          if(typeof val === 'object') {
            this.observable(val)
          }
          // this.defineReactive(this.$data'username''LastStarDust')
          this.defineReactive(obj, key, val)
        })

        return obj
      }

      // 數據劫持,修改屬性的get和set方法
      defineReactive(obj, key, val) {
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get() {
            console.log(`取出${key}屬性值: 值爲${val}`)
            return val
          },
          set(newVal) {
            // 沒有發生變化, 不作更新
            if(newVal === val) {
              return
            }
            console.log(`更新屬性${key}的值爲: ${newVal}`)
            val = newVal
          }
        })
      }
    }
複製代碼

Dep消息訂閱器

存儲訂閱者, 收到通知時,取出訂閱者,調用訂閱者的update方法函數

    // 定義消息訂閱器
    class Dep {
      // 靜態屬性 Dep.target,這是一個全局惟一 的Watcher,由於在同一時間只能有一個全局的 Watcher
      static target = null
      constructor() {
        // 存儲訂閱者
        this.subs = []
      }
      // 添加訂閱者
      add(sub) {
        this.subs.push(sub)
      }
      // 通知
      notify() {
        this.subs.forEach(sub => {
          // 調用訂閱者的update方法
          sub.update()
        })
      }
    }
複製代碼

將消息訂閱器添加到數據劫持過程當中

爲每個屬性添加訂閱者ui

      defineReactive(obj, key, val) {
        const dep = new Dep()
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get() {
            // 會在初始化時, 觸發屬性get()方法,來到這裏Dep.target有值,將其做爲訂閱者存儲起來,在觸發屬性的set()方法時,調用notify方法
            if(Dep.target) {
              dep.add(Dep.target)
            }
            console.log(`取出${key}屬性值: 值爲${val}`)
            return val
          },
          set(newVal) {
            // 沒有發生變化, 不作更新
            if(newVal === val) {
              return
            }
            console.log(`更新屬性${key}的值爲: ${newVal}`)

            val = newVal
            dep.notify()
          }
        })
      }
複製代碼

訂閱者Wather

模型中取出數據並更新視圖this

    // 定義訂閱者類
    class Wather {
      constructor(vm, exp, cb) {
        this.vm = vm // vm實例
        this.exp = exp // 指令對應的字符串值, 如v-model="username", exp至關於"username"
        this.cb = cb // 回到函數 更新視圖時調用
        this.value = this.get() // 將本身添加到消息訂閱器Dep中
      }

      get() {
        // 將當前訂閱者做爲全局惟一的Wather,添加到Dep.target上
        Dep.target = this
        // 獲取數據,觸發屬性的getter方法
        const value = this.vm.$data[this.exp]
        // 在執行添加到消息訂閱Dep後, 重置Dep.target
        Dep.target = null
        return value
      }

      // 執行更新
      update() {
        this.run()
      }

      run() {
        // 從Model模型中取出屬性值
        const newVal = this.vm.$data[this.exp]
        const oldVal = this.value
        if(newVal === oldVal) {
          return false
        }
        // 執行回調函數, 將vm實例,新值,舊值傳遞過去
        this.cb.call(this.vm, newVal, oldVal)
      }
    }
複製代碼

解析器Compile

  • 解析模板指令,並替換模板數據,初始化視圖;
  • 將模板指令對應的節點綁定對應的更新函數,初始化相應的訂閱器;
  • 初始化編譯器, 存儲el對應的dom元素, 存儲vm實例, 調用初始化方法
  • 在初始化方法中, 從根節點開始, 取出根節點的全部子節點, 逐個對節點進行解析
  • 解析節點過程當中
  • 解析指令存在, 取出綁定值, 替換模板數據, 完成首次視圖的初始化
  • 給指令對應的節點綁定更新函數, 並實例化一個訂閱器Wather
  • 對於v-model指令, 監聽'input'事件,實現視圖更新是,去更新模型的數據
    // 定義解析器
    // 解析指令,替換模板數據,初始視圖
    // 模板的指令綁定更新函數, 數據更新時, 更新視圖
    class Compile {
      constructor(el, vm) {
        this.el = el
        this.vm = vm
        this.init(this.el)
      }

      init(el) {
        this.compileEle(el)
      }
      compileEle(ele) {
        const nodes = ele.children
		// 遍歷節點進行解析
        for(const node of nodes) {
		  // 若是有子節點,遞歸調用
          if(node.children && node.children.length !== 0) {
            this.compileEle(node)
          }

          // 指令時v-model而且是標籤是輸入標籤
          const hasVmodel = node.hasAttribute('v-model')
          const isInputTag = ['INPUT''TEXTAREA'].indexOf(node.tagName) !== -1
          if(hasVmodel && isInputTag) {
            const exp = node.getAttribute('v-model')
            const val = this.vm.$data[exp]
            const attr = 'value'
            // 初次模型值推到視圖層,初始化視圖
            this.modelToView(node, val, attr)
            // 實例化一個訂閱者, 將更新函數綁定到訂閱者上, 將來數據更新,能夠更新視圖
            new Wather(this.vm, exp, (newVal)=> {
              this.modelToView(node, newVal, attr)
            })

            // 監聽視圖的改變
            node.addEventListener('input', (e) => {
              this.viewToModel(exp, e.target.value)
            })
          }
		 
		  // 指令時v-bind
          if(node.hasAttribute('v-bind')) {
            const exp = node.getAttribute('v-bind')
            const val = this.vm.$data[exp]
            const attr = 'innerHTML'
            // 初次模型值推到視圖層,初始化視圖
            this.modelToView(node, val, attr)
            // 實例化一個訂閱者, 將更新函數綁定到訂閱者上, 將來數據更新,能夠更新視圖
            new Wather(this.vm, exp, (newVal)=> {
              this.modelToView(node, newVal, attr)
            })
          }
        }
      }
      // 將模型值更新到視圖
      modelToView(node, val, attr) {
        node[attr] = val
      }
      // 將視圖值更新到模型上
      viewToModel(exp, val) {
        this.vm.$data[exp] = val
      }
    }
複製代碼

完整代碼

<!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">
    <form>
      <input type="text" v-model="username">
    </form>
    <div>
      <span v-bind="username"></span>
    </div>
    <p v-bind="username"></p>
  </div>
  <script>

    class MyVue {
      constructor(options) {
        this.$options = options
        this.$el = document.querySelector(this.$options.el)
        this.$data = options.data

        // 監視數據的屬性
        this.observable(this.$data)

        // 編譯節點
        new Compile(this.$el, this)
      }
      // 遞歸遍歷數據對象的全部屬性, 進行數據屬性的劫持 { username: 'LastStarDust' }
      observable(obj) {
        // obj爲空或者不是對象, 不作任何操做
        const isEmpty = !obj || typeof obj !== 'object'
        if(isEmpty) {
          return
        }

        // ['username']
        const keys = Object.keys(obj)
        keys.forEach(key => {
          // 若是屬性值是對象,遞歸調用
          let val = obj[key]
          if(typeof val === 'object') {
            this.observable(val)
          }
          // this.defineReactive(this.$data'username''LastStarDust')
          this.defineReactive(obj, key, val)
        })

        return obj
      }

      // 數據劫持,修改屬性的get和set方法
      defineReactive(obj, key, val) {
        const dep = new Dep()
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get() {
            // 會在初始化時, 觸發屬性get()方法,來到這裏Dep.target有值,將其做爲訂閱者存儲起來,在觸發屬性的set()方法時,調用notify方法
            if(Dep.target) {
              dep.add(Dep.target)
            }
            console.log(`取出${key}屬性值: 值爲${val}`)
            return val
          },
          set(newVal) {
            // 沒有發生變化, 不作更新
            if(newVal === val) {
              return
            }
            console.log(`更新屬性${key}的值爲: ${newVal}`)

            val = newVal
            dep.notify()
          }
        })
      }
    }

    // 定義消息訂閱器
    class Dep {
      // 靜態屬性 Dep.target,這是一個全局惟一 的Watcher,由於在同一時間只能有一個全局的 Watcher
      static target = null
      constructor() {
        // 存儲訂閱者
        this.subs = []
      }
      // 添加訂閱者
      add(sub) {
        this.subs.push(sub)
      }
      // 通知
      notify() {
        this.subs.forEach(sub => {
          // 調用訂閱者的update方法
          sub.update()
        })
      }
    }

    // 定義訂閱者類
    class Wather {
      constructor(vm, exp, cb) {
        this.vm = vm // vm實例
        this.exp = exp // 指令對應的字符串值, 如v-model="username", exp至關於"username"
        this.cb = cb // 回到函數 更新視圖時調用
        this.value = this.get() // 將本身添加到消息訂閱器Dep中
      }

      get() {
        // 將當前訂閱者做爲全局惟一的Wather,添加到Dep.target上
        Dep.target = this
        // 獲取數據,觸發屬性的getter方法
        const value = this.vm.$data[this.exp]
        // 在執行添加到消息訂閱Dep後, 重置Dep.target
        Dep.target = null
        return value
      }

      // 執行更新
      update() {
        this.run()
      }

      run() {
        // 從Model模型中取出屬性值
        const newVal = this.vm.$data[this.exp]
        const oldVal = this.value
        if(newVal === oldVal) {
          return false
        }
        // 執行回調函數, 將vm實例,新值,舊值傳遞過去
        this.cb.call(this.vm, newVal, oldVal)
      }
    }

    // 定義解析器
    // 解析指令,替換模板數據,初始視圖
    // 模板的指令綁定更新函數, 數據更新時, 更新視圖
    class Compile {
      constructor(el, vm) {
        this.el = el
        this.vm = vm
        this.init(this.el)
      }

      init(el) {
        this.compileEle(el)
      }
      compileEle(ele) {
        const nodes = ele.children
        for(const node of nodes) {
          if(node.children && node.children.length !== 0) {
            // 遞歸調用, 編譯子節點
            this.compileEle(node)
          }

          // 指令時v-model而且是標籤是輸入標籤
          const hasVmodel = node.hasAttribute('v-model')
          const isInputTag = ['INPUT''TEXTAREA'].indexOf(node.tagName) !== -1
          if(hasVmodel && isInputTag) {
            const exp = node.getAttribute('v-model')
            const val = this.vm.$data[exp]
            const attr = 'value'
            // 初次模型值推到視圖層,初始化視圖
            this.modelToView(node, val, attr)
            // 實例化一個訂閱者, 將更新函數綁定到訂閱者上, 將來數據更新,能夠更新視圖
            new Wather(this.vm, exp, (newVal)=> {
              this.modelToView(node, newVal, attr)
            })

            // 監聽視圖的改變
            node.addEventListener('input', (e) => {
              this.viewToModel(exp, e.target.value)
            })
          }

          if(node.hasAttribute('v-bind')) {
            const exp = node.getAttribute('v-bind')
            const val = this.vm.$data[exp]
            const attr = 'innerHTML'
            // 初次模型值推到視圖層,初始化視圖
            this.modelToView(node, val, attr)
            // 實例化一個訂閱者, 將更新函數綁定到訂閱者上, 將來數據更新,能夠更新視圖
            new Wather(this.vm, exp, (newVal)=> {
              this.modelToView(node, newVal, attr)
            })
          }
        }
      }
      // 將模型值更新到視圖
      modelToView(node, val, attr) {
        node[attr] = val
      }
      // 將視圖值更新到模型上
      viewToModel(exp, val) {
        this.vm.$data[exp] = val
      }
    }

    const myVm = new MyVue({
      el: '#app',
      data: {
        username: 'LastStarDust'
      }
    })

    // console.log(Dep.target)
  </script>
</body>
</html>
複製代碼
相關文章
相關標籤/搜索