vue源碼解讀 -- 整體架構

vue在引入后會進行一些初始化操作,主要是在全局對象和vue原形鏈上掛載函數,這些函數在后續的實例化過程中用到時再進行討論。

一個例子

如下是vue一個最簡單的例子,基于此例展開對vue整體架構的分析

<div id="app">{{a}}</div>
<script>
  var model = new Vue({
    el: '#app',
    data: {
      a: 1
    }
  });
  model.a = 10;
</script>
1. 初始化

vue本身是個函數,執行new操作后調用其init函數:

function Vue (options) {
  this._init(options)
}

_init函數的作用可以歸納為兩點:初始化狀態和掛載節點。

  • 初始化狀態是對options中的data、props(組件中用到)、methods、computed等進行初始化操作,偏數據層面。
  • 掛在節點會對dom進行分析,生成指令,同時生成關聯數據和指令的watcher對象。在數據改變時,會通過watcher通知directive進行視圖的更新。
  Vue.prototype._init = function (options) {
    ...
    // 狀態的初始化
    this._initState()

    // 掛在到節點
    if (options.el) {
      this.$mount(options.el)
    }
  }
2. 狀態的初始化

狀態初始化會的作用作用是把data下面的數據字段都設計成響應式的:數據獲取(getter)的時候收集其關聯到的指令,數據變化(setter)的時候通知指令進行視圖的更新。
_initState主要包括以下幾個函數

Vue.prototype._initState = function () {
  this._initProps()  // 組件中的props初始化 
  this._initMeta()  // 不知道干嘛的
  this._initMethods()  // 把methods中的方法綁定到實例上
  this._initData()  // 數據初始化 下面分析
  this._initComputed()  // computed屬性的初始化
}

// _initMethods 可以直接在實例上獲取到methods中的方法
Vue.prototype._initMethods = function () {
  var methods = this.$options.methods
  if (methods) {
    for (var key in methods) {
      this[key] = bind(methods[key], this)
    }
  }
}

// _initData
Vue.prototype._initData = function () {
  ...
  // observe會遍歷data中的字段,對每個字段執行defineReactive操作
  // 讀取字段時,會收集其依賴,這里的依賴是watcher對象的列表
  // 當設置字段值時, 會對依賴執行notify()操作 
  observe(data, this)
}
function defineReactive (obj, key, val) {
  var dep = new Dep()
  ...
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend() // 收集字段的依賴
        if (childOb) {  // 收集子對象的依賴
          childOb.dep.depend()
        }
        ...
    },
    set: function reactiveSetter (newVal) {
      ...
      // 此處會調用依賴列表的watcher進行數據視圖的同步! 
      // watcher會調用directive的更新方法
      dep.notify()
    }
  })
}

// _initComputed
// 相對于普通的data字段,computed字段會在指令生成前額外生成一個watcher
// 此watcher也會加入到依賴列表中去;但是此watcher是lazy的。
// 非lazy的watcher會在watcher生成后執行get函數
// lazy的watcher  會在computed中的字段讀取值時才會調用到其getter函數
Vue.prototype._initComputed = function () {
  for (var key in computed) {
    var userDef = computed[key]
    var def = {
      enumerable: true,
      configurable: true
    }
    def.get = makeComputedGetter(userDef, this)
    Object.defineProperty(this, key, def)
  }
}

function makeComputedGetter (getter, owner) {
  var watcher = new Watcher(owner, getter, null, {
    lazy: true
  })
  return function computedGetter () {
    if (watcher.dirty) {
      watcher.evaluate()
    }
    if (Dep.target) {
      watcher.depend()
    }
    return watcher.value
  }
}
3. 節點掛載

節點掛載集中了dom結構分析、指令的生成、指令和數據關聯的watcher對象生成等幾個功能。

Vue.prototype.$mount = function (el) {
  ...
  this._compile(el)
  ...
  return this
}

這個compile函數會對DOM節點進行解析,根據指令的不同(如:v-model、v-text、{{}})等生成link函數;這些link函數最終都會執行到下面代碼段中的函數,這個函數的作用可以概括為:生成指令并和節點進行關聯。

  // el:指令所在的節點 例子中的{{a}}
  // descriptor: 例子中的解析得到的指令:
  //    expression:"a"
  //    filters:undefined
  //    name:"text"
  vm._bindDir(descriptor, el, host, scope, frag)

