vue簡版源碼淺析

大家好,最近在看一些Vue的源碼,由淺入深,先從最簡單的說起吧。

vue簡版源碼

這款vue簡版源碼可以很好的幫我們理解mvvm的實現,源碼只有四個js文件,我們來一起看一下:

index.html 主頁面
index.js 入口文件
observer.js 設置訪問器及依賴收集
compile.js dom編譯
watcher.js 依賴監聽類

接下來我們看一些核心代碼(部分代碼省略)
index.html

<div id="app">
  <h2>{{title}}</h2>
  <input v-model="name">
  <h1>{{name}}</h1>
  <button v-on:click="clickMe">click me!</button>
</div>
<script type="text/javascript">
  new Vue({
    el: '#app',
    data: {
      title: 'vue code',
      name: 'imooc',
    },
    methods: {
      clickMe: function () {
        this.title = 'vue code click'
      },
    },
    mounted: function () {
      window.setTimeout(() => {
        this.title = 'timeout 1000'
      }, 1000)
    },
  })
</script>

index.html文件中主要是html片段和vue的實例化,那么我們的html是怎么和vue關聯起來的呢?數據變化是怎么影響html改變的呢?而頁面改變又怎么更新到數據的呢?帶著這兩個問題,我們走進index.js

index.js

function Vue (options) {
  // 初始化
  var self = this
  this.data = options.data
  this.methods = options.methods

  Object.keys(this.data).forEach(function (key) {
    // 將this.a訪問代理到this.data.a下(代碼略)
    self.proxyKeys(key)  
  })

  observe(this.data) // 設置訪問器及依賴收集
  new Compile(options.el, this) //dom編譯與綁定監聽
  options.mounted.call(this) // 所有事情處理好后執行 mounted 函數
}

在index.js中我寫了一些注釋,從注釋中可以看到執行過程與文件的對應關系,那么我們的兩個問題也有了思路:
1.數據變化是怎么影響html改變的呢,在observe里找答案
2.頁面改變又怎么更新到數據的呢,在compile里找答案
而解決上面兩個問題,又離不開watcher的輔助,只有相互依賴互相監聽,我們才能建立聯系,OK,我們先從第一個問題下手:

observe.js

function observe (value, vm) {
  ...
  return new Observer(value)
}
function Observer (data) {
  this.data = data
  this.walk(data)
}
Observer.prototype = {
  walk: function (data) {
    var self = this
    Object.keys(data).forEach(function (key) {
      self.defineReactive(data, key, data[key])
    })
  },
  defineReactive: function (data, key, val) {
    var dep = new Dep()
    var childObj = observe(val)
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get: function getter () {
        if (Dep.target) {
          dep.addSub(Dep.target)
        }
        return val
      },
      set: function setter (newVal) {
        if (newVal === val) {
          return
        }
        val = newVal
        dep.notify()
      },
    })
  },
}
function Dep () {
  this.subs = []
}
Dep.prototype = {
  addSub: function (sub) {
    this.subs.push(sub)
  },
  notify: function () {
    this.subs.forEach(function (sub) {
      sub.update()
    })
  },
}
Dep.target = null

observe.js文件分為兩個類,第一段代碼是Observer用于屬性訪問器設置,第二段代碼是Dep類于用依賴收集,核心的地方在于get set時對dep對象的處理。
我們先來看Dep.target,這是一個靜態屬性,用于存放watch監聽對象,定義在全局中,可見全局中只會有一個值,具體怎么用我們等下看watch。
代碼邏輯中判斷是否有Dep.target,如果有就收集這個依賴,這個時候,我們可以大膽假設一下,對this.a進行get訪問時,收集了什么依賴,然后在this.a = 1時,對收集的依賴進行了更新notify,這個什么依賴應該就是第一個問題的答案了吧,沒錯,就是對dom的監聽。
這樣我們在observe中就看到了數據變化時觸發dom監聽去更新dom,第一個問題就有了答案,接下來我們為了優先把上面的疑問解開,先看watcher文件

watcher.js

function Watcher (vm, exp, cb) {
  this.cb = cb
  this.vm = vm
  this.exp = exp
  this.value = this.get() // 將自己添加到訂閱器的操作
}
Watcher.prototype = {
  update: function () {
    this.run()
  },
  run: function () {
    var value = this.vm.data[this.exp]
    var oldVal = this.value
    if (value !== oldVal) {
      this.value = value
      this.cb.call(this.vm, value, oldVal)
    }
  },
  get: function () {
    Dep.target = this // 緩存自己
    var value = this.vm.data[this.exp] // 強制執行監聽器里的 get 函數
    Dep.target = null // 釋放自己
    return value
  },
}

在watcher文件中我們看到了一些熟悉的身影,Dep.targetupdate方法沒錯,這些是在observe中出現的,我們先看update,update方法是Dep類中notify調用的,notify是依賴通知,在update中我們又看到了cb回調函數。 看到這我們會想這個回調是什么,我們看Watcher的函數定義,第一個可以認為就是this,第二個是表達式(簡單理解就是this.a),cb是回調函數,可見在實例化Watcher的時候,我們已經拿到了屬性對應的回調,所以notify就是在通知屬性對應的依賴觸發,去做一些更新dom的事。那么notify出現在屬性在重新賦值的地方也就順理成章。

