我是如何看Vue源碼的


前言

關於vue響應式的文章其實已經挺多了,不過大多都在淺嘗輒止,基本就是簡單介紹一下Object.defineProperty,覆蓋一下setter做個小demo就算解決,好一點的會幫你引入observe、watcher、dep的概念,以及加入對Array的特殊處理,所以本篇除了上述以外,更多的重心將放在setter引發render的機制與流程上,然后結合這個這個響應式機制解析vue中的watchcomputed語法實現

文章分為兩部分,第一部分會簡單介紹vue實例構建流程,第二部分則深入探究響應式實現。

建議對照源碼閱讀文章,因為很多本文很多地方會直接指出文件路徑,同時將省略部分代碼而直述功能

版本信息:

  • vue: 2.6.12

一、尋找vue

直入主題

真正的vue實例在core/instance/index中可以找到

function Vue (options) {
  ....
  this._init(options) // 這個方法在initMixin中定義
}

initMixin(Vue)  // 掛載_init()
stateMixin(Vue)  // 掛載狀態處理方法(掛載data,methods等)
eventsMixin(Vue)  // 掛載 事件 的方法($on,$off等)
lifecycleMixin(Vue) // 掛載 生命周期方法(update,destory)
renderMixin(Vue)  // 掛載與渲染有關的方法($nextTick,_render) 

每個方法可以按照代碼邏輯來看,實現對應功能,這里拿initMixin舉例

篇幅有限,所有此處僅解釋initMixin邏輯,剩余幾個方法大家可以自己探索哦

initMixin

initMixin中僅僅掛載了_init()方法,在_init中,初始化了整個vue的狀態:

function _init(option) {
  ...
  vm._uid = uid++ // 即component id
  ...
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm)
  initProvide(vm)
  callHook(vm, 'created')
  ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el) // 開始掛載
  }
} 

這里我們可以看到幾個beforeCreate,createdMount關鍵字,大概就能夠猜到vue實例的部分生命周期方法就是在這里進行了掛載,再結合 vue官方文檔的圖示

關於初始化整個vue的狀態,可以舉例來說,例如initLifecycle中就賦值了parent,children,以及一些isMounted,isDestroy的標識符。initRender中就將attrs,listeners響應化,等等,諸如此類。

initMixin=>initState=>initData,便可以看到掛載props,methods,data,computed,watch了,

可以看到,此處先掛載了props,methods,然后是data的順序,其實再往下探究邏輯就可以知道,如果存在變量重名,優先級是props>methods>data的,這也就解釋了為什么初始化的順序是這樣安排的

initData中,先是獲取了data數據,判斷props,methods變量重名問題,然后是走了一個代理,將變量名代理到vue實例上,這樣的話你的vue實例中,使用this.x指向就可以訪問到this.data.x,這類代理也用在了propsmethods

initData獲取數據中可以看到一個判斷typeof data === 'function' ? getData(data, vm) : data || {}, 支持兩種方式獲取,實際上如果是自己寫這樣一個邏輯是會藏有隱患的,如果你的data是直接使用對象,而js的復雜數據類型是地址引用,這意味着,你實例化了兩個vue對象,實際上他們的data引用地址是同一個地址,對其中一個vue data的修改會觸發另一個vue數據的變動,帶來的問題是巨大的

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
} 

這個邏輯處理的設計也是非常巧妙,他覆蓋了實例中對該key的訪問,使用settergetter將實際訪問指向了this.data[key]

這里可以說一下computed的邏輯,實際上也是取巧使用了原本用於data的響應式邏輯,其實看到上面貼出來的proxy代碼,大概就能猜到,既然proxy能夠改變一個變量讀取的指向,那么他也能創造一個虛假變量的指向,這個創造出來的這個變量實際上就是computed所使用的變量,將每次computed函數賦給getter,再加上響應式處理,就完全實現了computed,

走到最后,就是observe(data),也就是開始處理vue數據的雙向綁定

二、雙向綁定

不同於react的單向數據流,vue使用的雙向綁定,單向數據流可以理解為當源頭的數據發生變動則觸發行為,當然這個變動是主動的,即你需要setState才能觸發,而雙向綁定則可以抽象為,每一個數據旁邊都有一個監護人(一種處理邏輯),當數據發生變化,這個監護人就會響應行為,這個流程是被動發生的,只要該數據發生變動,就會通過監護人觸發行為。

如果你之前有過了解,大概就會知道,js每個數據的變動都是通過Object原型鏈中的setter去改變值,而如果你在他改變值之前,去通知監護人,就能夠實現上述的邏輯,這一點很多博客文章都寫的非常清楚了。

