手寫VUE1——實現解析k-text、k-html、插值表達式功能

創建KVue類,保存options和data,需要實現響應式、數據代理、編譯模板的功能

class KVue {
  constructor(options) {
    this.$options = options
    this.$data = options.data
    // 響應式處理
    observe(this.$data)

    // 數據代理
    Proxy(this, '$data')

    // 編譯模板
    new Compile(this, options.el)
  }
}
  • 數據代理
// 數據代理函數,將$data中的數據代理到vm上,這樣我們就能使用vm.xx來獲取數據
function Proxy (vm, prop) {
  // 遍歷vm[prop]中的數據,使用object.defineProperty()將每一個數據綁定到vm上
  // 使用vm.xx時,如果是獲取值,則會觸發get,設置值觸發set
  Object.keys(vm[prop]).forEach(key => {
    Object.defineProperty(vm, key, {
      get () {
        return vm[prop][key]
      },
      set (value) {
        vm[prop][key] = value
      }
    })
  })
}
  • 數據響應式處理,和響應式基礎的代碼是一樣的,只是這裏我們創建了一個Observer類來進行數據劫持和響應化
// 1、拿出數組的原型
// 2、克隆數組的原型,不然所有數組都變了
// 3、修改七個方法
// 4、覆蓋需要響應式處理的數組的原型

const arrayProto = Array.prototype
const newProto = Object.create(arrayProto)
const methods = ['push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice']
methods.forEach(method => {
  newProto[method] = function () {
    // 執行這個方法原有的功能
    arrayProto[method].apply(this, arguments)
    // 這裏添加一個更新通知,並重新對arr做響應式處理
    observe(this)
    console.log(method + '執行了!')
  }
})

// 數據劫持構造函數,對所有屬性做響應式處理
class Observer {
  constructor(value) {
    this.value = value
    this.walk(value)
  }
  walk (val) {
    // 判斷是否數組
    if (Array.isArray(val)) {
      val.__proto__ = newProto
      for (let i = 0; i < val.length; i++) {
        defineReactive(val[i])
      }
    } else {
      Object.keys(val).forEach(key => {
        defineReactive(val, key, val[key])
      })
    }
  }
}

// 監控數據,如果是對象或數組需要做響應式處理
function observe (obj) {
  if (!obj || typeof obj !== 'object') {
    return
  }
  obj.__ob__ = new Observer(obj)
}

// 響應式處理數據
function defineReactive (obj, key, val) {
  // 遞歸處理val
  observe(val)
  Object.defineProperties(obj, key, {
    get () {
      return val
    },
    set (newVal) {
      if (newVal !== val) {
        observe(newVal)
        val = newVal
      }
    }
  })
}
  • 編譯模板

編譯模板中主要完成兩個功能:解析指令和插值表達式、每個指令對應的方法

class Compile {
  constructor(vm, el) {
    this.$vm = vm
    this.$el = document.querySelector(el)
    this.compile(this.$el)
  }
  compile(el) {
    // 遍歷當前所有的子節點,判斷它是元素節點還是文本節點,nodeType等於1是html元素,等於3是文本
    const childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      if (node.nodeType === 1) {
        // 元素節點,獲取他的attr,並判斷是否是指令
        this.compileElement(node)
      } else if (node.nodeType === 3) {
        // 文本節點,判斷是否插值表達式{{aaa}}並解析
        this.compileText(node)
      }
      // 子節點還有子節點,則要遞歸處理
      if (node.childNodes) {
        this.compile(node)
      }
    })
  }
  // 編譯html元素,或取其中的指令
  compileElement(node) {
    // 獲取node的所有attr進行遍歷,得到我們需要的指令並進行對應操作
    const nodeAttr = node.attributes
    Array.from(nodeAttr).forEach(attr => {
      // 獲取屬性的名稱和值,根據名稱判斷是否是指令,這裏支持‘k-’開頭的指令
      let attrName = attr.name
      let attrValue = attr.value
      if (attrName.indexOf('k-') === 0) {
        // 如果是指令,執行對應的方法
        const dir = attrName.substring(2)
        this[dir] && this[dir](node, attrValue)
      }
    })
  }
  // 編譯文本,判斷是否插值表達式,並觸發更新函數
  compileText(node) {
    // /\{\{(.*)\}\}/.test(node.textContent)匹配{{}}並把大括號裏面的文本賦值給RegExp.$1
    if (/\{\{(.*)\}\}/.test(node.textContent)) {
      node.textContent = this.$vm[RegExp.$1]
      this.update(node, RegExp.$1, 'text')
    }
  }
  // 更新方法,接收節點、值、指令名稱三個參數
  update(node, exp, dir) {
    const fn = this[dir + 'Updater']
    fn && fn(node, this.$vm[exp])
    // 觸發Watcher去更新視圖
  }
  // k-html對應的方法
  html(node, exp) {
    node.textContent = this.$vm[exp]
    this.update(node, exp, 'html')
  }
  htmlUpdater(node, val) {
    node.innerHTML = val
  }
  // k-text對應的方法
  text(node, exp) {
    node.textContent = this.$vm[exp]
    this.update(node, exp, 'text')
  }
  textUpdater(node, val) {
    node.textContent = val
  }
}
  • 依賴收集

所謂依賴,就是視圖中所使用的data中的某個key。依賴收集就就是給每一個依賴創建一個Watcher來維護他,相同key的Watcher使用一個Dep來管理,當數據變化時,通過這個個Dep統一通知更新。

// 監聽數據的變化,然後更新視圖。接收key和它對應的更新函數
class Watcher {
  constructor(vm, key, fn) {
    this.$vm = vm
    this.$key = key
    this.$updateFn = fn

    // 這裏將dep的target設置爲當前的這個watcher,然後通過觸發這個key的getter,將自己push到key對應的dep中去,實現一個key對應一個dep,一個dep對應多個Watcher
    Dep.target = this
    this.$vm[this.$key]
    Dep.target = null
  }
  // 觸發key的對應更新函數
  update() {
    this.$updateFn.call(this.$vm, this.$vm[this.$key])
  }
}

// 依賴,管理Watcher
class Dep {
  constructor() {
    this.watchers = []
  }
  addDep(watcher) {
    this.watchers.push(watcher)
  }
  // 當key變化的時候,通知這個key對應的所有Watcher更新視圖
  notify() {
    this.watchers.forEach(watcher => {
      watcher.update()
    })
  }

然後我們需要修改一下defineReactive方法,每響應化一個變量,都創建一個dep,這個變量的getter和setter都能訪問到它。

function  (obj, key, val) {
  // 遞歸處理val
  observe(val)

  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      Dep.target && dep.addDep(Dep.target)
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        observe(newVal)
        val = newVal
        // 通知watcher更新視圖
        dep.notify()
      }
    }
  })
}

compile中的update方法也需要創建Watcher,去更新視圖。

 // 更新方法,接收節點、值、指令名稱三個參數
  update(node, exp, dir) {
    const fn = this[dir + 'Updater']
    fn && fn(node, this.$vm[exp])
    // 觸發Watcher去更新視圖
    new Watcher(this.$vm, exp, function(val) {
      fn && fn(node, val)
    })
  }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章