嘗試自行實現一個簡版的vue

背景

一直在使用vue作爲前端框架,預計將來很長一段時間,前端框架都將持續存在並發展,因此,更加深入瞭解vue便是一件非常重要的事。要想真正深入瞭解vue,除了根據問題去查詢他人對vue的看法外,直接自行實現一個簡版的vue,也是非常好的方式。

簡要分析

  • vue 的特點:響應式。具體點:根據數據模型變動情況,自行更新視圖。因此,實際上要實現兩個基本的功能:①監聽數據模型的變動;②更新視圖。最後,還要有一個將兩者聯繫起來的中間橋樑,去進行分發。

  • 做到監聽的前提條件,也就是採用es6約定的 Object.defineProperty() 中的 get 和 set。舉個例子:

    	let responsiveObj = {name: '', age: '', intro: ''}
    	Object.defineProperty(responsiveObj, 'name', {
    		get () {
    			console.log('someone is getting name now')
    			return 'long'
    		},
    		set (val) {
    			console.log('someone is setting name now')
    			this._name = val
    		}
    	})
    

    通過這種方式,就能夠在每一次讀取值的時候和更新值的時候進行一次處理,在這個處理中,就可以加入我們想要的動作。

  • 那麼,我們想要做什麼呢?
    如上所說,我們需要①對於每一個 ‘希望響應式更新’ 的實例,都對自己 ‘用過的值’ 進行監聽。 ②數據模型中的每一個項,都要保存 ‘誰引用了自己’ 的列表。 ③數據模型中的每一項進行了值更新時,就循環通知 ‘引用了自己的實例’,並把新的值一塊兒傳過去。④每一個收到了通知的實例,都要根據更新的內容對自身做一些改變。

    在vue中,①就對應着我們寫的 <template> 中的每個節點,在html中,也就直接對應着 ‘視圖’,因爲要具有‘監聽’的功能,因此每個節點都要有一個 Watcher 實例; ②對應着一個類 Dependency,數據模型也就是 export default 中的部分內容,包括 data () {}、 computed () {},也就意味着數據模型中的每一項都要有一個Dependency實例; ③對應着我們在上面的例子中寫的 set 後面要進行的處理。④對應着①中的每一個實例要對自己做的事,也就是做 ‘替換視圖’ 的事,這是由節點中的另一個 Complier實例 去實現。

總結一下幾個類

  • Watcher 訂閱者
    這是每一個節點具有的實例,用來監聽自己需要的值的變化情況,依據監聽的結果,對節點進行處理(complier)。

  • Observer 觀察者
    數據模型中的每一項都是一個Observer,實際就是使用 Object.defineProperty() 實現的。

  • Dependency 依賴項
    數據模型中的每一項都具有此實例,用來收集對某個數據源有依賴的節點。

  • Complier 編譯者
    每一個節點具有的實例,用於進行 DOM 的更新。

探討一下‘名字’

在上面,我們用了 Watcher 訂閱者,Observer 觀察者,Dependency 依賴項,Complier 編譯者 這幾個名字,之前看這一塊兒內容的時候,總覺得名字起的很奇怪,尤其是不懂 Watcher 和 Observer 的區別,以自己二流的英語能力,會把 Watcher 翻譯成觀察者,而 Observer 似乎更像是 ‘被觀察對象’, 把 Dependency 理解成 ‘在觀察自己的實例的列表’

但其實,‘觀察者’ 指的是 ‘更近距離’ 地接觸到 ‘被觀察對象’ 的實例,當這個觀察者發現被觀察的內容發生了變化(調用了 set , 例如 $data.name = ‘xxx’),這裏的 $data.name 纔是被觀察的對象,而在 set 中所執行的一系列操作,都是 ‘觀察者’ 在做自己該做的事(調用一些方法讓節點知道值發生了變化)。

而 Watcher 實際是一個 ‘訂閱者’ 的概念,就像一個讀者在出版社的 ‘訂閱表’ 上填上了自己的地址(添加到 數據模型的 Dependency 實例中),當出版社出新書的時候(值發生了變化),出版社就會拿着新書(新值),送到每一位訂閱了的讀者所填的地址去。

依賴項 Dependency,其實表示的是 ‘依賴了當前數據模型的節點’ ,而不是 ‘自己所依賴的數據’。

實現一下幾個方法

// 用來控制 Watcher 加入到 Dependency 中的全局變量
this.globalWatcher = null

function defineObj (obj, key, val) { // 實際進行響應式改造的方法
  let dep = new Set() // 依賴收集,可以把這個抽象成一個 Dependency 類
  Object.defineProperty(obj, key, {
    get () {
      if (globalWatcher) {
        dep.add(globalWatcher) // 把當前讀取該值的 watcher 添加到依賴項中
        globalWatcher = null
      }
      return val
    },
    set (newVal) { 
      val = newVal // 賦新值
      observe(val) // 變成響應式
      // 通知依賴項中的每一個watcher
      Array.from(dep).forEach(item => {
        item.update && item.update(val)
      })
    }
  })
}

function observe (obj) { // 處理‘是否爲對象’
  let isObj = (tar) => Object.prototype.toString.call(tar) === '[object Object]'

  if (isObj(obj)) {
    Object.keys(obj).forEach(prop => {
      if (isObj(obj[prop])) { // 子屬性爲對象,則往下遍歷
        observe(obj[prop])
      } else {
        defineObj(obj, prop, obj[prop])
      }
    })
  }
}

// 網上大家基本都是用 es6 中的 class 關鍵字實現的
// 這裏試着用原始版的 js類 實現方式
function Watcher (val, prop) { // 觀察者類
  globalWatcher = this
  this.nameVal= val[prop]
}

Watcher.prototype.update = function (val) { // 接收更新後的處理
  console.log('update this val in update')
  // 這裏可以做很多事情,編譯器的調用也可以在這裏
  this.nameVal = val
}

// 舉個例子,定義一個數據模型
let obj = {
  name: 'long',
  age: 18,
  say () {
    console.log('hello , I am ' + this.name + ', and I am ' + this.age + ' years old.')
  }
}
// 把這個數據模型變成響應式的
observe(obj)

// 創建一個觀察數據模型中的name的實例
let eleNameNode = new Watcher(obj, 'name')

// 之後改變 obj.name 的時候,都會觸發到 eleNameNodede 原型上 update 方法
// 雖然現在 update 中只進行了 console.log ,但這裏面可以添加一切想做的事
obj.name = 'longalong' // => 'update this val in update'

總結一下

上面的內容,還沒有包含 Complier,這部分主要是和正則表達式進行字符串解析相關,我目前對正則表達式的掌握還遠遠不夠,因此暫時省略了這部分內容,實現了響應式的前半部分,後半部分明天再弄一下。

要去做的

從上面寫這篇文章的過程,能看出我至少在以下幾個方面的熟練度是不夠的,認識是不清晰的:

  • 面向對象的思想。 => 總是在想要怎麼‘一步步實現’,而不是‘實現這個功能的實例要具有什麼方法’。
  • 正則表達式。 => 嗯,只知道點皮毛,應用層面差得不是一點半點。
  • DOM 對象實例。 => 對DOM對象的理解太淺,沒有真正把它當做一個 對象 去看待。
  • 數據結構與算法。 => 任何問題,最終都可以歸結到數據結構上,捋清數據結構,就能比較清晰地明白用什麼樣的算法去實現。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章