Vue源碼解讀(預):手寫一個簡易版Vue

MVVM 設計模式,是由 MVC、MVP 等設計模式進化而來,M - 數據模型(Model),VM - 視圖模型(ViewModel),V - 視圖層(View)。MVVM 的核心是 ViewModel 層,它就像是一個中轉站(value converter),負責轉換 Model 中的數據對象來讓數據變得更容易管理和使用,該層向上與視圖層進行雙向數據綁定,向下與 Model 層通過接口請求進行數據交互,起呈上啟下作用。如下圖所示:

mvvm

Vue中的MVVM思想

使用 MVVM 設計模式的前端框架很多,其中漸進式框架 Vue 是典型的代表,深得廣大前端開發(fā)者的青睞。

MVVM

從上圖中可以看出MVVM主要分為這么幾個部分:

  • 模板編譯(Compile)
  • 數據劫持(Observer)
  • 訂閱-發(fā)布(Dep)
  • 觀察者(Watcher)

我們來看一個 vue 的實例:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue</title>
</head>
<body>
  <div id="app">
    <p>{{ number }}</p>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <script>
    const VM = new Vue({
      el: '#app',
      data: {
        number: 0
      },
    })
    setInterval(() => {
      VM.number++
    }, 1000)
  </script>
</body>
</html>

我們對原理進行分析一下:

  • 首先 new Vue() 執(zhí)行初始化,通過 Observerdata 上的屬性執(zhí)行響應式處理。也就是 Object.defineProperty 對數據屬性進行劫持。
  • 通過Compile 進行模板編譯,對模板里動態(tài)綁定的數據,使用 data 數據進行初始化。
  • 在模板初始化時,在觸發(fā)Object.defineProperty 內的 getter 時,創(chuàng)建更新函數和 Watcher 類。
  • 同一屬性在模板中可能出現(xiàn)多次,就會創(chuàng)建多個Watcher ,就需要Dep 來統(tǒng)一管理。
  • 當數據發(fā)生變化時,找到屬性對應的 dep ,通知所有 Watcher 執(zhí)行更新函數。

創(chuàng)建Vue類

// 創(chuàng)建 Vue 類
class Vue {
  constructor(options) {
    // 保存選項
    this.$options = options;
    this.$data = options.data;
    // 響應式處理
    observe(this.$data)
    // 將數據代理到實例上
    proxyData(this, '$data')
    // 用數據和元素進行編譯
    new Compiler(options.el, this)
  }
}
// 代理數據的方法
function proxyData (vm, sourceKey) {
  Object.keys(vm[sourceKey]).forEach(key => {
    Object.defineProperty(vm, key, {
      get () {
        return vm[sourceKey][key]
      },
      set (newVal) {
        vm[sourceKey][key] = newVal
      }
    })
  })
}

上面代碼創(chuàng)建了一個 Vue 類和 proxyData 方法,Vue 類接收 options 參數,內部調用 observe 方法對傳入參數 options.data 數據,遞歸進行響應式處理。
使用 proxyData 方法把數據代理到實例上,讓我們獲取和修改數據的時候可以直接通過 thisthis.$data, 如: this.numberthis.$data.number 。
最后使用 Compiler 對模板進行編譯,初始化動態(tài)綁定的數據。

模板編譯(Compile)

// 模板編譯
class Compiler {
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);
    if (this.$el) {
      // 執(zhí)行編譯
      this.compile(this.$el)
    }
  }
  compile (el) {
    // 遍歷 el 樹
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach((node) => {
      if (this.isElementNode(node)) { // 元素節(jié)點
        // 編譯元素節(jié)點的方法
        this.compileElement(node)
      } else { // 文本節(jié)點
        // 編譯文本節(jié)點的方法
        this.compileText(node)
      }
      // 如果還有子節(jié)點,繼續(xù)遞歸
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node);
      }
    })
  }
  // 判斷是否是元素節(jié)點
  isElementNode (node) {
    return node.nodeType === 1
  }
  // 判斷屬性是否為指令
  isDirective (attr) {
    return attr.indexOf('v-') === 0
  }
  // 編譯元素
  compileElement (node) {
    // 遍歷節(jié)點屬性
    const nodeAttrs = node.attributes
    Array.from(nodeAttrs).forEach((attr) => {
      const attrName = attr.name // 屬性名
      const exp = attr.value // 動態(tài)判定的變量名
      //找到 v-xxx 的指令,如 v-text/v-html
      if (this.isDirective(attrName)) {
        let [, dir] = attrName.split("-") // 指令名 如 text/html
        // 調用指令對應得方法
        this[dir] && this[dir](node, exp)
      }
    })
  }
  // 編譯文本
  compileText (node) {
    let txt = node.textContent; // 獲取文本節(jié)點的內容
    let reg = /\{\{(.*)\}\}/; // 創(chuàng)建匹配 {{}} 的正則表達式
    // 如果存在 {{}} 則使用 text 指令的方法
    if (node.nodeType === 3 && reg.test(txt)) {
      this.update(node, RegExp.$1.trim(), 'text')
    }
  }
  update (node, exp, dir) {
    // 調用指令對應的更新函數
    const callback = this[dir + 'Updater'];
    callback && callback(node, this.$vm[exp])
    // 更新處理,創(chuàng)建 Watcher,保存更新函數
    new Watcher(this.$vm, exp, function (val) {
      callback && callback(node, val)
    })
  }
  // v-text
  text (node, exp) {
    this.update(node, exp, 'text')
  }
  textUpdater (node, value) {
    node.textContent = value
  }
  // v-html
  html (node, exp) {
    this.update(node, exp, 'html')
  }
  htmlUpdater (node, value) {
    node.innerHTML = value
  }
}