接下來看Dep.target,在Watch的構造函數中有this.value = this.get() ,然后在get方法中Dep.target=當前的watcher對象(屬性、回調),強制執行監聽器里的 get 函數,達到兩個效果,一watcher中保存了oldvalue,二執行get時,對應的屬性會進行依賴收集,有沒有印象if (Dep.target) ,所以這個時候,我們就完成了收集,最后Dep.target = null 釋放,因為js單線程,所以此處定義為全局變量也沒什么不可,畢竟收集上來的監聽對象都收集到了閉包私有變量dep中,使每個data的屬性都能對應自己的依賴。

至此,第一個問題已經驗證了很多回,那么dom改變如何影響數據改變呢?我們繼續看compile.js

compile.js

function Compile (el, vm) {
  this.vm = vm
  this.el = document.querySelector(el)
  this.fragment = null
  this.init()
}
Compile.prototype = {
  init: function () {
    if (this.el) {
      this.fragment = this.nodeToFragment(this.el)
      this.compileElement(this.fragment)
      this.el.appendChild(this.fragment)
    } else {
      console.log('DOM 元素不存在')
    }
  },
  nodeToFragment: function (el) {
    var fragment = document.createDocumentFragment()
    var child = el.firstChild
    while (child) {
      // 將 DOM 元素移入 fragment 中
      fragment.appendChild(child)
      child = el.firstChild
    }
    return fragment
  },
  compileElement: function (el) {
    var childNodes = el.childNodes
    var self = this;
    [].slice.call(childNodes).forEach(function (node) {
      var reg = /\{\{(.*)\}\}/
      var text = node.textContent

      if (self.isElementNode(node)) {
        self.compile(node)
      } else if (self.isTextNode(node) && reg.test(text)) {
        self.compileText(node, reg.exec(text)[1])
      }

      if (node.childNodes && node.childNodes.length) {
        self.compileElement(node)
      }
    })
  },
  compile: function (node) {
    var nodeAttrs = node.attributes
    var self = this
    Array.prototype.forEach.call(nodeAttrs, function (attr) {
      var attrName = attr.name
      if (self.isDirective(attrName)) {
        var exp = attr.value
        var dir = attrName.substring(2)
        if (self.isEventDirective(dir)) {  // 事件指令
          self.compileEvent(node, self.vm, exp, dir)
        } else {  // v-model 指令
          self.compileModel(node, self.vm, exp, dir)
        }
        node.removeAttribute(attrName)
      }
    })
  },
  compileText: function (node, exp) {
    var self = this
    var initText = this.vm[exp]
    this.updateText(node, initText)
    new Watcher(this.vm, exp, function (value) {
      self.updateText(node, value)
    })
  },
  compileEvent: function (node, vm, exp, dir) {
    var eventType = dir.split(':')[1]
    var cb = vm.methods && vm.methods[exp]

    if (eventType && cb) {
      node.addEventListener(eventType, cb.bind(vm), false)
    }
  },
  compileModel: function (node, vm, exp, dir) {
    var self = this
    var val = this.vm[exp]
    this.modelUpdater(node, val)
    new Watcher(this.vm, exp, function (value) {
      self.modelUpdater(node, value)
    })
    node.addEventListener('input', function (e) {
      var newValue = e.target.value
      if (val === newValue) {
        return
      }
      self.vm[exp] = newValue
      val = newValue
    })
  },
  updateText: function (node, value) {
    node.textContent = typeof value === 'undefined' ? '' : value
  },
  modelUpdater: function (node, value, oldValue) {
    node.value = typeof value === 'undefined' ? '' : value
  },
  isDirective: function (attr) {
    return attr.indexOf('v-') == 0
  },
  isEventDirective: function (dir) {
    return dir.indexOf('on:') === 0
  },
  isElementNode: function (node) {
    return node.nodeType == 1
  },
  isTextNode: function (node) {
    return node.nodeType == 3
  },
}

comile代碼較多,我們可以簡化理解
1.document.createDocumentFragment使用createDocumentFragment來將html映射為dom對象應該是1.X的方案,先不說這個,在2.X的文章中我們在講虛擬dom吧
2.核心方法compileElement,通過我們來分析每一個node節點的類型與內容,做不同的解析,比如{{a}}這里就存在一個監聽,v-mode={{a}},這里又一個監聽,最終跑完你會發現this.a更新賦值時,會有兩個監聽節點要更新,所以在html解析時,我們引入了watcher,傳入對應data和回調,回調無疑問就是更新node節點。上面提到的邏輯就又驗證了一遍。
3.在這里我們回答第二個問題,dom更新時如何使data變化,舉個簡單的例子,在input框輸入數值時,從代碼可以看到node.addEventListener('input', function (e)),在這里我們直接對屬性進行了賦值,從而更新了data。

以上就是mvvm的簡易實現,在此基礎上我們就更好去解讀Vue2.X的源碼,篇幅較長,下一篇見啦~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容