vue源碼解析流程總結

以前寫了三篇vue的源碼解析,響應式,虛擬dom,和模板編譯組件化.

這三篇是比較細的,這裏作個總結,先看總結,再看那三篇應該會更好,

這裏是把大概流程和前面的例子總結下.

一,首次渲染過程

  1. 首先咱們導入vue時會初始化實例成員,靜態成員vue

    1. 全局靜態例如config,options,內部工具方法,一些靜態方法例如set,nextTick,組件,指令,過濾器方法,而後原型方法例如:mount(內部調用mountComponent掛載),init,_render(方法裏默認調用了options裏的render,默認傳遞vm.$createElement提供給用戶傳入的render當 h函數,生成虛擬dom ,模板編譯出來的render內部使用的vm._c 不用傳遞進去這個),_update等,
    2. 在init初始化實例成員,例如options,_isVue,uid記錄, Vue.extend()初始化組件的構造函數,它繼承自vue全部原型方法,合併配置options.
  2. 實例化 new Vue(),這裏會調用 原型上定義的init方法;
  3. this._init()node

    1. 在這裏合併options配置,初始化生命週期變量,事件監聽自定義事件.
    2. 執行initRender函數(生成vm._c處理編譯生成render,生成vm.$createElement處理用戶傳入render)
    3. 執行鉤子回調,對傳入的data數據作響應式處理數組

      1. 劫持屬性
      2. 生成各個屬性節點的dep對象,dep對象用來通知watcher更新,而且劫持數組原型方法.
    4. 若是有計算屬性生成計算watcher,有偵聽器,生成偵聽watcher
    5. 生成watcher時會根據傳入的方法來決定是否去 取對應data中的值,若是傳入方法裏獲取值了,會觸發對應的咱們前面數據劫持的get方法,從而把咱們當前watcher添加到對應屬性的dep的subs數組中,若是當前屬性是子對象,對應子對象dep也須要添加watcher(set和數組時會用到).
    6. 而後觸發created建立完成的鉤子函數.
    7. 最後執行$mount掛載.
  4. vm.$mount();promise

    1. 這個方法會先查找options.render函數,看用戶有沒有傳入,沒有傳入的話,使用傳入的模板,調用compileToFunctions把模板轉換成render函數,這個render函數內部調用的是vm._c來處理模板編譯生成vNode
    2. 把生成的render賦值給options.render,,後續調用_render()時會從options取出render來調用,這個須要vue的編譯器版本.
    3. 用戶傳入了render的話,後續調用_render時就會直接調用用戶傳入的render,從options.render上獲取執行這會會使用傳入的vm.$createElement來當h函數生成虛擬dom,最後調用mountComponent來進行掛載.
  5. mountComponent主要功能瀏覽器

    1. 定義updateComponentapp

      1. 這個方法做用是更新界面_update(_render()) //render中編譯出的_c或 用戶傳入_$createElement生成虛擬dom
      2. _render()生成虛擬dom,_update()內部調用patchVnode 用來對比新舊vNode 來進行dom更新
      3. _render中會調用了對應的編譯vm._c或者vm._createElement,生成虛擬dom,在這個過程當中,會判斷若是裏面有自定義組件會調用 createComponent ,createComponent內部會調用extend()返回組件構造函數, 而且建立組件vnode, 而後註冊插件init鉤子,init鉤子裏作實例化組件,而後會調用繼承自Vue的init初始化方法,最後再調用mount(),生成渲染watcher,並把組件掛載到頁面上(vnode.elm,這裏驗證了一個組件對應一個渲染watcher)
    2. 建立渲染watcher實例,傳遞updateComponentdom

      1. 建立watcher實例時會傳入updateComponent方法,這裏初始化會調用傳入的函數,也就是updateComponent來更新界面.
      2. 在這個過程當中會 獲取 咱們前面data進行屬性劫持中的屬性,而後會觸發對應的get,來把渲染watcher添加到 對應屬性的dep的subs數組中.造成屬性dep和渲染watcher的互相依賴.(這樣就造成了一個觀察關係,在這裏一個渲染watcher,可能放入多個屬性dep的subs數組中,由於一個渲染watcher對應一個組件, 一個屬性中的dep的subs數組中也可能會放入多個不一樣watcher,例如同時存在渲染和計算||偵聽屬性的watcher
      3. 在這裏聲明下,一個組件對應一個渲染watcher.
  6. mounted最後執行這個鉤子,總體渲染完成
  7. 到此爲止 vue的首次渲染就完成了

二,響應式原理

前面講到了,咱們在new Vue()時調用init作了對數據data的劫持生成屬性對應的dep發佈者和對應的get和set方法,在實例化watcher時會把自身賦給Dep.target,而後獲取屬性值時再觸發對應的get,經過dep.depend()和 childOb.dep.depend(),來把當前的watcher添加到自身和子對象的dep的subs數組中. 同時watcher也記錄一下dep.id防止後續觸發get時重複添加.而後改變data中的屬性賦值時會觸發對應的set,set會判斷值是否改變,改變了的話賦給val,而後set 裏會判斷新賦值的值是不是對象,是的話繼續進行數據劫持observe,而後調用dep的notify方法,來調用dep的subs數組中的watcher的update方法.異步

  • updata方法中會調用queueWatcher方法函數

    • 這個方法,在這裏會使用watcher的id作一個對象的key來判斷,是否重複,不重複的話,把當前的watcher放入queue隊列中.
    • 而後來調用nextTick方法,傳入flushSchedulerQueue方法看成參數工具

      • flushSchedulerQueue方法的做用 是按watcher.id排序watcher,也就是建立順序(計算,偵聽,渲染)排序,而後清空前面用來重複添加對象key的id,再依次執行watcher.run()
      • watche.run裏執行了 this.get()也就是傳入的函數, 渲染watcher的話也就是updateComponent來調用 內部的_update(_render()),來生成Vnode和對比更新.若是是計算或偵聽watcher的話,執行完get()傳入的方法後,會執行cb傳入的回調。
      • watcher排序的做用以下:

        • 在這裏首先 組件從父組件更新到子組件 也就是說假若有多個渲染watcher 先更新父的渲染watcher 後執行的子的渲染watcher
        • 其次 組件的用戶監視程序在渲染監視程序以前運行 由於用戶觀察者在渲染觀察者以前建立 ,也就是說 每一級組件的計算和偵聽watcher是在渲染watcher以前執行的,由於渲染watcher中可能會用到 計算屬性.
        • 最後就是若是一個組件在父組件的監視程序運行期間被銷燬,它的觀察者能夠被跳過
    • 在這裏nextTick接收到傳入的函數後,生成一個匿名函數(匿名函數中執行當前傳入的函數,加了try catch的錯誤處理)放到一個 callbacks數組中,如今它並不會當即執行callbacks數組中的函數,而後pending屬性判斷是false,默認是false,若是是false的話,改成true標記爲本次的tick的任務,而後用Promise.resolve()生成一個promise的微任務then(flushCallbacks),掛在本次tick事件循環的最後, 在本輪tick事件循環的最後來執行微任務flushCallbacks回調,這個flushCallbacks回調的主要做用就是

      • pending狀態改成false,標記本輪tick結束
      • 生成callbacks數組的副本,而後依次執行callbacks中的函數.

異步promsie 若是瀏覽器不支持的話會降級成setTimeout

這裏也就體現了vue中的更新是異步的,批量的

這裏咱們用段僞代碼來推理一下它的更新流程

<div id="app">
    <p id="p" ref="p1">{{ msg }}</p>
    {{ name }}<br>
    {{ title }}<br>
  </div>
  <script src="../../dist/vue.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        msg: 'Hello nextTick',
        name: 'Vue.js',
        title: 'Title'
      },
      mounted() {
        this.msg = 'Hello Worlds'
        this.name = 'Hello snabbdom'
        this.title = 'Vue.js'
  
        Vue.nextTick(() => {
          console.log(this.$refs.p1.textContent)
        })
        this.msg = 'Hello'
      }
    })
  </script>
  1. 更新值,而後msg的dep.notify()//派發更新

    1. msg的dep.subs數組裏的watcher.update()
    2. 執行queueWatcher方法拿到watcher.id用個對象,記錄防止重複,不重複的話這個watcher放到queue隊列裏,(當前這個watcher是渲染watcher)
    3. 執行nextTick(flushSchedulerQueue) 而後僞代碼傳入的函數放入 callbacks.push(()=>{ flushSchedulerQueue() })
    4. 這時callbacks數組[執行queue中隊列watcher更新的方法也就是flushSchedulerQueue,]
    5. 而後這時pending爲false,改成true標記爲本次的tick處理 定義一個微任務promise掛在本次tick的最後,等待未來執行(flushCallbacks是then回調函數).
  2. 更新值,而後name.dep.notify()//派發更新

    1. name的dep.subs數組裏的watcher.update()
    2. 同上面同樣,可是watcher.id重複,同一個渲染watcher,退出queueWatcher方法 (這裏沒添加劇復的watcher,可是值已經更新了 val = 新值)
  3. 更新值,而後title的dep.notify()//派發更新

    1. title的dep.subs數組裏的watcher.update()
    2. 同上面同樣,可是watcher.id重複,同一個渲染watcher,退出queueWatcher方法(這裏沒添加劇復的watcher,可是值已經更新了 val = 新值)
  4. Vue.nextTick(回調)

    1. 執行nextTick(回調) 而後僞代碼傳入的函數放入 callbacks.push(()=>{ 回調() })
    2. 這時callbacks數組[執行queue中隊列watcher更新的方法也就是flushSchedulerQueue, 回調方法]
    3. 而後這時pending爲true 退出
  5. 更新值,而後msg的dep.notify()//派發更新

    1. msg的dep.subs數組裏的watcher.update()
    2. 同上面同樣,可是watcher.id重複,同一個渲染watcher,退出queueWatcher方法(這裏沒添加劇復的watcher,可是值已經更新了 val = 新值,這時的msg已是Hello 而不是Hello words)
  6. 本次tick最後了來執行屬於本次tick的微任務

    1. 執行flushCallbacks方法 callbacks數組中第一個方法是flushSchedulerQueue,這個方法執行queue隊列中的全部watcher的更新,咱們如今裏面就一個渲染watcher(由於id是相同的),執行渲染wtcher.
    2. 以後執行 步驟4 Vue.nextTick傳入的回調,這時渲染watcher已經執行完成了,內容已經改變了,而後執行回調再去獲取對應dom的textContent時就是咱們最後一次給msg賦值Hello.