編譯過程中,以根元素開始,也就是實例化 Vue 時傳入的 options.el 進行遞歸編譯節(jié)點,使用 isElementNode 方法判斷是文本節(jié)點還是元素節(jié)點。
如果是文本節(jié)點,正則匹配(雙大括號){{ xxx }};使用 v-text 指令方式初始化讀取數據。
若為元素節(jié)點,遍歷屬性,找到 v-textv-html,初始化動態(tài)綁定的數據。
在初始化數據時,創(chuàng)建 Watcher 和更新函數。

數據劫持(Observer)

function observe (obj) {
  if (typeof obj !== 'object' || obj == null) {
    return;
  }
  // 傳入境來的對象做響應式處理
  new Observer(obj)
}
function defineReactive (obj, key, val) {
  // 遞歸劫持數據
  observe(val)
  // 創(chuàng)建與 key 對應的 Dep 管理相關的 Watcher
  const dep = new Dep();
  //對數據進行劫持
  Object.defineProperty(obj, key, {
    get () {
      // 依賴收集
      Dep.target && dep.addDep(Dep.target)
      return val;
    },
    set (newVal) {
      if (newVal !== val) {
        // 如果 newVal 為 Object ,就需要對其響應式處理
        observe(newVal)
        val = newVal;
        // 通知更新
        dep.notify()
      }
    }
  })
}
class Observer {
  constructor(value) {
    this.value = value;
    if (typeof value === 'object') {
      this.walk(value)
    }
  }
  // 對象數據響應化
  walk (obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }
}

上面代碼,創(chuàng)建了 observedefineReactive 方法,還有 Observer 類 。
observe 方法用于類型判斷。
Observer 類收一個參數,若參數是 object 類型,調用 defineReactive 方法對其屬性進行劫持。
defineReactive 方法中,通過 Object.defineProperty 對屬性進行劫持。并對每個 key 創(chuàng)建 Dep 的實例,還記得模板編譯時,對動態(tài)綁定的值,進行初始化的時候會創(chuàng)建 Watcher 嗎?Watcher 內保存有對應的更新函數;defineReactive 中,數據被讀取的時候,就會觸發(fā) getter , getter 中就會把 Watcher push 到對應的 Dep 中,這個過程就叫做依賴收集。當值發(fā)生改變的時候,觸發(fā) setter 調用這個key 所對應的 Dep 內的 notify 方法,通知更新。

訂閱-發(fā)布(Dep)

class Dep {
  constructor() {
    this.deps = []
  }
  addDep (dep) {
    this.deps.push(dep)
  }
  notify () {
    this.deps.forEach(dep => dep.update())
  }
}

每個 key 都會創(chuàng)建一個 Dep ,每個 Dep 內都會有一個 deps 數組,用來同一管理這個 key 所對應的 Watcher 實例。
addDep 方法用于添加訂閱。
notify 方法用于通知更新。

觀察者(Watcher)

// 觀察者,保存更新函數。
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm
    this.key = key
    this.updateFn = updateFn
    Dep.target = this // 在靜態(tài)屬性上保存當前實例
    this.vm[this.key] // 觸發(fā)數據劫持 get
    Dep.target = null // 在讀取屬性 觸發(fā)get后,依賴收集完畢,現(xiàn)在置空
  }
  update () {
    this.updateFn.call(this.vm, this.vm[this.key])
  }
}

Watcher 類接收三個參數,Vue 的實例、 綁定的變量名 keyupdateFn 更新方法。Watcher 在模板編譯時被創(chuàng)建。我們用 Dep.target 靜態(tài)屬性來保存當前的實例。主動觸發(fā)一次響應式的 getter , 使其實例被添加到 Dep 中,完成依賴收集,完成后,將靜態(tài)屬性 Dep.target 置空。
內部創(chuàng)建一個 update 更新方法。在 Depnotify 方法通知更新時被調用。

完整代碼