接着第一部分的initData知道最后observe(data),這里開始正式處理響應式。

2.1 前置條件

前面一直提到,通過Object的原型鏈改變對象的默認行為:gettersetter,首先我們需要知道,在js中,讀取一個對象的值並不是直接讀取,而是通過Object的原型鏈上的默認行為getter拿到對應的值,而改變這種行為實際上是通過Object.defineProperty,來重新定義一個對象的gettersetter,在/src/core/observer/index.js中我們可以看一個defineReactive方法,他就是vue用來實現這種行為的方法,也是這個響應式的核心

function defineReactive(obj, key, val, ... ) {
  // 此處需要保留getter、setter是因為,開發者可能自己基於defineProperty已經做過一層覆蓋,
  // 而響應式又會覆蓋一次,所以為了保留開發者自己的行為,此處需要兼容原有的getter、setter
  const getter = property && property.get // 拿到默認的getter、setter行為
  const setter = property && property.set
  Object.defineProperty(obj, key, {
    enumerable: true, // 是否可以被枚舉出來(例如Object.keys(),for in)
    configurable: true, // 是否可以被配置,是否可以被刪除
    get: function() {
      const value = getter ? getter.call(obj) : val
      ...
      return value
    }
    set: function(newVal) {
      ...
      setter.call(obj, newVal)
  	} 
  })
} 

2. 2響應式

首先,我們猜想一下,雙向綁定的行為,數據能夠響應行為的變化,而行為又能夠操作數據的改變,雖然有部分教程會讓你站在數據的角度去理解這種行為,實際上,我們站在行為的角度上去理解是更加方便的。

我們將一種行為定義為一個Watcher,他有可能是一個vue文件的template中的dom節點渲染行為,也有可能是computed的計算值行為,總之,我們從行為的角度出發,一個行為的發生,會伴隨着對變量的讀取(回想一下我們在vue文件中的templatehtml標簽時,總是會使用{{obj.xxx}}來讀取某個變量並渲染),我們想要實現,變量的改變也會帶動這個行為的重新渲染,是不是我們只需要在首次行為發生的周期內,在讀取某個變量時,在這個變量內記錄這個Watcher,這樣的話,下次變量的改變時,我只要觸發我之前記錄過的Watcher就行了。所以,我們只需要在一個Watcher發生時,將其掛載到一個公共變量上,這樣在讀取一個值的時候,記錄這個公共變量,就能夠實現上述操作。

這里先不解釋Dep的作用,可以將其抽象理解為一個被掛載在數據上的數組,每次這個數據被一個watch讀取時,就會將這個watch記錄下來

2.2.1 Watcher

既然說到將一種行為定義為一個watcher,那么可以在/src/core/observer/watcher.js中看到Watcher的實體類,而我們之前一直所說的“行為”,實際上就是構造器的第二個參數expOrFn,可以有表達式或者函數讀取的兩種模式

class Watcher {
 	constructor ( vm: Component, // vue實例
    expOrFn: string | Function, // 行為
    cb: Function, // 為watch服務
    options?: ?Object,
    isRenderWatcher?: boolean // 判斷是否為渲染watcher, )
} 

接着來看一種最典型的watcher行為,在/src/core/instance/lifecycle.js中的moundComponent方法中,可以看到一個實例化watcher的方法

new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */) 

可以看到,他將updateComponent(可以抽象為渲染行為)傳給Watcher,而在Watcher的實例化中,將會執行此方法,當然在執行之前,pushTarget(this),將這個watcher掛載到公共變量上而后開始執行渲染行為,

class Watch {
  constructor(...) {
    ....
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    }
    this.get();
  }
  get() {
    pushTarget(this) // 掛載行為至公共Target
    value = this.getter.call(vm, vm) // 開始執行行為,之所以會有返回值是為了computed服務
    popTarget() // 取消掛載,避免下次讀取變量時又會綁定此行為
  }
} 

此時,如果此行為讀取了某個響應式變量,那么該變量的getter將會存儲公共變量target,當行為完成后就會取消行為的掛載,這個時候我們再回過頭來看前面的defineReactive的邏輯

function defineReactive(obj, key) {
  const dep = new Dep(); // 每個數據都有一個自己的存儲列表
  const getter = property && property.get
  const setter = property && property.set
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) { // 判斷公共變量中是否掛載了行為(watcher)
        dep.depend() // 將行為(watcher)加入dep(即此變量的存儲行為列表)
        ...
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return // 判斷變量沒有變化,則直接返回(后兩者判斷則是因為NaN!==NaN的特性)
      }
      if (setter) {
        setter.call(obj, newVal) // 開始
      } else {
        val = newVal
      }
      dep.notify() // 通知自己這個數據的存儲列表,數據發生改變,需要重新執行行為(watcher)
    }
   });
  } 