這就是vue數據響應式的原理,以及它的更新過程,以及Vue.nextTick爲何能獲取到更新以後的dom值

三,Vue 中模板編譯的過程

咱們開篇提到過,在調用$mount 掛載時 會調用compileToFunctions 把template模板轉換成render函數(內部調用_c()生成虛擬dom)

這個方法主要做用:

  • 這裏會首先使用parse()方法把模板轉換成 ast 抽象語法樹,抽象語法樹是以js對象形式,用來以樹形的方式描述代碼結構,這個裏就包含了對模板和 v-for v-if ref,等的解析.(v-for ,v-if結構化指令只能在編譯階段處理,render函數裏 不會再解析模板,因此要使用js的for 和if).
  • 而後優化生成的 ast 抽象語法樹 ,標記靜態節點和靜態根節點

    • 檢測子節點是否有是純靜態節點的,一旦檢測到純靜態節點,那就是永遠不會更改的節點,提高爲常量,從新渲染的時候不在從新建立節點
    • 在 patch 的時候直接跳過靜態子樹
  • 而後把抽象語法樹生成字符串形式的 js 代碼 這個js字符串代碼是編譯出來,也就是render函數代碼,裏面用的_c生成虛擬dom
  • 而後把字符串形式的js代碼轉換成js方法 賦值給凡出對象的render屬性
  • 返回編譯生成的render函數
  • 而後把返回的render函數賦值給options.render 後續渲染時調用的_render()就是這個render