// 模板編譯
class Compiler {
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);
    if (this.$el) {
      // 執(zhí)行編譯
      this.compile(this.$el)
    }
  }
  compile (el) {
    // 遍歷 el 樹
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach((node) => {
      if (this.isElementNode(node)) { // 元素節(jié)點
        // 編譯元素節(jié)點的方法
        this.compileElement(node)
      } else { // 文本節(jié)點
        // 編譯文本節(jié)點的方法
        this.compileText(node)
      }
      // 如果還有子節(jié)點,繼續(xù)遞歸
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node);
      }
    })
  }
  // 判斷是否是元素節(jié)點
  isElementNode (node) {
    return node.nodeType === 1
  }
  // 判斷屬性是否為指令
  isDirective (attr) {
    return attr.indexOf('v-') === 0
  }
  // 編譯元素
  compileElement (node) {
    // 遍歷節(jié)點屬性
    const nodeAttrs = node.attributes
    Array.from(nodeAttrs).forEach((attr) => {
      const attrName = attr.name // 屬性名
      const exp = attr.value // 動態(tài)判定的變量名
      //找到 v-xxx 的指令,如 v-text/v-html
      if (this.isDirective(attrName)) {
        let [, dir] = attrName.split("-") // 指令名 如 text/html
        // 調用指令對應得方法
        this[dir] && this[dir](node, exp)
      }
    })
  }
  // 編譯文本
  compileText (node) {
    let txt = node.textContent; // 獲取文本節(jié)點的內容
    let reg = /\{\{(.*)\}\}/; // 創(chuàng)建匹配 {{}} 的正則表達式
    // 如果存在 {{}} 則使用 text 指令的方法
    if (node.nodeType === 3 && reg.test(txt)) {
      this.update(node, RegExp.$1.trim(), 'text')
    }
  }
  update (node, exp, dir) {
    // 調用指令對應的更新函數
    const callback = this[dir + 'Updater'];
    callback && callback(node, this.$vm[exp])
    // 更新處理,創(chuàng)建 Watcher,保存更新函數
    new Watcher(this.$vm, exp, function (val) {
      callback && callback(node, val)
    })
  }
  // v-text
  text (node, exp) {
    this.update(node, exp, 'text')
  }
  textUpdater (node, value) {
    node.textContent = value
  }
  // v-html
  html (node, exp) {
    this.update(node, exp, 'html')
  }
  htmlUpdater (node, value) {
    node.innerHTML = value
  }
}
// 創(chuàng)建 Vue 類
class Vue {
  constructor(options) {
    // 保存選項
    this.$options = options;
    this.$data = options.data;
    // 響應式處理
    observe(this.$data)
    // 將數據代理到實例上
    proxyData(this, '$data')
    // 用數據和元素進行編譯
    new Compiler(options.el, this)
  }
}
// 代理數據的方法
function proxyData (vm, sourceKey) {
  Object.keys(vm[sourceKey]).forEach(key => {
    Object.defineProperty(vm, key, {
      get () {
        return vm[sourceKey][key]
      },
      set (newVal) {
        vm[sourceKey][key] = newVal
      }
    })
  })
}
function observe (obj) {
  if (typeof obj !== 'object' || obj == null) {
    return;
  }
  // 傳入境來的對象做響應式處理
  new Observer(obj)
}
function defineReactive (obj, key, val) {
  // 遞歸劫持數據
  observe(val)
  // 創(chuàng)建與 key 對應的 Dep 管理相關的 Watcher
  const dep = new Dep();
  //對數據進行劫持
  Object.defineProperty(obj, key, {
    get () {
      // 依賴收集
      Dep.target && dep.addDep(Dep.target)
      return val;
    },
    set (newVal) {
      if (newVal !== val) {
        // 如果 newVal 為 Object ,就需要對其響應式處理
        observe(newVal)
        val = newVal;
        // 通知更新
        dep.notify()
      }
    }
  })
}
class Observer {
  constructor(value) {
    this.value = value;
    if (typeof value === 'object') {
      this.walk(value)
    }
  }
  // 對象數據響應化
  walk (obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }
}
// 觀察者,保存更新函數。
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm
    this.key = key
    this.updateFn = updateFn
    Dep.target = this // 在靜態(tài)屬性上保存當前實例
    this.vm[this.key] // 觸發(fā)數據劫持 get
    Dep.target = null // 在讀取屬性 觸發(fā)get后,依賴收集完畢,現(xiàn)在置空
  }
  update () {
    this.updateFn.call(this.vm, this.vm[this.key])
  }
}
// 訂閱-發(fā)布,管理某個key相關所有Watcher實例
class Dep {
  constructor() {
    this.deps = []
  }
  addDep (dep) {
    this.deps.push(dep)
  }
  notify () {
    this.deps.forEach(dep => dep.update())
  }
}

相關鏈接

Vue源碼解讀(預):手寫一個簡易版Vue

Vue源碼解讀(一):準備工作

Vue源碼解讀(二):初始化和掛載

Vue源碼解讀(三):響應式原理

Vue源碼解讀(四):更新策略

Vue源碼解讀(五):render和VNode

Vue源碼解讀(六):update和patch

Vue源碼解讀(七):模板編譯(待續(xù))

如果覺得還湊合的話,給個贊吧?。。∫部梢詠砦业膫€人博客逛逛 https://www.mingme.net/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容