這個時候就很清晰明了了,這就是很多博客文章所說的依賴收集,變量在get時通過公共變量Target收集依賴(也就是本文所說的行為),在set時,即變量數據發生改變時,觸發更新notify;

2.2.2 Computed

前文有大致介紹computed的實現,實際上在介紹完Wacher之后就可以來詳細介紹了,計算屬性computed並沒有實際的變量,他通過原型鏈覆蓋創造了一個變量指向(src/core/instance/state.jsinitComputed),回憶一下computed的兩種寫法

'fullName': function() {
  return this.firstName + this.secondeName;
}
'fullName': {
  get: function () {...},
  set: function() {...},
} 

我們再來看一下initComputed

function initComputed (vm: Component, computed: Object) {
 const watchers = vm._computedWatchers = Object.create(null)
 for (const key in computed) {
   const userDef = computed[key]
   // 對照着computed的兩種寫法,就能理解為什么這里有這樣的判斷,
   const getter = typeof userDef === 'function' ? userDef : userDef.get
   watchers[key] = new Watcher(
    vm,
    getter || noop,
    noop,
    { lazy: true }
  )
   defineComputed(vm, key, userDef) // 通過defineProperty來創造一個掛載在vm上key(fullName)的指向
 }
} 

可以看到,他將computedgetter方法,作為Watcher的行為傳遞了進去,這樣在執行getter時,可以將此行為綁定至過程中所讀取到的變量(firstName),如此,再下次firstName發生改變時,就會觸發此Watcher,重新運行getter方法,得到一個新的fullName的值(還記得前文class Watch中的value = this.getter.call(vm, vm)嗎?這個返回值就是computed的返回值),這樣就實現了computed的邏輯

2.2.3 Watch

watch的用法,是監聽某個變量,當該變量發生變化時,執行特定的邏輯,

上文提到的兩種Watcher行為都是函數行為,但是Watcher的行為是支持函數或者表達式的(expOrFn),所以此處的exp(expression)這里就是可以提現到的,我們只需要在變量發生變化時,執行watch定義的邏輯即可,

還記得前文代碼defineReactiveset方法通知依賴更新(dep.notify()),雖然前文一直為了方便理解,將Dep描述為一種抽象的列表結構,僅用於依賴收集,但實際上他是一個單獨的數據結構,

