vue 源碼學習(一)入門和響應式原理

vue 版本爲 2.6.11 博客的篇幅有點大,若是以爲比較繁瑣的,能夠跳着看,裏面也沒有粘大量的源碼,我會吧git上源碼的連接貼出來,你們能夠對照着源碼連接或者把源碼下載下來對照着看

看了好久的vue 的源碼,也看了好多關於源碼的貼子,本身也嘗試了寫了好幾回vue源碼的帖子,一是以爲寫的沒有章法思路不夠清晰,二是以爲vue3都出了我如今寫vue2的源碼學習有點晚了因此沒有發表,後來想一想本身寫出來能夠沉澱一些東西,而且也給你們提供一個閱讀源碼的思路, 但願能寫出一篇對我和對你們都有益處的帖子html

首先設定目標, 咱們不可能一行不差的把vue的源碼都看一遍(若是每行都看很快就會失去方向和興趣),因此咱們要知道咱們看源碼的目標是什麼,以及看到什麼程度就認爲吧vue 的真正的核心代碼和思想都學會了。
我認爲把下面的這些點都瞭解的透徹了就算是真正的學到了vue 的精髓vue

  1. vdom
  2. compiler
  3. 響應式原理 (watch, computed,收集依賴,getter setter)
  4. 指令原理
  5. filter 原理
  6. vue2 和vue3 的核心區別點

上面的點就是咱們學習的目標,必定要先設定目標,要不就不知道咱們學習源碼的意義,學着學着就放棄了,咱們要學會以目標爲導向。node

首先咱們找到 vue 項目的入口而後咱們從我認爲vue中最重要的最核心的內容響應式原理開始學起react

找到入口

入口文件

若是想要知道如何找到入口請看 找入口的思路,若是以爲不必看能夠跳過 git

入口文件爲platforms/web/entry-runtime.js 或者 platforms/web/entry-runtime-with-compiler.js 前者爲不帶 compiler 的後者爲帶有 compiler 的,由於咱們的目標設定中有 compiler 的內容,因此咱們就選後者爲咱們本次源碼學習的入口,對文件的依賴進行分析找到 VUe 的構造函數文件在 core/instance/index.js 這個文件中github

function Vue (options) {  
  if (process.env.NODE_ENV !== 'production' &&  
    !(this instanceof Vue)  
  ) {  
    warn('Vue is a constructor and should be called with the `new` keyword')  
  }  
  this._init(options)  
}
initMixin(Vue)  // 添加 _init 方法  
stateMixin(Vue) // $set $delete $watch  $data  $props  
eventsMixin(Vue) //$on $once $off $emit  
lifecycleMixin(Vue) //_update  $forceUpdate  $destroy  
renderMixin(Vue) //$nextTick  _render

這個文件的主要做用是定義了Vue 並豐富了他的原型上的方法(我在上面的註釋中標註了每一個方法分別對應了添加了那些原型方法),看一下vue的構函數,發現只調用了 _init 方法,整個Vue的入口就是 _init 這個方法了web

_init 方法

咱們對 _init 方法的內容進行分析面試

Vue.prototype._init = function (options?: Object) {
  ...
 if (options && options._isComponent) {  
   initInternalComponent(vm, options)  
 } else {  
    vm.$options = mergeOptions(  
        resolveConstructorOptions(vm.constructor),  
  options || {},  
  vm)  
 }
 initLifecycle(vm)  
 initEvents(vm)  
 initRender(vm)  
 callHook(vm, 'beforeCreate')  
 initInjections(vm) // resolve injections before data/props  
 initState(vm)  
 initProvide(vm) // resolve provide after data/props  
 callHook(vm, 'created')
 if (vm.$options.el) {  
  vm.$mount(vm.$options.el)  
 }
  ...
}

我這裏省略了一部分源碼,你們能夠對照着本身下載下來的源碼看,這個 _init 方法作了作了好多的事情,咱們不可能在這裏一次說清楚,而且,咱們要知道咱們真正想要看的是什麼,找到這個文件的目的是爲了讓咱們更好的達成目標(固然不是說你們就不用看阿,只是不要太深究,由於每個方法都有特別深的調用鏈,若是看下去確定會失去方向跑偏的哦),經過語義上看,咱們要看的響應式原理應該從 initState 方法入手 往下深刻的學習(vue的命名規範很是的好,經過命名我就能輕鬆的看出來每一個方法實現的功能,也是咱們值得學習的地方)。express

找入口的思路