這就是模板的編譯渲染.

四,虛擬 DOM 中 Key 的做用和好處。

虛擬dom中的key 主要用來標記兩個節點是不是同一個,而後作新舊vNode對比使用,對比vnode的不一樣來更新老vNode,從而更新 對應的dom,由於在虛擬dom節點的對比時,節點對比規則會根據key來比較,新舊開始,新舊結束,舊開始新結束,舊結束新開始.
若是都不符合而後新的開始 在老的沒對比完的同級開始結束位置區間 找相同的 vnode 來跟新差別並根據須要移動位置.
這樣看的話 假如沒有帶Key,舉個例子

<ul>
      <li v-for="value in arr" 
      :key="value"
      >{{value}}</li>
    </ul>
    [a,b,c,d]
    //更新爲
    [a,x,b,c,d]

沒key的時候,對比開始第一個相同,key時undefined相等,標籤相同, 第2,3,4標籤相同,內容不一樣更新dom內容,第5個生成dom 插入d
也就是說 沒key時有 一個生成插入操做, 三個更新dom

有key的時候,會對比key,不會key出現undefined的狀況, 根據咱們上面說的規則,只須要執行一次x的生成插入操做.

從這個例子看出來,若是咱們 使用的列表,若是往不一樣位置插入數據時,沒有key的時候,更新的次數要遠遠大於有key的時候.因此使用列表時儘可能來使用Key.

此次總結就到這裏結束了.

相關文章
相關標籤/搜索