一個極其簡易版的vue.js實現

前言

以前項目中一直在用vue,也邊作邊學摸滾打爬了近一年。對一些基礎原理性的東西有過了解,可是不深刻,例如面試常常問的vue的響應式原理,可能大多數人都能答出來Object.defineProperty進行數據劫持,可是深刻其實現細節,仍是有不少以前沒考慮到的東西,例如依賴收集後如何通知訂閱器,以及訂閱發佈模式如何實現等等。過程當中讀了部分源碼,受益不淺,除此以外,動手去實現它也是個很棒的學習方式,話很少說,看代碼,倉庫地址javascript

實現

vue的更新機制咱們簡單歸納一下就是,先對template進行解析,若檢測到template中使用了data中定義的屬性,則生成一個對應的watcher,經過劫持getter進行依賴(即watcher)收集,收集的內容保存在訂閱器Dep,經過劫持setter作到改變屬性從而通知訂閱器更新,那麼咱們首先要作的就是對屬性進行劫持。
vue2.0中使用的是Object.defineProperty,有傳言說vue 3.0將會使用Proxy來代替Object.defineProperty,其有諸多好處:vue

  • defineProperty不能對數組進行劫持,所以vue的文檔中才會提到只有push、pop等8種方法可以檢測變化,而arr[index] = newValue並不能檢測變化,push等方法能檢測變化也是由於開發者對Array原生方法進行hack實現的。
  • defineProperty只能改變對象的某一個屬性,若須要劫持整個對象,必須遍歷對象,對每一個屬性劫持,所以效率並不高。而Proxy更像是一個代理,它會產生一個新的對象,該對象內部的屬性均以實現劫持。但要注意,某個屬性若也是一個對象類型,須要對該屬性也執行proxy操做才能實現劫持。

Proxy目前來看惟一的缺點就是兼容性可能存在問題,不過無傷大雅,咱們也順應潮流,使用Proxy來實現數據劫持,代碼很簡單:java

/**
 * 接受一個對象,對屬性進行依賴追蹤
 */
function observable(obj) {
  const dep = new Dep()
  
  const proxy = new Proxy(obj, {
    get(target, property) {
      const value = target[property]
      if (value && typeof value === 'object') { // 若屬性爲object,遞歸處理
        target[property] = observable(value)
      }
      if (Dep.target) { // Dep.target指向當前watcher
        dep.addWatcher(Dep.target)
      }
      return target[property]
    },
    set(target, property, value) {
      target[property] = value
      dep.notify() // 通知訂閱器
    }
  })
  return proxy
}

注意該方法須要返回proxy實例,由於只有經過proxy實例訪問屬性才具備劫持效果。咱們能夠看到代碼中有一個Dep,這個東西便是訂閱器,能夠理解爲它維護了一個依賴(watcher)的數組,並實現了一些管理數據的方法諸如addWatcher添加依賴,以及須要提供一個notify方法來遍歷全部的watcher執行其相應的更新函數,一樣代碼很簡單:git

/**
 * 依賴收集器,存放全部的watcher,並提供發佈功能(notify)
 */
class Dep {
  constructor() {
    this.watchers = []
  }
  addWatcher(watcher) { // 添加watcher
    this.watchers.push(watcher)
  }
  notify() { // 通知方法,調用即依次遍歷全部watcher執行更新
    this.watchers.forEach((watcher) => {
      watcher.update()
    })
  }
}

最後咱們來看下watcher,咱們知道watcher即咱們所說的依賴,它是在編譯template的時候,若找到data中聲明的屬性,即會生成一個對應的watcher實例,觸發依賴收集,加入訂閱器。同時還須要提供一個update函數,在觸發notify的時候調用來更新視圖,代碼以下:github

/**
 * watcher即所謂的依賴,監聽具體的某個屬性
 */