如何找到入口呢,其實寫框架和咱們日常寫代碼同樣,想一想,咱們若是接觸到一個陌生的項目應該如何找到項目的入口呢,確定是先看 package.json,而後分析其中的 script ,根據經驗和語義的判斷得出(你們也能夠看看vue 的開發者文檔之類的也能找到一些介紹,我以爲找一個入口不想太費時間全部就沒有那麼嚴謹的去看),vue 打包的命令應該爲 "build": "node scripts/build.js",,咱們如今分析這個文件,這個文件不用太仔細的看,若是真的想要學習 rollup 打包的話能夠深刻的看一看,咱們的目的是找到項目真真的入口,在這裏深刻的去學習只會把咱們帶偏。json

文件 scripts/build.js

let builds = require('./config').getAllBuilds()

文件 scripts/config.js

const builds = {
  ...
  'web-runtime-esm': {  
    entry: resolve('web/entry-runtime.js'),  
    dest: resolve('dist/vue.runtime.esm.js'), 
    ...
  },
  'web-full-dev': {  
    entry: resolve('web/entry-runtime-with-compiler.js'),  
    dest: resolve('dist/vue.js'),  
  },
  ...
}

上面我粘出來的代碼就是咱們所尋找的入口文件,我是如何定位到這兩個的呢,經過看package.json 中的 main,module,unpkg 這三個配置分別爲咱們使用vue 庫時候的 cjs,esm,cdn的引入文件,他們對應的entry就是入口文件了。

響應式原理

首先咱們看vue的官方文檔,對響應式原理的介紹很是的詳細(必定要看,必定要看,而且要記到內心,深入理解,面試的時候會問的哦 ),我這裏就不在多作贅述 官方文檔

首先用一句話來解釋響應式原理 當組件的數據發生變化的時候觸發組件的從新渲染
響應式原理核心的點有哪些,也就是咱們學習響應式原理最終要學會什麼,也就是咱們細分的 目標

  1. watcher
  2. observer
  3. getter, setter
  4. 依賴收集 (dependency collect)依賴和watcher的關係

我認爲吧上面的幾個點搞明白也就真正的搞明白了響應式原理

下面咱們從源碼入手開始分析響應式原理

原本準備從 _init 中調用的 _initState 中深刻分析的,可是其實你在看了 _initState 方法以後你會發現,這裏面大量的依賴了 watcher observer 和dep 這幾個類,因此咱們先把這幾個類徹底的搞清楚再看 _initState 方法這樣有助於咱們的閱讀

閱讀源碼不必定非要按照調用棧一層層的扒代碼學習,當你發現這部分代碼強烈的依賴另外一部分代碼的時候,能夠先搞懂他依賴的那一部分,這樣更方便咱們的理解。

咱們首先想一想 vue 的實現響應式原理的邏輯,再去看這三個類,若是咱們忘了核心的思想去看代碼會特別的迷茫,就像沒有需求寫代碼同樣,而且在咱們本身寫代碼的過程當中也是先想清楚了這個類要實現什麼功能,再動手去寫這個類,回顧一下思想, 大體流程是 爲data設置getter 和setter, getter 的時候收集依賴,setter 的時候調用依賴的回調。

源代碼通常篇幅比較大調用棧比較深,當遇到不理解的問題時回顧基本原理和目標,就不會丟了方向了。

總的關係

若是不想看代碼細節的同窗,能夠跳過下面的代碼細節,這裏總結了一下 Observer watcher 和dep 各自的主要功能和他們之間的關係

observer 主要功能,將傳入的data 轉換爲Observer data ,就是設置新的get 和set 方法

dep 對象主要是數據和 watcher 之間的一個橋樑,存放的是數據更新時須要調用的 watcher,並在數據更新的時候觸發全部watcher 的update

watcher 對象是監控數據變化並調用回調,與dep的關係是,dep觸發watcher 的更新,一個watcher能夠有多個dep

Observer

文件:core/observer/index.js

咱們大體的瀏覽這個中的內容,導出了一個 Observer類,defineReactive 和 observe 這個方法,其餘的方法先不看,等用到的時候再看。

observe方法

  1. 若是傳入的不是一個對象或者傳入的是一個 Vnode 就return
  2. 若是傳入的對象中包含 __ob__ob = value.__ob__
  3. 不然若是 shouldObserve==true 加上其餘的一些校驗經過的話 ob = new Observer(value)
  4. 若是 asRootData && obvmCount++ 若是是組件的跟data對象 記錄一下 vmCount

總的來講這個方法就是 new Observer 接下來咱們看一下Observer 類

Observer類

構造函數 constructor(value: any)

  1. this.value 的值爲傳入的value
  2. this.dep = new Dep()
  3. this.vmCount = 0 記錄依賴組件的數量
  4. 把this對象添加到 value 對象的 __ob__ 屬性上
  5. 若是value爲數組調用observeArray 不然調用 walk