let uid = 0;
class Dep {
  constructor() {
    this.id = uid ++; 
    this.subs = []; // 真正用於收集依賴的數據
  }
  depend () { // 依賴收集
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  notify() { // 變量值發生變化,通知更新
    // 遍歷所有收集的依賴,注意觸發更新,
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
  ...
}
Dep.target = null; // 這就是一直說的,用於掛載Watcher行為的公共變量
function pushTarget(target){ Dep.target = target };
function popTarget() { Dep.target = null }; 

實際上這里的靜態變量target以及pushTarget、popTarget是經過簡化的,因為渲染並不是一個單一的行為,他是層層嵌套的行為,所以在綁定響應式時,也是需要區分該變量到底是要綁定至哪個行為(否則每個變量都綁定最頂層的行為,一個變量的變化,將會引發整個頁面的update),因此真正的target是還有一個stack棧結構,用於掛載多個嵌套的行為

可以看到,每次變量更新,都會觸發watcher.update,那么對於watch監聽的回調,就可以放到在update中調用

class Watch {
  constructor(vm, expOrFn, cb, ...) {
    this.cb = cb // 這個cb就是watch監聽的回調
  }
  update() {
    this.run()
  }
  run() {
    ...
    this.cb.call(this.vm, ...)
	}
} 

至此,關於watch監聽的實現邏輯大致就是如此

關於依賴收集,實際上並不是在get變量時,直接將watcher綁定至Dep中,可以看到Dep.depend(),他先通知行為(watcher),叫他先綁定自己,然后watcher綁定完dep之后,才會回過頭,告知DepaddSub(),這里的邏輯像是一個圈

所以現在我們回過頭來看,前文說了,每個數據都有一個“監護人”,來記錄此數據所綁定的行為,那么這個“監護人”到底在哪里呢? 可以看到/src/core/observer/index.jsclass Observer中,

class Observer {
  constructor(val) {
    ...
    def(value, '__ob__', this) // 對value定義__ob__屬性,掛載此object
    ...
  }
} 

對於每一份需要響應式處理的數據,都會掛載一個Observer實例,其內subs就是用於記錄綁定此數據的Watcher,同時也可以看到,這份數據的get、set方法已經是被重寫過了,也就是前文的defineReactive中的覆蓋行為。

2.2.4 其他

其實對於Array的響應式是需要特殊處理的,因為他除了set、get之外,還會對數組進行增減操作(splice等),而這些操作是set無法捕捉的,所以覆蓋get、set顯然無法實現數組的響應式,而vue中采用的是直接覆蓋數組的原型鏈中會對數據本身改變的方法(push、shift、splice等),/src/core/observer/array.js整個文件就是對數據的特殊處理 最新的vue3中,使用了ES6proxy特性來替代這種覆蓋set、get實現響應式行為,這種模式同時也能夠處理Array

三、結尾

vue的源碼當然沒有如此簡單,很多東西文章都沒有涉及到,譬如說,通過上面的邏輯其實你可以發現,depwatcher其實是互相引用的,而js的垃圾回收是檢測變量引用的機制,所以如果是簡單的復制上文的邏輯,最終的這部分的內存其實是無法被回收的,需要你手動清除,當然vue中也做了這樣的處理(每個vm下其實有一個watcherList,用於記錄這個示例中所有使用到的watcher,再vm.destroy時,通過遍歷watcherList,再銷毀每一個watcher,而watcher中又會自己銷毀Dep),但是限於篇幅原因無法詳細介紹了。

最后

為了幫助大家更好溫習重點知識、更高效的准備面試,特別整理了《95頁前端學習筆記》電子稿文件。

主要內容包括html,css,html5,css3,JavaScript,正則表達式,函數,BOM,DOM,jQuery,AJAX,vue 等等。

👉點擊這里免費獲取👈

html5/css3

  • HTML5 的優勢

  • HTML5 廢棄元素

  • HTML5 新增元素

  • HTML5 表單相關元素和屬性

  • CSS3 新增選擇器

  • CSS3 新增屬性

  • 新增變形動畫屬性

  • 3D變形屬性

  • CSS3 的過渡屬性

  • CSS3 的動畫屬性

  • CSS3 新增多列屬性

  • CSS3新增單位

  • 彈性盒模型

JavaScript

  • JavaScript基礎

  • JavaScript數據類型

  • 算術運算

  • 強制轉換

  • 賦值運算

  • 關系運算

  • 邏輯運算

  • 三元運算

  • 分支循環

  • switch

  • while

  • do-while

  • for

  • break和continue

  • 數組

  • 數組方法

  • 二維數組

  • 字符串

正則表達式

  • 創建正則表達式

  • 元字符

  • 模式修飾符

  • 正則方法

  • 支持正則的 String方法

js對象

  • 定義對象

  • 對象的數據訪問

  • JSON

  • 內置對象

  • Math 方法

  • Date 方法

面向對象是一種編程思想

  • 定義對象
  • 原型和原型鏈
  • 原型鏈
  • 原型

常用的JavaScript設計模式

  • 單體模式

  • 工廠模式

  • 例模式

函數

  • 函數的定義

  • 局部變量和全局變量

  • 返回值

  • 匿名函數

  • 自運行函數

  • 閉包

BOM

  • BOM概述

  • window方法

  • frames [ ] 框架集

  • history 歷史記錄

  • location 定位

  • navigator 導航

  • screen 屏幕

  • document 文檔

DOM

  • DOM對象方法
  • 操作DOM間的關系
  • DOM節點屬性

事件

  • 事件分類

  • 事件對象

  • 事件流

  • 事件目標

  • 事件委派(delegate)

  • 事件監聽

jQuery

  • jQuery 選擇器

  • 屬性選擇器

  • 位置選擇器

  • 后代選擇器

  • 子代選擇器

  • 選擇器對象

  • 子元素

  • DOM操作

  • JQuery 事件

  • 容器適應

  • 標簽樣式操作

  • 滑動

  • 自定義動畫

AJAX

  • 工作原理
  • XMLHttpRequest對象
  • XML和HTML的區別
  • get() 和post()

HTTP

  • HTTP消息結構

  • url請求過程

性能優化

  • JavaScript代碼優化
  • 提升文件加載速度

webpack

  • webpack的特點

  • webpack的缺點

  • 安裝

  • webpack基本應用

  • 配置文件入門

vue

  • MVC模式

  • MVVM模式

  • 基礎語法

  • 實例屬性/方法

  • 生命周期

  • 計算屬性

  • 數組的更新檢查

  • 事件對象

  • Vue組件

  • 路由使用

  • 路由導航

  • 嵌套路由

  • 命名視圖

👉點擊這里免費獲取👈


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM