從零實現一個簡單的Vue框架,掌握MVVM框架原理

--文末附視頻教程javascript

本文主要學習掌握 Vue 雙向綁定的核心部分原理。\
代碼爲簡化版,相對比較簡陋。也未考慮數組等其餘處理。\
歡迎一塊兒學習交流。

1、準備工做

1. 什麼是 MVVM 框架?

MVVM 是 Model-View-ViewModel 的簡寫,雙向數據綁定,即視圖影響模型,模型影響數據。它本質上就是MVC 的改進版。vue

  • Model(模型)是數據訪問層,例如後臺接口傳遞的數據
  • View(視圖)是用戶在屏幕上看到的頁面的結構、佈局、外觀(UI)
  • ViewModel(視圖模型)負責將 View 的變化同步到 Model,或 Model 的變化轉化爲 View。

2. Vue 怎麼實現雙向數據綁定

Vue2.x 是經過 Object.defineProperty 實現的雙向數據綁定,該方法不支持 ie8 及如下版本。
相關語法直接查看mdn文檔
其中,屬性描述符有不少個,下面簡單說明一下經常使用的幾個,具體詳細內容直接查看文檔。
下面是給定義 obj 對象定義一個名稱爲 Vue 的屬性。java

Object.defineProperty(obj, 'vue', {
  configurable: true,
  writable: true,
  enmerbale: true,
  value: 'Hello, Vue',
  get() {
    return 'Hello, Vue'
  }
  set(val) {
    console.log(val)
  }
})

configurable: 指定屬性是否能夠配置,若是不設置爲true,則沒法刪除該屬性,例如:delete obj.vue='react'無效node

writable: 指定屬性是否能被賦值運算符改變,若是不設置爲true,則給 vue 屬性賦值,例如:obj.vue='react'無效react

enmerbale: 指定屬性是否能夠被枚舉到,若是不設置爲true,使用 for...in... 或 Object.keys 是讀不到該屬性的git

value: 指定屬性對應的值,與 get 屬性衝突,一塊兒使用會報錯github

get: 訪問該屬性時,若是有設置 get 方法,會執行這個方法並返回segmentfault

set: 修改該屬性值時,若是有設置 set 方法,會執行這個方法,並把新的值做爲參數傳進入 object.vue = 'hello, Vuex'數組

3. 流程圖

流程圖

4. 流程分析

這裏咱們先看看代碼實現,大概瞭解一下整個過程,最後再對整個過程進行分析。緩存

結合代碼、註釋、過程分析能夠更好的理解整個過程。

2、開始實現

參考 Vue2.x 源碼實現,與實際 Vue 的實現有差異,但原理上差很少。建議看完能夠繼續深刻學習 Vue 實際源碼

1. Vue 入口

// 模擬 Vue 的入口
function MVVM(options) {
    var vm = this;
    vm.$options = options || {};
    vm._data = vm.$options.data;
    /**
     * initState 主要對數據對處理
     * 實現 observe,即對 data/computed 等作響應式處理以及將數據代理到 vm 實例上
     */
    initState(vm)
        // 編譯模版
    this.$compile = new Compile(options.el || document.body, this)
}

2. 模版編譯

這裏的 Compile 只是簡單的模版編譯,與 Vue 實際Compile 有較大區別,實際的 Compile 實現比較複雜,須要通過 parse、optimize、generate 三個階段處理。

  • parse: 使用正則解析template中的vue的指令(v-xxx) 變量等等 造成抽象語法樹AST
  • optimize: 標記一些靜態節點,用做後面的性能優化,在diff的時候直接略過
  • generate: 把第一部生成的AST 轉化爲渲染函數 render function