walk(obj: Object) 方法

循環對象的keys,調用 defineReactive從新設置屬性的 get和set

observeArray(items: Array<any>) 方法

循環數組對象,調用observe(item[i]),吧數組中的每個對象都設置爲響應式數據

defineReactive方法
主要參數: obj 對象, key修改的字段
主要功能: 1.獲取當前傳入key對應值的 Observer 實例 2. 爲 傳入對象的key屬性設置get和set

咱們知道vue 響應式是在 get 的時候收集依賴,在set 的時候調用回調的,咱們看看這裏是如何收集和調用回調的呢?

const dep = new Dep()
...
let childOb = !shallow && observe(val)
...
get: function reactiveGetter() {  
  const value = getter ? getter.call(obj) : val  
  if (Dep.target) {  
    dep.depend()  
    if (childOb) {  
      childOb.dep.depend()  
      if (Array.isArray(value)) {  
        dependArray(value)  
      }  
    }  
  }  
  return value  
},
set: function reactiveSetter(newVal) {  
  const value = getter ? getter.call(obj) : val  
  /* eslint-disable no-self-compare */  
  if (newVal === value || (newVal !== newVal && value !== value)) {  
    return  
  }  
  /* eslint-enable no-self-compare */  
  if (process.env.NODE_ENV !== 'production' && customSetter) {  
    customSetter()  
  }  
  // #7981: for accessor properties without setter  
  if (getter && !setter) return  
  if (setter) {  
    setter.call(obj, newVal)  
  } else {  
    val = newVal  
  }  
  childOb = !shallow && observe(newVal)  
  dep.notify()  
}

get 方法調用 dep 的depend 的方法收集依賴
set 方法調用 dep 的 notify 發通知更改

總結:Observer 的使命就是吧data 轉換爲一個響應式的數據,數據添加 __ob__get set 方法收集依賴和通知更新

Dep 依賴

文件 core/observer/dep.js

屬性介紹 1. 是id從0 開始計數,每次new 一個新實例加一 2. subs:指訂閱了該dep的全部watcher 對象

方法介紹

addSub 添加訂閱者
removeSub 刪除指定訂閱者
depend 收集依賴
notify 通知更新,遍歷全部的訂閱者,調用訂閱者(watcher)的update方法

靜態變量target

當前的系統中的 watcher 對象 ,同一時刻只能有一個存在,經過調用 pushTarget 和 popTarget 這兩個方法來設置

Watcher

文件 core/observer/watcher.js

構造函數邏輯

  1. 若是爲 isRenderWatcher 狀況爲vm._watcher = this 賦值 (在render的時候會用到能夠先不關注)
  2. 把當前的實例放到 vm._watchers 對象中,vm._watchers中包含當前vm的全部 watcher對象
  3. 把options 的值賦值到 this 對象上,須要特別關注的幾個屬性

    1. deep
    2. lazy 爲true的時候不調用 get 方法, initComputed 的時候lazy爲true
    3. sync 表示的是同步更新,若是sync爲false watcher 對象則放到一個隊列中執行
  4. 把cb 賦值給 this.cb, cb爲 watcher 對象的回調,每次數據更新的時候都會調用
  5. 添加id屬性 watcher 對象的標識,從0 開始計數,每 new 一個實例 +1
  6. 添加active屬性 當前watcher 對象的狀態,若是active 爲false則當前watcher對象再也不生效
  7. 添加dirty 屬性 當lazy爲true 的時候 dirty 也爲true,lazy爲false 的時候不會直接調用get方法,dirty用來標識get是否被調用過,調用以後變爲 false
  8. 添加deps 屬性 watcher當前訂閱的全部dep 對象
  9. 添加 newDeps 屬性 從新運算以後當前 watcher 訂閱的全部dep 對象 (主要用於新老依賴作對比,清除dep中無用的subs)
  10. 添加 depIds 屬性 watcher當前訂閱的全部dep 對象的id
  11. newDepIds 屬性 從新運算以後當前 watcher 訂閱的全部dep 對象的id
  12. expression 用於異常處理的提示
  13. 添加getter 屬性,getter屬性由 expOrFn 轉化而來的一個 function ,若是expOrFn 的值爲一個function則getter 爲該方法,不然調用parsePath 方法將表達式轉換爲一個方法,此時getter方法的返回值爲監控的數據 (line 79~91)
  14. 添加value 屬性, value屬性爲 watcher 對象監控的數據, 當lazy 爲true(computed配置裏的watcher 對象的lazy爲true) 的時候爲undefined ,lazy爲false的時候調用 get 方法收集依賴並獲取value