上述步驟執行完后,vue會依據directive的priority進行排序,然后對指令執行bind操作:

function linkAndCapture (linker, vm) {
  var originalDirCount = vm._directives.length
  linker()
  var dirs = vm._directives.slice(originalDirCount)
  dirs.sort(directiveComparator)
  for (var i = 0, l = dirs.length; i < l; i++) {
    // 指令的bind方法:把數據和視圖進行關聯
    // 生成watcher實例 通過watcher進行后續的數據和視圖同步:
    // 1、一個雙向綁定的元素,如input,通過監聽其input or change事件, 有數據更新時,調用directive的set
    // 2、directive的set調用watcher的set方法
    // 
    dirs[i]._bind()
  }
  return dirs
}

bind()可以理解為數據和dom的一種綁定,其包括兩步:數據和watcher綁定、watcher和指令綁定。 也就是數據更新是通過watcher通知directive,進而更新view:

  Directive.prototype._bind = function () {
  ...
    // watcher的更新回調  用于directive的更新
    // 當數據更新時會執行到此函數
    if (this.update) {
      this._update = function (val, oldVal) {
        if (!dir._locked) {
          dir.update(val, oldVal)
        }
      }
    }
    ...
    // 1. 生成指令的watcher對象 
    var watcher = this._watcher = new Watcher(
      this.vm,
      this.expression,
      this._update, // callback
      {
        filters: this.filters,
        twoWay: this.twoWay,
        deep: this.deep,
        preProcess: preProcess,
        postProcess: postProcess,
        scope: this._scope
      }
    )
    
    if (this.afterBind) {
      this.afterBind()
    } else if (this.update) {
      // 2. 已經綁定了數據的依賴  進行view的更新
      this.update(watcher.value)
    }
  }
  this._bound = true
}

watcher實例生成時,會進行數據的依賴綁定:

export default function Watcher (vm, expOrFn, cb, options) {
  ...
  vm._watchers.push(this)
  ...
  // get函數的執行主要是收集依賴
  this.value = this.lazy
    ? undefined
    : this.get()
}

Watcher.prototype.get = function () {
  this.beforeGet() // 依賴配置
  ...
  value = this.getter.call(scope, scope)  // 進行收集
}
// 在beforeGet中進行依賴的配置
Watcher.prototype.beforeGet = function () {
  // 這個設置 會讓getter執行時 把watcher作為依賴
  Dep.target = this
  this.newDeps = Object.create(null)
}
// defineReactive中進行收集
export function defineReactive (obj, key, val) {
  ...
  Object.defineProperty(obj, key, {
      ...
      get: function reactiveGetter () {
        ...
        // 依賴收集
        if (Dep.target) {
          dep.depend()
  ...
}
4. 數據更新

實例中執行model.a = 10;,它會首先進入defineReactive的setter

  set: function reactiveSetter (newVal) {
      ...
      // 此處會調用依賴列表的watcher進行數據視圖的同步! 
      // watcher會調用directive的更新方法
      dep.notify()
    }

如下是nodtify的定義,subs是依賴到的watcher列表:

Dep.prototype.notify = function () {
  // stablize the subscriber list first
  var subs = toArray(this.subs)
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

watcher的update函數會把watcher放到一個列表中,然后統一依次調用callback執行更新,這里的callback是Directive.prototype._bind中的update:

Watcher.prototype.update = function (shallow) {
  ...
  pushWatcher(this)
}

export function pushWatcher (watcher) {
  ...
    q.push(watcher)
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushBatcherQueue)
    }
  }
}

function flushBatcherQueue () {
  runBatcherQueue(queue)
  ...
}

function runBatcherQueue (queue) {
  for (var i = 0; i < queue.length; i++) {
    var watcher = queue[i]
    ...
    watcher.run()
  }
}

Watcher.prototype.run = function () {
  ...
  // 此處的cb就是_bind中的update函數
  this.cb.call(this.vm, value, oldValue)
  ...
}

至此就實現了一個最基本的數據和視圖的綁定。如下是一張簡單的架構圖:

架構圖
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容