class Watcher {
  constructor(proxy, property, cb) {
    this.proxy = proxy
    this.property = property
    this.cb = cb
    this.value = this.get()
  }
  update() { // 執行更新
    const newValue = this.proxy[this.property]
    if (newValue !== this.value && this.cb) { // 對比property新舊值,決定是否更新
      this.cb(newValue)
    }
  }
  get() { // 只在初始化時調用,用於依賴收集
    Dep.target = this // 將自身指向Dep.target,執行完依賴收集再去釋放
    const value = this.proxy[this.property]
    Dep.target = null
    return value
  }
}

至此,響應式原理大體已經成形,接着咱們只要寫一個簡易的模板解析,demo就能跑起來啦。我這邊的實現比較挫,僅僅是經過正則匹配來實現了一個不帶diff的virture dom,純屬娛樂,重點仍是在實現響應式原理上,這邊貼一下代碼:面試

let init = false // 只在初始化時去生成watcher
const eventMap = new Map() // 存放事件
const root = document.getElementById('root') // 根節點

/**
 * 用於將傳入RayActive的vm對象進行代理,可經過this.xx訪問this.data.xx
 * @param {Object} vm 
 * @param {Proxy} proxydata 通過proxy代理的vm.data對象,使this.xx操做也能觸發視圖更新
 */
function vmProxy(vm, proxydata) {
  return new Proxy(vm, {
    get(target, property) {
      return target.data[property] || target.methods[property]
    },
    set(target, property, value) {
      proxydata[property] = value
    }
  })
}

/**
 * 編譯vm,分別對data和render作相應處理
 * @param {Object} vm 須要被編譯的vm對象
 */
function compile(vm) {
  const proxydata = compileData(vm.data)
  compileRender(proxydata, vm.render)
  bindEvents(vm, vmProxy(vm, proxydata))
}

/**
 * 
 * @param {Object} data 須要被編譯的vm中的data對象
 */
function compileData(data) {
  return observable(data)
}

/**
 * 
 * @param {*} render 須要被編譯的render字符串
 * @param {*} proxydata 經proxy轉換過的data
 */
function compileRender(proxydata, render) {
  if (render) {
    const variableRegexp = /\{\{(.*?)\}\}/g
    const variableResult = render.replace(variableRegexp, (a, b) => { // 替換變量爲相應的data值
      if (!init) { // 只在初始化時去生成watcher
        new Watcher(proxydata, b, function() {
          compileRender(proxydata, render)
        })
      }
      return proxydata[b]
    })
    const eventRegexp = /(?<=<.*)@(.*)="(.*?)"(?=.*>)/
    const result = variableResult.replace(eventRegexp, (a, b, c) => { // 爲綁定事件的標籤添加惟一id標識
      const id = Math.random().toString(36).slice(2)
      eventMap.set(id, {
        type: b,
        method: c
      })
      return a + ` id=${id}`
    })
    init = true
    root.innerHTML = result
  }
}

/**
 * 經過root節點作事件代理,綁定模板中聲明的事件
 * @param {*} vm 
 * @param {*} proxyvm 通過proxy代理的vm
 */
function bindEvents(vm, proxyvm) {
  for (let [key, value] of eventMap) {
    root.addEventListener(value.type, (e) => {
      const method = vm.methods[value.method]
      if (method && e.target.id === key) {
        method.apply(proxyvm) // 將vm中methods方法的this指向通過proxy的vm對象
      }
    })
  }
}

/**
 * 可理解爲Vue中的Vue類,使用方式爲new RayActive(vm)
 */
class RayActive {
  constructor(vm) {
    compile(vm)
  }
}

總結

這個簡易實現僅僅是幫助你們學習vue的一些原理性的東西,跟vue比其餘來只是冰山一角。這個代碼還有很大的優化空間,好比執行notify時這裏會通知全部的watcher等等,值得有空去研究一下。同時,咱們能看到訂閱發佈模式帶來的好處。若是不引入訂閱器,那咱們更新dom的代碼得放到setter中去,那麼就耦合了數據劫持與操做dom的邏輯。引入訂閱器,能讓咱們在proxy中僅僅作依賴收集和通知的操做,剩下的各類複雜的或是個性化的邏輯能夠放到watcher中去實現,完美作到了關注點分離。數組

相關文章
相關標籤/搜索