get方法的邏輯

  1. 調用 dep 中的 pushTarget 方法把dep中的 Dep.target 設置爲當前的 watcher 對象,表示後面全部的依賴收集都收集到當前的 watcher對象上
  2. 調用 this.getter 方法實現依賴的收集,this.getter 方法執行過程當中全部被observer過的對象的依賴都會收集到當前的能夠 watcher 對象上,
  3. 在finally 方法中若是deep爲true 則調用traverse方法對最終的value進行遞歸,實現對全部的子屬性的依賴收集,最後調用 dep中的popTarget方法,關掉收集,並調用 cleanupDeps 方法

addDep方法的邏輯

  1. 爲當前的 watcher 對象添加newDepIds, 判斷當前watcher 中newDepIds 對象是否包含 dep的id,若是不包含,爲當前的watcher 對象的newDepIds 和newDeps 添加新的依賴
  2. 把當前的watcher對象添加到 dep 對象的subs屬性中 ,若是 this.depIds 中不存在 depid 則把這個當前的watcher 對象放到dep對象的subs 中

cleanupDeps方法的邏輯

get 方法調用的

  1. 對deps(老的依賴)進行循環,若是newDepIds中不包含循環項目,則說明當前watcher對象沒有依賴老的dep對象,則調用dep.removeSub(this) 把 dep subs中的當前watcher 清除
  2. 把 depIds 更新爲 newDepIds,把newDepIds 清空
  3. 把 deps 更新爲 newDeps, 把 newDeps 清空

update方法的邏輯

dep 的notify 方法中調用的

在監聽數據發生變化的時候更新,若是 lazy==truethis.dirty = true 不更新,若是sync爲true 則同步更新不然調用 queueWatcher(this)方法放入隊列執行

run方法

run方法主要是從新計算value, 並執行回調

evaluate 方法

這個方法只在lazy爲true的時候調用
計算value 的值, 並吧dirty 設置爲false

teardown方法

清空watcher 對象上的依賴,以及清空 dep 上的subs 的watcher 對象,而且吧 this.active 設置爲 false

下面咱們看 initState 方法

入口方法_init 中調用的initState 方法

export function initState(vm: Component) {  
  vm._watchers = []  
  const opts = vm.$options  
  if (opts.props) initProps(vm, opts.props)  
  if (opts.methods) initMethods(vm, opts.methods)  
  if (opts.data) {  
    initData(vm)  
  } else {  
    observe(vm._data = {}, true /* asRootData */)  
  }  
  if (opts.computed) initComputed(vm, opts.computed)  
  if (opts.watch && opts.watch !== nativeWatch) {  
    initWatch(vm, opts.watch)  
  }  
}

initState 方法定義了 _watchers 變量, 並調用 initProps, initMethods, initData ,initComputed, initWatcher 分別對 props, methods, data,computed,watcher進行初始化

initProps

  1. 定義 vm._props
  2. 遍歷 propsOptions 調用 defineReactive 把 propsData 中的數據轉化爲響應式數據 , 而後調用 proxy(vm, '_props', key) 這個把propsData 中的數據經過代理的方式掛到 vm 上

initData

  1. vm._data 賦值,若是 options.data是一個方法則調用 getData(data,vm) 不然直接返回 options.data , getData 方法主要是執行了 options.data方法
  2. 循環data的keys, 調用 proxy(vm, "_data", key) 方法,將_data上的屬性使用代理的方式掛到vm 上
  3. 調用 observe(data, true);方法吧 data對象轉換爲 observe 以後的對象

initComputed

  1. 定義了 vm._computedWatchers
  2. 循環 options.computed 將 computed 中的每個方法 new 一個 Watcher 對象放到了 vm._computedWatchers,須要注意的是 這裏 watcher 對象沒有回調,而且設置了 options的{lazy: true}這意味着,computed 的方法不是當即被調用的
  3. 調用 defineComputed 方法把 compute 上的每一個key 做爲一個變量掛到的 vm 上,變量的get方法是經過調用 createComputedGetter 這個方法返回的一個方法
  4. createComputedGetter 方法:根據傳入的key 調用_computedWatchers[key]的 get方法, 最後返回watcher 的 value

initWatch

循環 options.watcher而且調用 createWatcher ,createWatcher 又調用了vm.$watch, 這個方法 new 了一個 Watcher對象

initMethods

內容比較簡單主要是將 options.methods 中的方法掛載到 vm 對象上並別將 this 指向了 vm

跟Vue3 的區別

我尚未看vue3 的源碼等看完以後在補充上來

相關文章
相關標籤/搜索