function Compile(el, vm) {
  this.$vm = vm;
  this.$el = this.isElementNode(el) ? el : document.querySelector(el);

  if (this.$el) {
    // 將原生節點轉爲文檔碎片節點,提升操做效率
    this.$fragment = this.node2Fragment(this.$el);
    // 編譯模版內容,同時進行依賴收集
    this.compile(this.$fragment);
    // 將處理後的 dom 樹掛載到真實 dom 節點中
    this.$el.appendChild(this.$fragment);
  }
}
// compile 相關方法實現
Compile.prototype = {
  node2Fragment(el) {
    const fragment = document.createDocumentFragment();
    /**
     * 將原生節點拷貝到 fragment,
     * 每次循環都會把 el 中的第一個節點取出來追加到 fragment 後面,直到 el 沒有字節點
     */
    let child;
    while (child = el.firstChild) {
      fragment.appendChild(child);
    }
  
    return fragment;
  },
  compile: function (el) {
    const childNodes = el.childNodes
    // childNodes 不是標準數組,經過 Array.from 把 childNodes 轉成數組並遍歷處理每個節點。
    Array.from(childNodes).forEach(node => {
      // 利用閉包機制,保存文本節點最初的文本,後面更新根據最初的文本進行替換更新。
      const text = node.textContent;
      // 元素節點,對元素屬性綁定對指令進行處理
      if (this.isElementNode(node)) {
        this.compileElement(node);
      }
      // 文本節點而且包含 {{xx}} 字符串對文本,模版內容替換
      else if (this.isTextNode(node) && /\{\{(.*)\}\}/.test(text)) {
        this.compileText(node, RegExp.$1.trim(), text);
      }
      // 遞歸編譯子節點的內容
      if (node.childNodes && node.childNodes.length) {
        this.compile(node);
      }
    });
  },

  compileElement: function (node) {
    const nodeAttrs = node.attributes

    Array.from(nodeAttrs).forEach(attr => {
      const attrName = attr.name;
      // 判斷屬性是不是一個指令,例如: v-text 等
      if (this.isDirective(attrName)) {
        const exp = attr.value;
        const dir = attrName.substring(2);
        // 事件指令
        if (this.isEventDirective(dir)) {
          compileUtil.eventHandler(node, this.$vm, exp, dir);
        }
        // 普通指令
        else {
          compileUtil[dir] && compileUtil[dir](node, this.$vm, exp);
        }
        node.removeAttribute(attrName);
      }
    });
  },

  compileText: function (node, exp) {
    // compileUtil.text(node, this.$vm, exp);
    // 利用閉包機制,保存文本節點最初的文本,後面更新根據最初的文本進行替換更新。
    const vm = this.$vm
    let text = node.textContent
    const updaterFn = updater.textUpdater

    let value = text.replace(/\{\{(.*)\}\}/, compileUtil._getVMVal(vm, exp))
    updaterFn && updaterFn(node, value);

    new Watcher(vm, exp, function (value) {
      updaterFn && updaterFn(node, text.replace(/\{\{(.*)\}\}/, value));
    });
  },
  // ... 省略
};

指令集合處理

// 指令處理集合
const compileUtil = {
  text: function (node, vm, exp) {
    this.update(node, vm, exp, 'text');
  },
    // ... 省略
  update: function (node, vm, exp, dir) {
    // 針對不一樣的指令使用不一樣的函數渲染、更新數據。
    const updaterFn = updater[dir + 'Updater'];
    // 這裏取值,而後進行初次的內容渲染
    updaterFn && updaterFn(node, this._getVMVal(vm, exp));
    new Watcher(vm, exp, function (value, oldValue) {
      updaterFn && updaterFn(node, value, oldValue);
    });
  },
     // ... 省略
};
  
const updater = {
  textUpdater: function (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value;
  },
  // ... 省略
};

3. 響應式對象

initState

initState 方法主要是對 propsmethodsdatacomputedwathcer 等屬性作了初始化操做。這裏咱們主要實現對 datacomputed 的操做。

function initState(vm) {
  const opts = vm.$options
  // 初始化 data
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true)
  }
  // 初始化 computed
  if (opts.computed) initComputed(vm, opts.computed)
}

initData

主要實現如下兩個操做:

  1. 調用 observe 方法觀測整個 data 的變化,把 data 也變成響應式,能夠經過 vm._data.xxx 訪問到定義 data 返回函數中對應的屬性。
  2. 對定義 data 函數返回對象的遍歷,經過 proxy 把每個值 vm._data.xxx 都代理到 vm.xxx 上。
function initData(vm) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function' ?
    data.call(vm, vm) :
    data || {}
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (methods && hasOwn(methods, key)) {
      console.log(`Method "${key}" has already been defined as a data property.`, vm)
    }
    if (props && hasOwn(props, key)) {
      console.log(`The data property "${key}" is already declared as a prop. Use prop default value instead.`, vm)
    } else if (!isReserved(key)) {
      // 數據代理,實現 vm.xxx -> vm._data.xxx,至關於 vm 上面多了 xxx 這個屬性
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true)
}

proxy

把每個值 vm._data.xxx 都代理到 vm.xxx 上。

這是一個公用的方法。這裏咱們只是對 data 定義對屬性作裏代理。實際上 vue 還經過這個方法對 props 也作了代理,proxy(vm, '_props', key)

// 數據代理,proxy(vm, '_data', key)。
function proxy(target, sourceKey, key) {
  Object.defineProperty(target, key, {
    enumerable: true,
    configurable: true,
    get: function proxyGetter() {
      // initData 裏把 vm._data 處理成響應式對象。
      // 這裏返回 this['_data'][key],實現 vm[key] -> vm._data[key]
      return this[sourceKey][key]
    },
    set: function proxySetter(val) {
      // 這裏修改 vm[key] 其實是修改了 this['_data'][key]
      this[sourceKey][key] = val
    }
  })
}

observe

observe 的功能就是用來監測數據的變化。

function observe(value) {
  if (!isObject(value)) {
    return
  }
  return new Observer(value);
}

Observer

Observer 是一個類,它的做用是給對象的屬性添加 getter 和 setter,用於依賴收集和派發更新

class Observer {
  constructor (value) {
    this.value = value
    this.dep = new Dep()
    this.walk(value)
  }
  walk (obj) {
    // 遍歷 data 對象的 key 調用 defineReactive 方法建立響應式對象
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}

defineReactive

defineReactive 的功能就是定義一個響應式對象,給對象動態添加 getter 和 setter,getter 作的事情是依賴收集,setter 作的事情是派發更新。

function defineReactive (obj, key) {
  // 初始化 Dep,用於依賴收集
  const dep = new Dep()

  let val = obj[key]

  // 對子對象遞歸調用 observe 方法,這樣就保證了不管 obj 的結構多複雜,
  // 它的全部子屬性也能變成響應式的對象,
  // 這樣咱們訪問或修改 obj 中一個嵌套較深的屬性,也能觸發 getter 和 setter。
  // 使 foo.bar 等多層的對象也能夠實現響應式。
  let childOb = observe(val)
  // Object.defineProperty 去給 obj 的屬性 key 添加 getter 和 setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // Dep.target 指向 watcher
      if (Dep.target) {
        // 依賴收集,每一個使用到 data 裏的值的地方,都會調用一次 get,而後就會被收集到一個數組中。
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return val
    },
    set: function reactiveSetter (newVal) {
      // 當值沒有變化時,直接返回
      if (newVal === val) {
        return
      }
      // 對 val 設置新的
      val = newVal
      // 若是新傳入的值時一個對象,須要從新進行 observe,給對象的屬性作響應式處理。
      childOb = observe(newVal)
      dep.notify()
    }
  })
}

4.依賴收集、派發更新

Dep

Dep 是整個 getter 依賴收集的核心,這裏須要特別注意的是它有一個靜態屬性 target,這是一個全局惟一 Watcher,這是一個很是巧妙的設計,由於在同一時間只能有一個全局的 Watcher 被計算,另外它的自身屬性 subsWatcher 的數組。

Dep 實際上就是對 Watcher 的一種管理,Dep 脫離 Watcher 單獨存在是沒有意義的。

class Dep {
  static target;
  constructor () {
    // 存放 watcher 的地方
    this.subs = []
  }

  addSub (sub) {
    this.subs.push(sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
    // 派發更新
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Dep.target = null

Watcher

class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    this.cb = cb
    this.expOrFn = expOrFn;
    this.depIds = {};
    // 判斷 expOrFn 是否是一個函數,若是不是函數會經過 parsePath 把它變成一個函數。
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // parsePath 把 expOrFn 變成一個函數
      this.getter = parsePath(expOrFn) || function noop (a, b, c) {}
    }
    // 取值,觸發依賴收集。
    this.value = this.get()
  }
  get() {
    // 這裏 Dep.target 指向 watcher 自己,而後會取值,取值觸發對應屬性的 getter 方法。
    // 此時 getter 方法裏面使用的 Dep.target 就有值了。
    // 經過一系列的代碼執行 dep.depend() -> Dep.target.addDep(dep) -> dep.addSub(watcher) 
    // 最後把 watcher 存到 subs 數組裏,完成依賴收集。
    // 最後把 Dep.target 刪除,保證來 Dep.target 在同一時間內只有惟一一個。
    Dep.target = this;
    const vm = this.vm
    let value = this.getter.call(vm, vm)
    Dep.target = null;
    return value
  }
  addDep(dep) {
    if (!this.depIds.hasOwnProperty(dep.id)) {
      dep.addSub(this);
      this.depIds[dep.id] = dep;
    }
  }
  update() {
    // this.value 是 watcher 緩存的值,用來與改變後的值進行對比,若是先後值沒有變化,就不進行更新。
    const value = this.get()
    const oldValue = this.value
    if (value !== oldValue) {
      // 緩存新的值,下次操做用
      this.value = value
      // 以 vm 爲 cb 的 this 值,調用 cb。
      // cb 就是 在 new watcher 使傳入的更新函數。會把新的值傳入經過更新函數,更新到視圖上。
      this.cb.call(this.vm, value, oldValue)
    }
  }
}

3、過程分析

  1. new MVVM() 的時候,首先,會對 datapropscomputed 進行初始化,使它們變成響應式的對象。
  2. 響應式是經過使用 Object.defineProperty 給對象的屬性設置 getset,爲屬性提供 getter、setter 方法,一旦對象擁有了 getter 和 setter,咱們能夠簡單地把這個對象稱爲響應式對象。。
  3. 當咱們訪問了該屬性的時候會觸發 getter 方法,當咱們對該屬性作修改的時候會觸發 setter 方法。
  4. 在 getter 方法裏作依賴的收集。由於在使用屬性的時候,就會觸發 getter,這時就會把這個使用記錄起來,後面屬性有改動的時候,就會根據這個收集的記錄進行更新。
  5. 在 setter 方法裏作派發更新。由於在對屬性作修改的時候會觸發這個setter,這時就能夠根據以前在 getter 裏面收集的記錄,去作對應的更新。
  6. getter 的實現中,是經過 Dep 實現依賴收集的。getter 方法中調用了 Dep.depend() 進行收集,Dep.depend() 中又調用了 Dep.target.addDep(this)
  7. 這裏 Dep.target 是個很是巧妙的設計,由於在同一時間 Dep.target 只指向一個 Watcher,使得同一時間內只能有一個全局的 Watcher 被計算。
  8. Dep.target.addDep(this) 等於調用 Watcher.addDep(dep) ,裏面又調用了 dep.addSub(this) 把這個全局惟一的 watcher 添加到 dep.subs 數組中,收集了起來,而且 watcher 自己也經過 depIds 收集持有的 Dep 實例。
  9. 上面只是定義了一個流程,可是須要訪問數據對象才能觸發 getter 使這個流程運轉起來。那何時觸發呢?
  10. Vue 會經過 compile 把模版編譯成 render 函數,並在 render 函數中訪問數據對象觸發 getter。這裏咱們是直接在 compile 的時候訪問數據對象觸發 getter。
  11. compile 負責內容的渲染與數據更新。compile 編譯模版中的內容,把模版中的 {{xx}} 字符串替換成對應的屬性值時會訪問數據對象觸發 getter,不過此時尚未 watcher,沒有依賴收集。
  12. compile 接下來會實例化 Watcher,實例化過程會再去取一次值,此時觸發到 getter 纔會進行依賴收集。具體看 Watcher 的 構造函數與 get 方法實現。
  13. 到這裏,頁面渲染完成,依賴收集也完成。
  14. 接下來會監控數據的變化,數據若是發生變化,就會觸發屬性值的 setter 方法,setter 方法除了把值設置爲新的值以外,還會進行派發更新。執行 dep.notify(),循環調用 subs 裏面保存的 watcherupdate 方法進行更新。

獲取視頻教程+源碼

相關文章
相關標籤/搜索