Vue(v2.6.11)萬行源碼生啃,就硬剛!

前言

源碼閱讀可能會遲到,可是必定不會缺席!css

衆所周知,如下代碼就是 vue 的一種直接上手方式。經過 cdn 能夠在線打開 vue.js。一個文件,一萬行源碼,是萬千開發者賴以生存的利器,它究竟作了什麼?讓人品味。html

<html>
<head></head>
<body>
    <div id="app">
        {{ message }}
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
    var app = new Vue({
        el: '#app',
        data: {
            message: 'See Vue again!'
        },
    })
</script>
</html>
複製代碼

源碼cdn地址:cdn.jsdelivr.net/npm/vue/dis…,當下版本:v2.6.11。vue

本瓜選擇生啃的緣由是,能夠更自主地選擇代碼段分輕重來閱讀,一方面測試本身的掌握程度,一方面追求更直觀的源碼閱讀。node

固然你也能夠選擇在 github.com/vuejs/vue/t… 分模塊的閱讀,也能夠看各路大神的歸類整理。react

其實因爲本次任務量並不算小,爲了能堅持下來,本瓜將源碼儘可能按 500 行做爲一個模塊來造成一個 md 文件記錄(分解版本共 24 篇感興趣可移步),結合註釋、本身的理解、以及附上對應查詢連接來逐行細讀源碼,此篇爲合併版本webpack

目的:自我梳理,分享交流。ios

最佳閱讀方式推薦:先點贊👍再閱讀📖,靴靴靴靴😁c++

正文

第 1 行至第 10 行

// initgit

(
    function (global, factory) {
        typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = global || self, global.Vue = factory());
    }(
        this,
        function () {
            'use strict';
            //...核心代碼...
        }
    )
);
// 變形
if (typeof exports === 'object' && typeof module !== 'undefined') { // 檢查 CommonJS
    module.exports = factory()
} else {
    if (typeof define === 'function' && define.amd) { // AMD 異步模塊定義 檢查JavaScript依賴管理庫 require.js 的存在 [link](https://stackoverflow.com/questions/30953589/what-is-typeof-define-function-defineamd-used-for)
        define(factory)
    } else {
        (global = global || self, global.Vue = factory());
    }
}
// 等價於
window.Vue=factory() 
// factory 是個匿名函數,該匿名函數並沒自執行 設計參數 window,並傳入window對象。不污染全局變量,也不會被別的代碼污染
複製代碼

第 11 行至第 111 行

// 工具代碼es6

var emptyObject = Object.freeze({});// 凍結的對象沒法再更改 [link](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)
複製代碼

// 接下來是一些封裝用來判斷基本類型、引用類型、類型轉換的方法

  • isUndef//判斷未定義

  • isDef// 判斷已定義

  • isTrue// 判斷爲 true

  • isFalse// 判斷爲 false

  • isPrimitive// 判斷爲原始類型

  • isObject// 判斷爲 obj

  • toRawType // 切割引用類型獲得後面的基本類型,例如:[object RegExp] 獲得的就是 RegExp

  • isPlainObject// 判斷純粹的對象:"純粹的對象",就是經過 { }、new Object()、Object.create(null) 建立的對象

  • isRegExp// 判斷原生引用類型

  • isValidArrayIndex// 檢查val是不是一個有效的數組索引,其實就是驗證是不是一個非無窮大的正整數

  • isPromise// 判斷是不是 Promise

  • toString// 類型轉成 String

  • toNumber// 類型轉成 Number

第 113 行至第 354 行

  • makeMap// makeMap 方法將字符串切割,放到map中,用於校驗其中的某個字符串是否存在(區分大小寫)於map中 e.g.
var isBuiltInTag = makeMap('slot,component', true);// 是否爲內置標籤
isBuiltInTag('slot'); //true
isBuiltInTag('slot1'); //undefined
var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');// 是否爲保留屬性
複製代碼
  • remove// 數組移除元素方法

  • hasOwn// 判斷對象是否含有某個屬性

  • cached// ※高級函數 cached函數,輸入參數爲函數,返回值爲函數。同時使用了閉包,其會將該傳入的函數的運行結果緩存,建立一個cache對象用於緩存運行fn的運行結果。link

function cached(fn) {
    var cache = Object.create(null);// 建立一個空對象
    return (function cachedFn(str) {// 獲取緩存對象str屬性的值,若是該值存在,直接返回,不存在調用一次fn,而後將結果存放到緩存對象中
        var hit = cache[str];
        return hit || (cache[str] = fn(str))
    })
}
複製代碼
  • camelize// 駝峯化一個連字符鏈接的字符串

  • capitalize// 對一個字符串首字母大寫

  • hyphenateRE// 用字符號鏈接一個駝峯的字符串

  • polyfillBind// ※高級函數 參考link

  • Function.prototype.bind() // link1link2

  • toArray// 將像數組的轉爲真數組

  • extend// 將多個屬性插入目標的對象

  • toObject// 將對象數組合併爲單個對象。

e.g.

console.log(toObject(["bilibli"]))
//{0: "b", 1: "i", 2: "l", 3: "i", 4: "b", 5: "l", 6: "i", encodeHTML: ƒ}
複製代碼
  • no// 任何狀況都返回false

  • identity // 返回自身

  • genStaticKeys// 從編譯器模塊生成包含靜態鍵的字符串。TODO:demo

  • looseEqual//※高級函數 對對象的淺相等進行判斷

//有贊、頭條面試題

function looseEqual(a, b) {
    if (a === b) return true
    const isObjectA = isObject(a)
    const isObjectB = isObject(b)
    if(isObjectA && isObjectB) {
        try {
            const isArrayA = Array.isArray(a)
            const isArrayB = Array.isArray(b)
            if(isArrayA && isArrayB) {
                return a.length === b.length && a.every((e, i) => {
                    return looseEqual(e, b[i])
                })
            }else if(!isArrayA && !isArrayB) {
                const keysA = Object.keys(a)
                const keysB = Object.keys(b)
                return keysA.length === keysB.length && keys.every(key => {
                    return looseEqual(a[key], b[key])
                })
            }else {
                return false
            }
        } catch(e) {
            return false
        }
    }else if(!isObjectA && !isObjectB) {
        return String(a) === String(b)
    }else {
        return false
    }
}
複製代碼
  • looseIndexOf// 返回索引,若是沒找到返回-1,不然執行looseEqual()
  • once// 確保函數只被調用一次,用到閉包

階段小結

  • cached
  • polyfillBind
  • looseEqual

這三個函數要重點細品!主要的點是:閉包、類型判斷,函數之間的互相調用。也便是這部分工具函數的精華!

第 356 行 至 第 612 行

// 定義常量和配置

  • SSR_ATTR// 服務端渲染
  • ASSET_TYPES// 全局函數 component、directive、filter
  • LIFECYCLE_HOOKS// 生命週期,無需多言
  • config // 全局配置 link
  • unicodeRegExp//用於解析html標記、組件名稱和屬性pat的unicode字母
  • isReserved// 檢查變量的開頭是 $ 或 _
  • def// 在一個對象上定義一個屬性的構造函數,其中 !!enumerable 強制轉換 boolean
  • parsePath// 解析一個簡單路徑 TODO:
  • userAgent// 瀏覽器識別
  • inBrowser
  • _isServer//檢測 vue的服務器渲染是否存在, 並且避免webpack去填充process
  • isNative //這裏判斷 函數是不是系統函數, 好比 Function Object ExpReg window document 等等, 這些函數應該使用c/c++實現的。這樣能夠區分 Symbol是系統函數, 仍是用戶自定義了一個Symbol
  • hasSymbol//這裏使用了ES6的Reflect方法, 使用這個對象的目的是, 爲了保證訪問的是系統的原型方法, ownKeys 保證key的輸出順序, 先數組 後字符串
  • _Set// 設置一個Set

link

第 616 行至第 706 行

//設置warn,tip等全局變量 TODO:

  • warn
  • tip
  • generateComponentTrace// 生成組件跟蹤路徑(組件數規則)
  • formatComponentName// 格式化組件名

第 710 行至第 763 行

Vue核心:數據監聽最重要之一的 Dep

// Dep是訂閱者Watcher對應的數據依賴
var Dep = function Dep () {
  //每一個Dep都有惟一的ID
  this.id = uid++;
  //subs用於存放依賴
  this.subs = [];
};

//向subs數組添加依賴
Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
};
//移除依賴
Dep.prototype.removeSub = function removeSub (sub) {
  remove(this.subs, sub);
};
//設置某個Watcher的依賴
//這裏添加了Dep.target是否存在的判斷,目的是判斷是否是Watcher的構造函數調用
//也就是說判斷他是Watcher的this.get調用的,而不是普通調用
Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};

Dep.prototype.notify = function notify () {
  var subs = this.subs.slice();
  //通知全部綁定 Watcher。調用watcher的update()
  for (var i = 0, l = subs.length; i &lt; l; i++) {
    subs[i].update();
  }
};
複製代碼

強烈推薦閱讀:link

Dep 至關於把 Observe 監聽到的信號作一個收集(collect dependencies),而後經過dep.notify()再通知到對應 Watcher ,從而進行視圖更新。

第 767 行至第 900 行

Vue核心:視圖更新最重要的 VNode( Virtual DOM)

  • VNode
  • createEmptyVNode
  • createTextVNode
  • cloneVNode

把你的 template 模板 描述成 VNode,而後一系列操做以後經過 VNode 造成真實DOM進行掛載

更新的時候對比舊的VNode和新的VNode,只更新有變化的那一部分,提升視圖更新速度。

e.g.

<div class="parent" style="height:0" href="2222">
    111111
</div>

//轉成Vnode
{    

    tag: 'div',    

    data: {        

        attrs:{href:"2222"}

        staticClass: "parent",        

        staticStyle: {            

            height: "0"

        }
    },    

    children: [{        

        tag: undefined,        

        text: "111111"

    }]
}
複製代碼

強烈推薦閱讀:link

  • methodsToPatch

將數組的基本操做方法拓展,實現響應式,視圖更新。

由於:對於對象的修改是能夠直接觸發響應式的,可是對數組直接賦值,是沒法觸發的,可是用到這裏通過改造的方法。咱們能夠明顯的看到 ob.dep.notify() 這一核心。

階段小結

這一 part 最重要的,毋庸置疑是:Dep 和 VNode,需重點突破!!!

第 904 行至第 1073 行

Vue核心:數據監聽最重要之一的 Observer

  • 核心的核心!Observer(發佈者) => Dep(訂閱器) => Watcher(訂閱者)

類比一個生活場景:報社將各類時下熱點的新聞收集,而後製成各種報刊,發送到每家門口的郵箱裏,訂閱報刊人們看到了新聞,對新聞做出評論。

在這個場景裏,報社==發佈者,新聞==數據,郵箱==訂閱器,訂閱報刊的人==訂閱者,對新聞評論==視圖更新

  • Observer//Observer的調用過程:initState()-->observe(data)-->new Observer()
var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, '__ob__', this);
  if (Array.isArray(value)) {
    if (hasProto) {
      protoAugment(value, arrayMethods);
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};
複製代碼
  • ※※ defineReactive 函數,定義一個響應式對象,給對象動態添加 getter 和 setter ,用於依賴收集和派發更新。
function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()// 1. 爲屬性建立一個發佈者

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get // 依賴收集
  const setter = property && property.set // 派發更新
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)// 2. 獲取屬性值的__ob__屬性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()// 3. 添加 Dep
        if (childOb) {
          childOb.dep.depend()//4. 也爲屬性值添加一樣的 Dep 
          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()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
複製代碼

第 4 步很是重要。爲對象的屬性添加 dep.depend(),達到監聽對象(引用的值)屬性的目的

重點備註

Vue對數組的處理跟對象仍是有挺大的不一樣,length是數組的一個很重要的屬性,不管數組增長元素或者刪除元素(經過splice,push等方法操做)length的值一定會更新,爲何不直接操做監聽length呢?而須要攔截splice,push等方法進行數組的狀態更新?

緣由是:在數組length屬性上用defineProperty攔截的時候,會報錯。

Uncaught TypeError: Cannot redefine property: length
複製代碼

再用Object.getOwnPropertyDescriptor(arr, 'length')查看一下://(Object.getOwnPropertyDescriptor用於返回defineProperty.descriptor)

{ configurable: false enumerable: false value: 0 writable: true } configurable爲false,且MDN上也說重定義數組的length屬性在不一樣瀏覽器上表現也是不一致的,因此仍是老老實實攔截splice,push等方法,或者使用ES6的Proxy。

第 1075 行至第 1153 行

  • set //在對象上設置一個屬性。若是是新的屬性就會觸發更改通知(舊屬性也會觸發更新通知,由於第一個添加的時候已經監聽了,以後自動觸發,再也不手動觸發)
  • del //刪除一個屬性,若是必要觸發通知
  • dependArray // 收集數組的依賴 link

第 1157 行至第 1568 行

// 配置選項合併策略

ar strats = config.optionMergeStrategies;
複製代碼
  • mergeData
  • strats.data
  • mergeDataOrFn
  • mergeHook
  • mergeAssets
  • strats.watch
  • strats.computed
  • defaultStrat
  • checkComponents
  • validateComponentName
  • normalizeProps
  • normalizeInject
  • normalizeDirectives
  • assertObjectType
  • mergeOptions

這一部分代碼寫的就是父子組件配置項的合併策略,包括:默認的合併策略、鉤子函數的合併策略、filters/props、data合併策略,且包括標準的組件名、props寫法有一個統一化規範要求。

一圖以蔽之

強烈推薦閱讀:link

階段小結

這一部分最重要的就是 Observer(觀察者) ,這也是 Vue 核心中的核心!其次是 mergeOptions(組件配置項的合併策略),可是一般在用的過程當中,就已經瞭解到了大部分的策略規則。

第 1570 行至第 1754 行

  • resolveAsset// resolveAsset 全局註冊組件用到

e.g.

咱們的調用 resolveAsset(context.options, 'components', tag),即拿 vm.options.components[tag],這樣咱們就能夠在 resolveAsset 的時候拿到這個組件的構造函數,並做爲 createComponent 的鉤子的參數。

  • validateProp// prop的格式校驗

校驗prop:

  1. prop爲Boolean類型時作特殊處理
  2. prop的值爲空時,獲取默認值,並建立觀察者對象
  3. prop驗證
  • getPropDefaultValue// 獲取默認 prop 值

獲取 prop 的默認值 && 建立觀察者對象

  1. @param {*} vm vm 實例
  2. @param {*} prop 定義選項
  3. @param {*} vmkey prop 的 key

// 在非生產環境下(除去 Weex 的某種狀況),將對prop進行驗證,包括驗證required、type和自定義驗證函數。

  • assertProp //驗證 prop Assert whether a prop is valid.
case 1: 驗證 required 屬性
   case 1.1: prop 定義時是 required,可是調用組件時沒有傳遞該值(警告)
   case 1.2: prop 定義時是非 required 的,且 value === null || value === undefined(符合要求,返回)
case 2: 驗證 type 屬性-- value 的類型必須是 type 數組裏的其中之一
case 3: 驗證自定義驗證函數
複製代碼
  • assertType
`assertType`函數,驗證`prop`的值符合指定的`type`類型,分爲三類:
  - 第一類:經過`typeof`判斷的類型,如`String`、`Number`、`Boolean`、`Function`、`Symbol`
  - 第二類:經過`Object.prototype.toString`判斷`Object`/`Array`
  - 第三類:經過`instanceof`判斷自定義的引用類型
複製代碼

第 1756 行至第 1823 行

// 輔助函數:檢測內置類型

  • getType
  • isSameType
  • getTypeIndex
  • getInvalidTypeMessage
  • styleValue
  • isExplicable
  • isBoolean

第 1827 行至第 1901 行

// 輔助函數:處理錯誤、錯誤打印

  • handleError
  • invokeWithErrorHandling
  • globalHandleError
  • logError

第 1905 行至第 2007 行

  • flushCallbacks// flushCallbacks 挨個同步執行callbacks中回調
  • MutationObserver
  • nextTick// 把傳入的 cb 回調函數用 try-catch 包裹後放在一個匿名函數中推入callbacks數組中,這麼作是由於防止單個 cb 若是執行錯誤不至於讓整個JS線程掛掉,每一個 cb 都包裹是防止這些回調函數若是執行錯誤不會相互影響,好比前一個拋錯了後一個仍然能夠執行。

精髓中的精髓 —— nextTick

這裏有一段很重要的註釋

// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).

在vue2.5以前的版本中,nextTick基本上基於 micro task 來實現的,可是在某些狀況下 micro task 具備過高的優先級,而且可能在連續順序事件之間(例如#4521,#6690)或者甚至在同一事件的事件冒泡過程當中之間觸發(#6566)。可是若是所有都改爲 macro task,對一些有重繪和動畫的場景也會有性能影響,如 issue #6813。vue2.5以後版本提供的解決辦法是默認使用 micro task,但在須要時(例如在v-on附加的事件處理程序中)強制使用 macro task。
複製代碼

什麼意思呢?分析下面這段代碼。

<span id='name' ref='name'>{{ name }}</span>
<button @click='change'>change name</button>

methods: {
      change() {
          this.$nextTick(() => console.log('setter前:' + this.$refs.name.innerHTML))
          this.name = ' vue3 '
          console.log('同步方式:' + this.$refs.name.innerHTML)
          setTimeout(() => this.console("setTimeout方式:" + this.$refs.name.innerHTML))
          this.$nextTick(() => console.log('setter後:' + this.$refs.name.innerHTML))
          this.$nextTick().then(() => console.log('Promise方式:' + this.$refs.name.innerHTML))
      }
  }
//同步方式:vue2
//setter前:vue2
//setter後: vue3 
//Promise方式: vue3 
//setTimeout方式: vue3 
複製代碼
  1. 同步方式: 當把data中的name修改以後,此時會觸發name的 setter 中的 dep.notify 通知依賴本data的render watcher去 update,update 會把 flushSchedulerQueue 函數傳遞給 nextTick,render watcher在 flushSchedulerQueue 函數運行時 watcher.run 再走 diff -> patch 那一套重渲染 re-render 視圖,這個過程當中會從新依賴收集,這個過程是異步的;因此當咱們直接修改了name以後打印,這時異步的改動尚未被 patch 到視圖上,因此獲取視圖上的DOM元素仍是原來的內容。
  2. setter前: setter前爲何還打印原來的是原來內容呢,是由於 nextTick 在被調用的時候把回調挨個push進callbacks數組,以後執行的時候也是 for 循環出來挨個執行,因此是相似於隊列這樣一個概念,先入先出;在修改name以後,觸發把render watcher填入 schedulerQueue 隊列並把他的執行函數 flushSchedulerQueue 傳遞給 nextTick ,此時callbacks隊列中已經有了 setter前函數 了,由於這個 cb 是在 setter前函數 以後被push進callbacks隊列的,那麼先入先出的執行callbacks中回調的時候先執行 setter前函數,這時並未執行render watcher的 watcher.run,因此打印DOM元素仍然是原來的內容。
  3. setter後: setter後這時已經執行完 flushSchedulerQueue,這時render watcher已經把改動 patch 到視圖上,因此此時獲取DOM是改過以後的內容。
  4. Promise方式: 至關於 Promise.then 的方式執行這個函數,此時DOM已經更改。
  5. setTimeout方式: 最後執行macro task的任務,此時DOM已經更改。

備註:前文提過,在依賴收集原理的響應式化方法 defineReactive 中的 setter 訪問器中有派發更新 dep.notify() 方法,這個方法會挨個通知在 dep 的 subs 中收集的訂閱本身變更的 watchers 執行 update。

強烈推薦閱讀:link

0 行 至 2000 行小結

0 至 2000 行主要的內容是:

  1. 工具代碼
  2. 數據監聽:Obeserve,Dep
  3. Vnode
  4. nextTick

第 2011 行至第 2232 行

  • perf// performance
  • initProxy// 代理對象是es6的新特性,它主要用來自定義對象一些基本操做(如查找,賦值,枚舉等)。link

//proxy是一個強大的特性,爲咱們提供了不少"元編程"能力。

const handler = {
    get: function(obj, prop) {
        return prop in obj ? obj[prop] : 37;
    }
};

const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b);      // 1, undefined
console.log('c' in p, p.c); // false, 37
複製代碼

link

  • traverse// 遍歷:_traverse 深度遍歷,用於

traverse 對一個對象作深層遞歸遍歷,由於遍歷過程當中就是對一個子對象的訪問,會觸發它們的 getter 過程,這樣就能夠收集到依賴,也就是訂閱它們變化的 watcher,且遍歷過程當中會把子響應式對象經過它們的 dep id 記錄到 seenObjects,避免之後重複訪問。

  • normalizeEvent// normalizeEvents是針對v-model的處理,例如在IE下不支持change事件,只能用input事件代替。
  • createFnInvoker// 在初始構建實例時,舊節點是不存在的,此時會調用createFnInvoker函數對事件回調函數作一層封裝,因爲單個事件的回調能夠有多個,所以createFnInvoker的做用是對單個,多個回調事件統一封裝處理,返回一個當事件觸發時真正執行的匿名函數。
  • updateListeners// updateListeners的邏輯也很簡單,它會遍歷on事件對新節點事件綁定註冊事件,對舊節點移除事件監聽,它即要處理原生DOM事件的添加和移除,也要處理自定義事件的添加和移除,

階段小結

Vue 的事件機制

第 2236 行至第 2422 行

  • mergeVNodeHook// 重點 合併 VNode

// 把 hook 函數合併到 def.data.hook[hookey] 中,生成新的 invoker,createFnInvoker 方法

// vnode 本來定義了 init、prepatch、insert、destroy 四個鉤子函數,而 mergeVNodeHook 函數就是把一些新的鉤子函數合併進來,例如在 transition 過程當中合併的 insert 鉤子函數,就會合併到組件 vnode 的 insert 鉤子函數中,這樣當組件插入後,就會執行咱們定義的 enterHook 了。

  • extractPropsFromVNodeData// 抽取相應的從父組件上的prop
  • checkProp// 校驗 Prop
// The template compiler attempts to minimize the need for normalization by
    // statically analyzing the template at compile time.
    // 模板編譯器嘗試用最小的需求去規範:在編譯時,靜態分析模板

    // For plain HTML markup, normalization can be completely skipped because the
    // generated render function is guaranteed to return Array<VNode>. There are
    // two cases where extra normalization is needed:
    // 對於純 HTML 標籤,可跳過標準化,由於生成渲染函數必定會會返回 Vnode Array.有兩種狀況,須要額外去規範

    // 1. When the children contains components - because a functional component
    // may return an Array instead of a single root. In this case, just a simple
    // normalization is needed - if any child is an Array, we flatten the whole
    // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
    // because functional components already normalize their own children.
    // 當子級包含組件時-由於功能組件可能會返回Array而不是單個根。在這種狀況下,須要規範化-若是任何子級是Array,咱們將整個具備Array.prototype.concat的東西。保證只有1級深度,由於功能組件已經規範了本身的子代。

    // 2. When the children contains constructs that always generated nested Arrays,
    // e.g. <template>, <slot>, v-for, or when the children is provided by user
    // with hand-written render functions / JSX. In such cases a full normalization
    // is needed to cater to all possible types of children values.
    // 當子級包含始終生成嵌套數組的構造時,例如<template>,<slot>,v-for或用戶提供子代時,具備手寫的渲染功能/ JSX。在這種狀況下,徹底歸一化,才能知足全部可能類型的子代值。
複製代碼

Q:這一段話說的是什麼意思呢?

A:歸一化操做其實就是將多維的數組,合併轉換成一個一維的數組。在 Vue 中歸一化分爲三個級別,

  1. 不須要進行歸一化

  2. 只須要簡單的歸一化處理,將數組打平一層

  3. 徹底歸一化,將一個 N 層的 children 徹底打平爲一維數組

利用遞歸來處理的,同時處理了一些邊界狀況。

第 2426 行至第 2490 行

  • initProvide
  • initInjections
  • resolveInject

第 2497 行至第 2958 行

  • resolveSlots// Runtime helper for resolving raw children VNodes into a slot object.
  • isWhitespace
  • normalizeScopedSlots
  • normalizeScopedSlot
  • proxyNormalSlot
  • renderList// Runtime helper for rendering v-for lists.
  • renderSlot// Runtime helper for rendering <slot>
  • resolveFilter// Runtime helper for resolving filters
  • checkKeyCodes// Runtime helper for checking keyCodes from config.
  • bindObjectProps// Runtime helper for merging v-bind="object" into a VNode's data.
  • renderStatic// Runtime helper for rendering static trees.
  • markOnce// Runtime helper for v-once.

這一部分講的是輔助程序 —— Vue 的各種渲染方法,從字面意思中能夠知道一些方法的用途,這些方法用在Vue生成的渲染函數中。

  • installRenderHelpers// installRenderHelpers 用於執行以上。

第 2962 行至第 3515 行

  • FunctionalRenderContext// 建立一個包含渲染要素的函數
  • createFunctionalComponent

函數式組件的實現

Ctor,                                       //Ctro:組件的構造對象(Vue.extend()裏的那個Sub函數)
  propsData,                                  //propsData:父組件傳遞過來的數據(還未驗證)
  data,                                       //data:組件的數據
  contextVm,                                  //contextVm:Vue實例 
  children                                    //children:引用該組件時定義的子節點
複製代碼

// createFunctionalComponent 最後會執行咱們的 render 函數

特注:Vue 組件是 Vue 的核心之一

組件分爲:異步組件和函數式組件

這裏就是函數式組件相關

Vue提供了一種可讓組件變爲無狀態、無實例的函數化組件。從原理上說,通常子組件都會通過實例化的過程,而單純的函數組件並無這個過程,它能夠簡單理解爲一箇中間層,只處理數據,不建立實例,也是因爲這個行爲,它的渲染開銷會低不少。實際的應用場景是,當咱們須要在多個組件中選擇一個來代爲渲染,或者在將children,props,data等數據傳遞給子組件前進行數據處理時,咱們均可以用函數式組件來完成,它本質上也是對組件的一個外部包裝。

函數式組件會在組件的對象定義中,將functional屬性設置爲true,這個屬性是區別普通組件和函數式組件的關鍵。一樣的在遇到子組件佔位符時,會進入createComponent進行子組件Vnode的建立。**因爲functional屬性的存在,代碼會進入函數式組件的分支中,並返回createFunctionalComponent調用的結果。**注意,執行完createFunctionalComponent後,後續建立子Vnode的邏輯不會執行,這也是以後在建立真實節點過程當中不會有子Vnode去實例化子組件的緣由。(無實例)

官方說明

  • cloneAndMarkFunctionalResult
  • mergeProps
  • componentVNodeHooks
  • createComponent // createComponent 方法建立一個組件的 VNode。這 createComponent 是建立子組件的關鍵

// 建立組件的 VNode 時,若組件是函數式組件,則其 VNode 的建立過程將與普通組件有所區別。

  • createComponentInstanceForVnode // link

推薦閱讀:link

  • installComponentHooks // installComponentHooks就是把 componentVNodeHooks的鉤子函數合併到data.hook中,,在合併過程當中,若是某個時機的鉤子已經存在data.hook中,那麼經過執行mergeHook函數作合併勾子。

  • mergeHook$1

  • transformModel

  • createElement// 建立元素

  • _createElement

  • applyNS

  • registerDeepBindings

  • initRender // 初識渲染

link

階段小結

這一部分主要是圍繞 Vue 的組件的建立。Vue 將頁面劃分紅各種的組件,組件思想是 Vue 的精髓之一。

第 3517 行至第 3894 行

  • renderMixin // 引入視圖渲染混合函數

  • ensureCtor

  • createAsyncPlaceholder

  • resolveAsyncComponent

  • isAsyncPlaceholder

  • getFirstComponentChild

  • initEvents// 初始化事件

  • add

  • remove$1

  • createOnceHandler

  • updateComponentListeners

  • eventsMixin // 掛載事件響應相關方法

第 3898 行至第 4227 行

  • setActiveInstance
  • initLifecycle
  • lifecycleMixin// 掛載生命週期相關方法
  • mountComponent
  • updateChildComponent
  • isInInactiveTree
  • activateChildComponent
  • deactivateChildComponent
  • callHook

幾乎全部JS框架或插件的編寫都有一個相似的模式,即向全局輸出一個類或者說構造函數,經過建立實例來使用這個類的公開方法,或者使用類的靜態全局方法輔助實現功能。相信精通Jquery或編寫過Jquery插件的開發者會對這個模式很是熟悉。Vue.js也一模一樣,只是一開始接觸這個框架的時候對它所能實現的功能的感嘆蓋過了它也不過是一個內容較爲豐富和精緻的大型類的本質。

link

階段小結

這裏要對 js 的繼承有一個深入的理解。 link

  1. 類繼承
function Animal(){
    this.live=true;
}
function Dog(name){
    this.name=name
}
Dog.prototype=new Animal()
var dog1=new Dog("wangcai")
console.log(dog1)// Dog {name: "wangcai"}
console.log(dog1.live)// true
複製代碼
  1. 構造繼承
function Animal(name,color){
    this.name=name;
    this.color=color;}
function Dog(){
    Animal.apply(this,arguments)
}
var dog1=new Dog("wangcai","balck")
console.log(dog1)// Dog {name: "wangcai", color: "balck"}
複製代碼
  1. 組合繼承(類繼承 + 構造繼承)
function Animal(name,color){
    this.name=name;
    this.color=color;
    this.live=true;
}
function Dog(){
    Animal.apply(this, arguments);   
}
Dog.prototype=new Animal()
var dog1=new Dog("wangcai","black")
console.log(dog1)// Dog {name: "wangcai", color: "black", live: true}
複製代碼
  1. 寄生組合式繼承
  2. extend繼承

Vue 同 Jquery 同樣,本質也是一個大型的類庫。

// 定義Vue構造函數,形參options

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)
}
複製代碼

// 功能函數

// 引入初始化混合函數
import { initMixin } from './init'
// 引入狀態混合函數
import { stateMixin } from './state'
// 引入視圖渲染混合函數
import { renderMixin } from './render'
// 引入事件混合函數
import { eventsMixin } from './events'
// 引入生命週期混合函數
import { lifecycleMixin } from './lifecycle'
// 引入warn控制檯錯誤提示函數
import { warn } from '../util/index'
...

// 掛載初始化方法
initMixin(Vue)
// 掛載狀態處理相關方法
stateMixin(Vue)
// 掛載事件響應相關方法
eventsMixin(Vue)
// 掛載生命週期相關方法
lifecycleMixin(Vue)
// 掛載視圖渲染方法
renderMixin(Vue)
複製代碼

第 4231 行至第 4406 行

  • resetSchedulerState // 重置狀態
  • flushSchedulerQueue// 據變化最終會把flushSchedulerQueue傳入到nextTick中執行flushSchedulerQueue函數會遍歷執行watcher.run()方法,watcher.run()方法最終會完成視圖更新

vue中dom的更像並非實時的,當數據改變後,vue會把渲染watcher添加到異步隊列,異步執行,同步代碼執行完成後再統一修改dom。

  • callUpdatedHooks
  • queueActivatedComponent
  • callActivatedHooks
  • queueWatcher

link

第 4412 行至第 4614 行

  • Watcher// !important 重中之重的重點

這一 part 在 Watcher 的原型鏈上定義了get、addDep、cleanupDeps、update、run、evaluate、depend、teardown 方法,即 Watcher 的具體實現的一些方法,好比新增依賴、清除、更新試圖等。

每一個Vue組件都有一個對應的watcher,這個watcher將會在組件render的時候收集組件所依賴的數據,並在依賴有更新的時候,觸發組件從新渲染。

第 4618 行至第 5071 行

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }
    // 若是是Vue的實例,則不須要被observe
    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    // 第一步: options參數的處理
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // mergeOptions接下來咱們會詳細講哦~
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // 第二步: renderProxy
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // 第三步: vm的生命週期相關變量初始化
    initLifecycle(vm)
    
    // 第四步: vm的事件監聽初始化
    initEvents(vm)
    // 第五步: vm的編譯render初始化
    initRender(vm)
    // 第六步: vm的beforeCreate生命鉤子的回調
    callHook(vm, 'beforeCreate')
    // 第七步: vm在data/props初始化以前要進行綁定
    initInjections(vm) // resolve injections before data/props
    
    // 第八步: vm的sate狀態初始化
    initState(vm)
    // 第九步: vm在data/props以後要進行提供
    initProvide(vm) // resolve provide after data/props
    // 第十步: vm的created生命鉤子的回調
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }
    // 第十一步:render & mount
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
複製代碼

主要是爲咱們的Vue原型上定義一個方法_init。而後當咱們執行new Vue(options) 的時候,會調用這個方法。而這個_init方法的實現,即是咱們須要關注的地方。 前面定義vm實例都挺好理解的,主要咱們來看一下mergeOptions這個方法,其實Vue在實例化的過程當中,會在代碼運行後增長不少新的東西進去。咱們把咱們傳入的這個對象叫options,實例中咱們能夠經過vm.$options訪問到。

link

0 至 5000 行 總結

從 0 至 5000 行咱們能夠清晰看到 Vue 模板編譯的輪廓了。

  • 筆者將這一部分出現的關鍵詞進行按順序羅列:
  1. function (global, factory)
  2. 工具函數
  3. Dep
  4. Observe
  5. VNode
  6. nextTick
  7. 事件機制
  8. Render
  9. components
  10. Watcher

咱們能夠總結:Vue 的核心就是 VDOM !對 DOM 對象的操做調整爲操做 VNode 對象,採用 diff 算法比較差別,一次 patch。

render 的流程是:

  1. Vue使用HTML的Parser將HTML模板解析爲AST
  2. function render(){}
  3. Virtual DOM
  4. watcher將會在組件render的時候收集組件所依賴的數據,並在依賴有更新的時候,觸發組件從新渲染

推薦閱讀:link

第 5073 行至第 5446 行

// 定義 Vue 構造函數
function Vue (options) {
      if (!(this instanceof Vue)
      ) {
        warn('Vue is a constructor and should be called with the `new` keyword');
      }
      this._init(options);
    }

// 將 Vue 做爲參數傳遞給導入的五個方法
initMixin(Vue);// 初始化 Mixin
stateMixin(Vue);// 狀態 Mixin
eventsMixin(Vue);// 事件 Mixin
lifecycleMixin(Vue);// 生命週期 Mixin
renderMixin(Vue);// 渲染 Mixin
複製代碼

這一部分就是初始化函數的調用。

// 
Object.defineProperty(Vue.prototype, '$isServer', {
      get: isServerRendering
    });
複製代碼

爲何這麼寫?

Object.defineProperty能保護引入的庫不被從新賦值,若是你嘗試重寫,程序會拋出「TypeError: Cannot assign to read only property」的錯誤。

link-【譯】Vue框架引入JS庫的正確姿式

// 版本
Vue.version = '2.6.11';
複製代碼

階段小結

這一部分是 Vue index.js 的內容,包括 Vue 的整個掛在過程

  1. 先進入 initMixin(Vue),在prototype上掛載
Vue.prototype._init = function (options) {} 
複製代碼
  1. 進入 stateMixin(Vue),在prototype上掛載 Vue.prototype.$data
Vue.prototype.$props 
Vue.prototype.$set = set 
Vue.prototype.$delete = del 
Vue.prototype.$watch = function(){} 
複製代碼
  1. 進入eventsMixin(Vue),在prototype上掛載
Vue.prototype.$on 
Vue.prototype.$once 
Vue.prototype.$off 
Vue.prototype.$emit
複製代碼
  1. 進入lifecycleMixin(Vue),在prototype上掛載
Vue.prototype._update 
Vue.prototype.$forceUpdate 
Vue.prototype.$destroy
複製代碼
  1. 最後進入renderMixin(Vue),在prototype上掛載 Vue.prototype.$nextTick
Vue.prototype._render 
Vue.prototype._o = markOnce 
Vue.prototype._n = toNumber 
Vue.prototype._s = toString 
Vue.prototype._l = renderList 
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual 
Vue.prototype._i = looseIndexOf 
Vue.prototype._m = renderStatic 
Vue.prototype._f = resolveFilter 
Vue.prototype._k = checkKeyCodes 
Vue.prototype._b = bindObjectProps 
Vue.prototype._v = createTextVNode 
Vue.prototype._e = createEmptyVNode 
Vue.prototype._u = resolveScopedSlots 
Vue.prototype._g = bindObjectListeners
複製代碼

mergeOptions使用策略模式合併傳入的options和Vue.options合併後的代碼結構, 能夠看到經過合併策略components,directives,filters繼承了全局的, 這就是爲何全局註冊的能夠在任何地方使用,由於每一個實例都繼承了全局的, 因此都能找到。

推薦閱讀:

link

link

new 一個 Vue 對象發生了什麼:

第 5452 行至第 5655 行

// these are reserved for web because they are directly compiled away
// during template compilation

// 這些是爲web保留的,由於它們是直接編譯掉的
// 在模板編譯期間
複製代碼
  • isBooleanAttr
  • genClassForVnode// class 轉碼獲取vonde 中的staticClass 靜態class 和class動態class轉義成真實dom須要的class格式。而後返回class字符串
  • mergeClassData// mergeClassData
  • renderClass// 渲染calss 這裏獲取到已經轉碼的calss
  • stringifyClass// 轉碼 class,把數組格式,對象格式的calss 所有轉化成 字符串格式
  • stringifyArray// 數組字符串變成字符串,而後用空格 隔開 拼接 起來變成字符串
  • stringifyObject// 對象字符串變成字符串,而後用空格 隔開 拼接 起來變成字符串
  • namespaceMap
  • isHTMLTag
  • isSVG// 判斷svg 標籤
  • isUnknownElement// 檢查dom 節點的tag標籤 類型 是不是VPre 標籤 或者是判斷是不是瀏覽器自帶原有的標籤
  • isTextInputType // //匹配'text,number,password,search,email,tel,url'

這一 part 沒有特別要說的,主要是對 class 的轉碼、合併和其餘二次封裝的工具函數。實際上咱們在 Vue 源碼不少地方看到了這樣的封裝,在日常的開發中,咱們也得要求本身封裝基本的函數。若是能造成本身習慣用的函數的庫,會方便不少,且對本身能力也是一個提高。

第 5659 行至第 5792 行

  • createElement // 建立元素,實例化 VNode
  • createElementNS
  • createTextNode
  • createComment
  • insertBefore
  • removeChild
  • appendChild
  • parentNode
  • nextSibling
  • tagName
  • setTextContent
  • setStyleScope
  • nodeOps
// nodeOps:
    createElement: createElement$1, //建立一個真實的dom
    createElementNS: createElementNS, //建立一個真實的dom svg方式
    createTextNode: createTextNode, // 建立文本節點
    createComment: createComment,  // 建立一個註釋節點
    insertBefore: insertBefore,  //插入節點 在xxx  dom 前面插入一個節點
    removeChild: removeChild,   //刪除子節點
    appendChild: appendChild,  //添加子節點 尾部
    parentNode: parentNode,  //獲取父親子節點dom
    nextSibling: nextSibling,     //獲取下一個兄弟節點
    tagName: tagName,   //獲取dom標籤名稱
    setTextContent: setTextContent, //  //設置dom 文本
    setStyleScope: setStyleScope  //設置組建樣式的做用域
複製代碼
  • ref
  • registerRef // 註冊ref或者刪除ref。好比標籤上面設置了ref='abc' 那麼該函數就是爲this.$refs.abc 註冊ref 把真實的dom存進去

階段小結

這裏的重點想必就是 「ref」 了

在絕大多數狀況下,咱們最好不要觸達另外一個組件實例內部或手動操做 DOM 元素。不過也確實在一些狀況下作這些事情是合適的。ref 爲咱們提供瞭解決途徑。

ref屬性不是一個標準的HTML屬性,只是Vue中的一個屬性。

第 5794 行至第 6006 行

Virtual DOM !

沒錯,這裏就是 虛擬 dom 生成的源碼相關。

  • sameVnode
  • sameInputType
  • createKeyToOldIdx
  • createPatchFunction // !important:patch 把 vonde 渲染成真實的 dom
  • emptyNodeAt
  • createRmCb
  • removeNode
  • isUnknownElement$$1
  • createElm // 創造 dom 節點
  • createComponent // 建立組件,而且判斷它是否實例化過
  • initComponent

createElement方法接收一個tag參數,在內部會去判斷tag標籤的類型,從而去決定是建立一個普通的VNode仍是一個組件類VNode;

createComponent 的實現,在渲染一個組件的時候的 3 個關鍵邏輯:

  1. 構造子類構造函數,
  2. 安裝組件鉤子函數
  3. 實例化 vnode。createComponent 後返回的是組件 vnode,它也同樣走到 vm._update 方法

咱們傳入的 vnode 是組件渲染的 vnode,也就是咱們以前說的 vm._vnode,若是組件的根節點是個普通元素,那麼 vm._vnode 也是普通的 vnode,這裏 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值是 false。接下來的過程就係列一的步驟同樣了,先建立一個父節點佔位符,而後再遍歷全部子 VNode 遞歸調用 createElm,在遍歷的過程當中,若是遇到子 VNode 是一個組件的 VNode,則重複過程,這樣經過一個遞歸的方式就能夠完整地構建了整個組件樹。

initComponent 初始化組建,若是沒有tag標籤則去更新真實dom的屬性,若是有tag標籤,則註冊或者刪除ref 而後爲insertedVnodeQueue.push(vnode);

參考link

第 6008 行至第 6252 行

  • reactivateComponent
  • insert
  • createChildren
  • isPatchable
  • invokeCreateHooks
  • setScope
  • addVnodes // 添加 Vnodes
  • invokeDestroyHook
  • removeVnodes // 移除 Vnodes
  • removeAndInvokeRemoveHook
  • updateChildren // 在patchVnode中提到,若是新老節點都有子節點,可是不相同的時候就會調用 updateChildren,這個函數經過diff算法儘量的複用先前的DOM節點。

// diff 算法就在這裏辣!詳解link

function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, elmToMove, refElm 
    
    while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (isUndef(oldStartVnode)) {
            oldStartVnode = oldCh[++oldStartIdx]
        } else if (isUndef(oldEndVnode)) {
            oldEndVnode = oldCh[--oldEndIdx]
        } else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
            canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
            oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } else {
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
            idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
            if (isUndef(idxInOld)) {
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
                newStartVnode = newCh[++newStartIdx]
            } else {
                elmToMove = oldCh[idxInOld]
                if (sameVnode(elmToMove, newStartVnode)) {
                    patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
                    oldCh[idxInOld] = undefined
                    canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
                    newStartVnode = newCh[++newStartIdx]
                } else {
                    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
                    newStartVnode = newCh[++newStartIdx]
                }
            }
        }
    }
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
}
複製代碼
  • checkDuplicateKeys
  • findIdxInOld

reactivateComponent 承接上文 createComponent

第 6259 行至第 6561 行

  • patchVnode // 若是符合sameVnode,就不會渲染vnode從新建立DOM節點,而是在原有的DOM節點上進行修補,儘量複用原有的DOM節點。
  • invokeInsertHook
  • isRenderedModule
  • hydrate
  • assertNodeMatch
  • patch // !important: patch的本質是將新舊vnode進行比較,建立、刪除或者更新DOM節點/組件實例

階段小結

Vue 的核心思想:組件化。

這一部分是關於構建組件樹,造成虛擬 dom ,以及很是重要的 patch 方法。

再來億遍:

  1. 緣由:當修改某條數據的時候,這時候js會將整個DOM Tree進行替換,這種操做是至關消耗性能的。因此在Vue中引入了Vnode的概念:Vnode是對真實DOM節點的模擬,能夠對Vnode Tree進行增長節點、刪除節點和修改節點操做。這些過程都只須要操做VNode Tree,不須要操做真實的DOM,大大的提高了性能。修改以後使用diff算法計算出修改的最小單位,在將這些小單位的視圖進行更新。

  2. 原理:data中定義了一個變量a,而且模板中也使用了它,那麼這裏生成的Watcher就會加入到a的訂閱者列表中。當a發生改變時,對應的訂閱者收到變更信息,這時候就會觸發Watcher的update方法,實際update最後調用的就是在這裏聲明的updateComponent。 當數據發生改變時會觸發回調函數updateComponent,updateComponent是對patch過程的封裝。patch的本質是將新舊vnode進行比較,建立、刪除或者更新DOM節點/組件實例。

聯繫先後QA

Q:vue.js 同時多個賦值是一次性渲染仍是屢次渲染DOM?

A:官網已給出答案:cn.vuejs.org/v2/guide/re…

可能你尚未注意到,Vue 在更新 DOM 時是異步執行的。只要偵聽到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據變動。若是同一個 watcher 被屢次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免沒必要要的計算和 DOM 操做是很是重要的。而後,在下一個的事件循環「tick」中,Vue 刷新隊列並執行實際 (已去重的) 工做。Vue 在內部對異步隊列嘗試使用原生的 Promise.then、MutationObserver 和 setImmediate,若是執行環境不支持,則會採用 setTimeout(fn, 0) 代替。

例如,當你設置 vm.someData = 'new value',該組件不會當即從新渲染。當刷新隊列時,組件會在下一個事件循環「tick」中更新。多數狀況咱們不須要關心這個過程,可是若是你想基於更新後的 DOM 狀態來作點什麼,這就可能會有些棘手。雖然 Vue.js 一般鼓勵開發人員使用「數據驅動」的方式思考,避免直接接觸 DOM,可是有時咱們必需要這麼作。爲了在數據變化以後等待 Vue 完成更新 DOM,能夠在數據變化以後當即使用 Vue.nextTick(callback)。這樣回調函數將在 DOM 更新完成後被調用。

這樣是否是有種先後連貫起來的感受,原來 nextTick 是這樣子的。

第 6566 行至第 7069 行

  • directives // 官網:cn.vuejs.org/v2/guide/cu…

  • updateDirectives // 更新指令

  • _update

  • normalizeDirectives // 統一directives的格式

  • getRawDirName // 返回指令名稱 或者屬性name名稱+修飾符

  • callHook$1 //觸發指令鉤子函數

  • updateAttrs // 更新屬性

  • setAttr // 設置屬性

  • baseSetAttr

  • updateClass // 更新樣式

  • klass

  • parseFilters // 處理value 解析成正確的value,把過濾器 轉換成 vue 虛擬dom的解析方法函數 好比把過濾器 ' ab | c | d' 轉換成 _f("d")(_f("c")(ab))

  • wrapFilter // 轉換過濾器格式

  • baseWarn // 基礎警告

  • pluckModuleFunction //循環過濾數組或者對象的值,根據key循環 過濾對象或者數組[key]值,若是不存在則丟棄,若是有相同多個的key值,返回多個值的數組

  • addProp //在虛擬dom中添加prop屬性

  • addAttr //添加attrs屬性

  • addRawAttr //添加原始attr(在預轉換中使用)

  • addDirective //爲虛擬dom 添加一個 指令directives屬性 對象

  • addHandler // 爲虛擬dom添加events 事件對象屬性

前面圍繞「指令」和「過濾器」的一些基礎工具函數。

後面圍繞爲虛擬 dom 添加屬性、事件等具體實現函數。

第 7071 行至第 7298 行

  • getRawBindingAttr
  • getBindingAttr // 獲取 :屬性 或者v-bind:屬性,或者獲取屬性 移除傳進來的屬性name,而且返回獲取到 屬性的值
  • getAndRemoveAttr // 移除傳進來的屬性name,而且返回獲取到 屬性的值
  • getAndRemoveAttrByRegex
  • rangeSetItem
  • genComponentModel // 爲虛擬dom添加model屬性
/*
    * Parse a v-model expression into a base path and a final key segment.
    * Handles both dot-path and possible square brackets.
    * 將 v-model 表達式解析爲基路徑和最後一個鍵段。
    * 處理點路徑和可能的方括號。
    */
複製代碼
  • parseModel //轉義字符串對象拆分字符串對象 把後一位key分離出來

// 若是數據是object.info.name的狀況下 則返回是 {exp: "object.info",key: "name"} // 若是數據是object[info][name]的狀況下 則返回是 {exp: "object[info]",key: "name"}

  • next
  • eof
  • parseBracket //檢測 匹配[] 一對這樣的=括號
  • parseString // 循環匹配一對''或者""符號

這一部分包括:原生指令 v-bind 和爲虛擬 dom 添加 model 屬性,以及格式校驗工具函數。

第 7300 行至第 7473 行

  • model
  • genCheckboxModel // 爲input type="checkbox" 虛擬dom添加 change 函數 ,根據v-model是不是數組,調用change函數,調用 set 去更新 checked選中數據的值
  • genRadioModel // 爲虛擬dom inpu標籤 type === 'radio' 添加change 事件 更新值
  • genSelect // 爲虛擬dom添加change 函數 ,change 函數調用 set 去更新 select 選中數據的值
  • genDefaultModel // 若是虛擬dom標籤是 'input' 類型不是checkbox,radio 或者是'textarea' 標籤的時候,獲取真實的dom的value值調用 change或者input方法執行set方法更新數據

參考link

階段小結

  • v-bind、v-model

區別:

  1. v-bind 用來綁定數據和屬性以及表達式,縮寫爲':'
  2. v-model 使用在表單中,實現雙向數據綁定的,在表單元素外使用不起做用

Q:你知道v-model的原理嗎?說說看

A: v-model本質上是語法糖,即利用v-model綁定數據,其實就是既綁定了數據,又添加了一個input事件監聽 link

  • 自定義指令鉤子函數

一個指令定義對象能夠提供以下幾個鉤子函數 (均爲可選):

1. bind:只調用一次,指令第一次綁定到元素時調用。在這裏能夠進行一次性的初始化設置。
2. inserted:被綁定元素插入父節點時調用 (僅保證父節點存在,但不必定已被插入文檔中)。
3. update:所在組件的 VNode 更新時調用,可是可能發生在其子 VNode 更新以前。指令的值可能發生了改變,也可能沒有。可是你能夠經過比較更新先後的值來忽略沒必要要的模板更新 (詳細的鉤子函數參數見下)。
4. componentUpdated:指令所在組件的 VNode 及其子 VNode 所有更新後調用。
5. unbind:只調用一次,指令與元素解綁時調用。
複製代碼
  • 指令鉤子函數會被傳入如下參數:
1. el:指令所綁定的元素,能夠用來直接操做 DOM 。
2. binding:一個對象,包含如下屬性:
     name:指令名,不包括 v- 前綴。
     value:指令的綁定值,例如:v-my-directive="1 + 1" 中,綁定值爲 2。
     oldValue:指令綁定的前一個值,僅在 update 和 componentUpdated 鉤子中可用。不管值是否改變均可用。
     expression:字符串形式的指令表達式。例如 v-my-directive="1 + 1" 中,表達式爲 "1 + 1"。
     arg:傳給指令的參數,可選。例如 v-my-directive:foo 中,參數爲 "foo"。
     modifiers:一個包含修飾符的對象。例如:v-my-directive.foo.bar 中,修飾符對象爲 { foo: true, bar: true }。
3. vnode:Vue 編譯生成的虛擬節點。移步 VNode API 來了解更多詳情。
4. oldVnode:上一個虛擬節點,僅在 update 和 componentUpdated 鉤子中可用。
複製代碼

除了 el 以外,其它參數都應該是隻讀的,切勿進行修改。若是須要在鉤子之間共享數據,建議經過元素的 dataset 來進行。

【譯】vue 自定義指令的魅力

第 7473 行至第 7697 行

  • normalizeEvents // 爲事件 多添加 change 或者input 事件加進去
  • createOnceHandler$1
  • add$1 // 爲真實的dom添加事件
  • remove$2
  • updateDOMListeners // 更新dom事件
  • updateDOMProps // 更新真實dom的props屬性
  • shouldUpdateValue // 判斷是否須要更新value
  • isNotInFocusAndDirty
  • isDirtyWithModifiers // 判斷髒數據修改 髒數據概念

第 7699 行至第 7797 行

  • domProps
  • parseStyleText // 把style 字符串 轉換成對象
  • normalizeStyleData // 在同一個vnode上合併靜態和動態樣式數據
  • normalizeStyleBinding // 將可能的數組/字符串值規範化爲對象
  • getStyle
/**
    * parent component style should be after child's * so that parent component's style could override it
    * 父組件樣式應該在子組件樣式以後
    * 這樣父組件的樣式就能夠覆蓋它
    * 循環子組件和組件的樣式,把它所有合併到一個樣式對象中返回 樣式對象 如{width:100px,height:200px} 返回該字符串。
    */
複製代碼
  • setProp // 設置 prop

第 7799 行至第 7995 行

  • normalize // 給css加前綴。解決瀏覽器兼用性問題,加前綴
  • updateStyle // 將vonde虛擬dom的css 轉義成而且渲染到真實dom的csszhong
  • addClass // 爲真實dom 元素添加class類
  • removeClass // 刪除真實dom的css類名
  • resolveTransition // 解析vonde中的transition的name屬性獲取到一個css過分對象類
  • autoCssTransition // 經過 name 屬性獲取過渡 CSS 類名 好比標籤上面定義name是 fade css就要定義 .fade-enter-active,.fade-leave-active,.fade-enter,.fade-leave-to 這樣的class
  • nextFrame // 下一幀

第 7997 行至第 8093 行

  • addTransitionClass // 獲取 真實dom addTransitionClass 記錄calss類
  • removeTransitionClass // 刪除vonde的class類和刪除真實dom的class類
  • whenTransitionEnds // 獲取動畫的信息,執行動畫。
  • getTransitionInfo // 獲取transition,或者animation 動畫的類型,動畫個數,動畫執行時間

這一部分關於:對真實 dom 的操做,包括樣式的增刪、事件的增刪、動畫類等。

回過頭再理一下宏觀上的東西,再來億遍-虛擬DOM:模板 → 渲染函數 → 虛擬DOM樹 → 真實DOM

那麼這一部分則處在「虛擬DOM樹 → 真實DOM」這個階段

第 8093 行至第 8518 行

  • getTimeout
// Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers
// in a locale-dependent way, using a comma instead of a dot.
// If comma is not replaced with a dot, the input will be rounded down (i.e. acting
// as a floor function) causing unexpected behaviors

// 根據本地的依賴方式,Chromium 的舊版本(低於61.0.3163.100)格式化浮點數字,使用逗號而不是點。若是逗號未用點代替,則輸入將被四捨五入而致使意外行爲
複製代碼
// activeInstance will always be the <transition> component managing this
// transition. One edge case to check is when the <transition> is placed
// as the root node of a child component. In that case we need to check
// <transition>'s parent for appear check. // activeInstance 將一直做爲<transition>的組件來管理 transition。要檢查的一種邊緣狀況:<transition> 做爲子組件的根節點時。在這種狀況下,咱們須要檢查 <transition> 的父項的展示。 複製代碼
  • leave // 離開動畫

  • performLeave

  • checkDuration // only used in dev mode : 檢測 val 必需是數字

  • isValidDuration

  • getHookArgumentsLength // 檢測鉤子函數 fns 的長度

  • _enter

  • createPatchFunction // path 把vonde 渲染成真實的dom:建立虛擬 dom - 函數體在 5845 行

  • directive // 生命指令:包括 插入 和 組件更新

更新指令 比較 oldVnode 和 vnode,根據oldVnode和vnode的狀況 觸發指令鉤子函數bind,update,inserted,insert,componentUpdated,unbind鉤子函數

此節前部分是 transition 動畫相關工具函數,後部分關於虛擬 Dom patch、指令的更新。

第 8520 行至第 8584 行

  • setSelected // 設置選擇 - 指令更新的工具函數
  • actuallySetSelected // 實際選擇,在 setSelected() 裏調用
  • hasNoMatchingOption // 沒有匹配項 - 指令組件更新工具函數
  • getValue // 獲取 option.value
  • onCompositionStart // 組成開始 - 指令插入工具函數
  • onCompositionEnd // 組成結束-指令插入工具函數:防止無端觸發輸入事件
  • trigger // 觸發事件

第 8592 行至第 8728 行

// 定義在組件根內部遞歸搜索可能存在的 transition

  • locateNode
  • show // 控制 el 的 display 屬性
  • platformDirectives // 平臺指令
  • transitionProps // 過渡Props對象
// in case the child is also an abstract component, e.g. <keep-alive>
    // we want to recursively retrieve the real component to be rendered
    // 若是子對象也是抽象組件,例如<keep-alive>
    // 咱們要遞歸地檢索要渲染的實際組件
複製代碼
  • getRealChild
  • extractTransitionData // 提取 TransitionData
  • placeholder // 佔位提示
  • hasParentTransition // 判斷是否有 ParentTransition
  • isSameChild // 判斷子對象是否相同

第 8730 行至第 9020 行

  • Transition // !important

前部分以及此部分大部分圍繞 Transition 這個關鍵對象。即迎合官網 「過渡 & 動畫」 這一節,是咱們須要關注的重點!

Vue 在插入、更新或者移除 DOM 時,提供多種不一樣方式的應用過渡效果。包括如下工具:

  • 在 CSS 過渡和動畫中自動應用 class
  • 能夠配合使用第三方 CSS 動畫庫,如 Animate.css
  • 在過渡鉤子函數中使用 JavaScript 直接操做 DOM
  • 能夠配合使用第三方 JavaScript 動畫庫,如 Velocity.js

在這裏,咱們只會講到進入、離開和列表的過渡,你也能夠看下一節的管理過渡狀態

vue - transition 裏面大有東西,這裏有一篇「細談」推薦閱讀。

  • props
  • TransitionGroup // TransitionGroup
  • callPendingCbs // Pending 回調
  • recordPosition // 記錄位置
  • applyTranslation // 應用動畫 - TransitionGroup.updated 調用
// we divide the work into three loops to avoid mixing DOM reads and writes
// in each iteration - which helps prevent layout thrashing.

//咱們將工做分爲三個 loops,以免將 DOM 讀取和寫入混合在一塊兒
//在每次迭代中-有助於防止佈局衝撞。
複製代碼
  • platformComponents // 平臺組件
// 安裝平臺運行時指令和組件
extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);
複製代碼

Q: vue自帶的內置組件有什麼?

A: Vue中內置的組件有如下幾種:

  1. component

component組件:有兩個屬性---is inline-template

渲染一個‘元組件’爲動態組件,按照'is'特性的值來渲染成那個組件

  1. transition

transition組件:爲組件的載入和切換提供動畫效果,具備很是強的可定製性,支持16個屬性和12個事件

  1. transition-group

transition-group:做爲多個元素/組件的過渡效果

  1. keep-alive

keep-alive:包裹動態組件時,會緩存不活動的組件實例,而不是銷燬它們

  1. slot

slot:做爲組件模板之中的內容分發插槽,slot元素自身將被替換

第 9024 行至第 9207 行

// install platform specific utils // 安裝平臺特定的工具

  • Vue.config.x
Vue.config.mustUseProp = mustUseProp;
Vue.config.isReservedTag = isReservedTag;
Vue.config.isReservedAttr = isReservedAttr;
Vue.config.getTagNamespace = getTagNamespace;
Vue.config.isUnknownElement = isUnknownElement;
複製代碼
  • Vue.prototype.$mount // public mount method 安裝方法 實例方法掛載 vm
// public mount method
Vue.prototype.$mount = function (
    el, // 真實dom 或者是 string
    hydrating //新的虛擬dom vonde
) {
    el = el && inBrowser ? query(el) : undefined;
    return mountComponent(this, el, hydrating)
};
複製代碼

devtools global hook // 開發環境全局 hook Tip

  • buildRegex // 構建的正則匹配
  • parseText // 匹配view 指令,而且把他轉換成 虛擬dom vonde 須要渲染的函數,好比指令{{name}}轉換成 _s(name)
  • transformNode // 獲取 class 屬性和:class或者v-bind的動態屬性值,而且轉化成字符串 添加到staticClass和classBinding 屬性中
  • genData // 初始化擴展指令 baseDirectives,on,bind,cloak方法,dataGenFns 獲取到一個數組,數組中有兩個函數 genData(轉換 class) 和 genData$1(轉換 style),
  • transformNode$1 // transformNode$1 獲取 style屬性和:style或者v-bind的動態屬性值,而且轉化成字符串 添加到staticStyle和styleBinding屬性中
  • genData$1 // 參見 genData
  • style$1 // 包含 staticKeys、transformNode、genData 屬性

第 9211 行至第 9537 行

  • he
  • isUnaryTag // 工具函數
  • canBeLeftOpenTag // 工具函數
  • isNonPhrasingTag // 工具函數 Regular Expressions
  • parseHTML // 解析成 HTML !important

parseHTML 這個函數實現大概兩百多行,是一個比較大的函數體了。

parseHTML 中的方法用於處理HTML開始和結束標籤。

parseHTML 方法的總體邏輯是用正則判斷各類狀況,進行不一樣的處理。其中調用到了 options 中的自定義方法。

options 中的自定義方法用於處理AST語法樹,最終返回出整個AST語法樹對象。

貼一下源碼,有興趣可自行感覺一二。附一篇詳解Vue.js HTML解析細節學習

function parseHTML(html, options) {
    var stack = [];
    var expectHTML = options.expectHTML;
    var isUnaryTag$$1 = options.isUnaryTag || no;
    var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;
    var index = 0;
    var last, lastTag;
    while (html) {
        last = html;
        // 確保咱們不在像腳本/樣式這樣的純文本內容元素中
        if (!lastTag || !isPlainTextElement(lastTag)) {
            var textEnd = html.indexOf('<');
            if (textEnd === 0) {
                // Comment:
                if (comment.test(html)) {
                    var commentEnd = html.indexOf('-->');

                    if (commentEnd >= 0) {
                        if (options.shouldKeepComment) {
                            options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3);
                        }
                        advance(commentEnd + 3);
                        continue
                    }
                }

                // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
                if (conditionalComment.test(html)) {
                    var conditionalEnd = html.indexOf(']>');

                    if (conditionalEnd >= 0) {
                        advance(conditionalEnd + 2);
                        continue
                    }
                }

                // Doctype:
                // 匹配 html 的頭文件
                var doctypeMatch = html.match(doctype);
                if (doctypeMatch) {
                    advance(doctypeMatch[0].length);
                    continue
                }

                // End tag:
                var endTagMatch = html.match(endTag);
                if (endTagMatch) {
                    var curIndex = index;
                    advance(endTagMatch[0].length);
                    parseEndTag(endTagMatch[1], curIndex, index);
                    continue
                }

                // Start tag:
                // 解析開始標記
                var startTagMatch = parseStartTag();
                if (startTagMatch) {
                    handleStartTag(startTagMatch);
                    if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
                        advance(1);
                    }
                    continue
                }
            }

            var text = (void 0),
                rest = (void 0),
                next = (void 0);
            if (textEnd >= 0) {
                rest = html.slice(textEnd);
                while (
                    !endTag.test(rest) &&
                    !startTagOpen.test(rest) &&
                    !comment.test(rest) &&
                    !conditionalComment.test(rest)
                ) {
                    // < in plain text, be forgiving and treat it as text
                    next = rest.indexOf('<', 1);
                    if (next < 0) {
                        break
                    }
                    textEnd += next;
                    rest = html.slice(textEnd);
                }
                text = html.substring(0, textEnd);
            }

            if (textEnd < 0) {
                text = html;
            }

            if (text) {
                advance(text.length);
            }

            if (options.chars && text) {
                options.chars(text, index - text.length, index);
            }
        } else {
            //  處理是script,style,textarea
            var endTagLength = 0;
            var stackedTag = lastTag.toLowerCase();
            var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
            var rest$1 = html.replace(reStackedTag, function (all, text, endTag) {
                endTagLength = endTag.length;
                if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
                    text = text
                        .replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
                        .replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
                }
                if (shouldIgnoreFirstNewline(stackedTag, text)) {
                    text = text.slice(1);
                }
                if (options.chars) {
                    options.chars(text);
                }
                return ''
            });
            index += html.length - rest$1.length;
            html = rest$1;
            parseEndTag(stackedTag, index - endTagLength, index);
        }

        if (html === last) {
            options.chars && options.chars(html);
            if (!stack.length && options.warn) {
                options.warn(("Mal-formatted tag at end of template: \"" + html + "\""), {
                    start: index + html.length
                });
            }
            break
        }
    }

    // Clean up any remaining tags
    parseEndTag();

    function advance(n) {
        index += n;
        html = html.substring(n);
    }

    function parseStartTag() {
        var start = html.match(startTagOpen);
        if (start) {
            var match = {
                tagName: start[1],
                attrs: [],
                start: index
            };
            advance(start[0].length);
            var end, attr;
            while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
                attr.start = index;
                advance(attr[0].length);
                attr.end = index;
                match.attrs.push(attr);
            }
            if (end) {
                match.unarySlash = end[1];
                advance(end[0].length);
                match.end = index;
                return match
            }
        }
    }

    function handleStartTag(match) {
        var tagName = match.tagName;
        var unarySlash = match.unarySlash;

        if (expectHTML) {
            if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
                parseEndTag(lastTag);
            }
            if (canBeLeftOpenTag$$1(tagName) && lastTag === tagName) {
                parseEndTag(tagName);
            }
        }

        var unary = isUnaryTag$$1(tagName) || !!unarySlash;

        var l = match.attrs.length;
        var attrs = new Array(l);
        for (var i = 0; i < l; i++) {
            var args = match.attrs[i];
            var value = args[3] || args[4] || args[5] || '';
            var shouldDecodeNewlines = tagName === 'a' && args[1] === 'href' ?
                options.shouldDecodeNewlinesForHref :
                options.shouldDecodeNewlines;
            attrs[i] = {
                name: args[1],
                value: decodeAttr(value, shouldDecodeNewlines)
            };
            if (options.outputSourceRange) {
                attrs[i].start = args.start + args[0].match(/^\s*/).length;
                attrs[i].end = args.end;
            }
        }

        if (!unary) {
            stack.push({
                tag: tagName,
                lowerCasedTag: tagName.toLowerCase(),
                attrs: attrs,
                start: match.start,
                end: match.end
            });
            lastTag = tagName;
        }

        if (options.start) {
            options.start(tagName, attrs, unary, match.start, match.end);
        }
    }

    function parseEndTag(tagName, start, end) {
        var pos, lowerCasedTagName;
        if (start == null) {
            start = index;
        }
        if (end == null) {
            end = index;
        }

        // Find the closest opened tag of the same type
        if (tagName) {
            lowerCasedTagName = tagName.toLowerCase();
            for (pos = stack.length - 1; pos >= 0; pos--) {
                if (stack[pos].lowerCasedTag === lowerCasedTagName) {
                    break
                }
            }
        } else {
            // If no tag name is provided, clean shop
            pos = 0;
        }

        if (pos >= 0) {
            // Close all the open elements, up the stack
            for (var i = stack.length - 1; i >= pos; i--) {
                if (i > pos || !tagName &&
                    options.warn
                ) {
                    options.warn(
                        ("tag <" + (stack[i].tag) + "> has no matching end tag."), {
                            start: stack[i].start,
                            end: stack[i].end
                        }
                    );
                }
                if (options.end) {
                    options.end(stack[i].tag, start, end);
                }
            }

            // Remove the open elements from the stack
            stack.length = pos;
            lastTag = pos && stack[pos - 1].tag;
        } else if (lowerCasedTagName === 'br') {
            if (options.start) {
                options.start(tagName, [], true, start, end);
            }
        } else if (lowerCasedTagName === 'p') {
            if (options.start) {
                options.start(tagName, [], false, start, end);
            }
            if (options.end) {
                options.end(tagName, start, end);
            }
        }
    }
}
複製代碼

第 9541 行至第 9914 行

Regular Expressions // 相關正則

  • createASTElement // Convert HTML string to AST.
  • parse // !important

parse 函數從 9593 行至 9914 行,共三百多行。核心嗎?固然核心!

引自 wikipedia:

在計算機科學和語言學中,語法分析(英語:syntactic analysis,也叫 parsing)是根據某種給定的形式文法對由單詞序列(如英語單詞序列)構成的輸入文本進行分析並肯定其語法結構的一種過程。

語法分析器(parser)一般是做爲編譯器或解釋器的組件出現的,它的做用是進行語法檢查、並構建由輸入的單詞組成的數據結構(通常是語法分析樹、抽象語法樹等層次化的數據結構)。語法分析器一般使用一個獨立的詞法分析器從輸入字符流中分離出一個個的「單詞」,並將單詞流做爲其輸入。實際開發中,語法分析器能夠手工編寫,也可使用工具(半)自動生成。

parse 的總體流程實際上就是先處理了一些傳入的options,而後執行了parseHTML 函數,傳入了template,options和相關鉤子。

具體實現這裏盜一個圖:

parse中的語法分析能夠看這一篇這一節

  1. start
  2. char
  3. comment
  4. end

parse、optimize、codegen的核心思想解讀能夠看這一篇這一節

這裏實現的細節還真很多!

階段小結(重點)

噫噓唏!來到第 20 篇的小結!來個圖鎮一下先!

還記得官方這樣的一句話嗎?

下圖展現了實例的生命週期。你不須要立馬弄明白全部的東西,不過隨着你的不斷學習和使用,它的參考價值會愈來愈高。

看了這麼多,咱們再回頭看看註釋版。

註釋版
link

上圖值得一提的是:Has "template" option? 這個邏輯的細化

碰到是否有 template 選項時,會詢問是否要對 template 進行編譯:即模板經過編譯生成 AST,再由 AST 生成 Vue 的渲染函數,渲染函數結合數據生成 Virtual DOM 樹,對 Virtual DOM 進行 diff 和 patch 後生成新的UI。

如圖(此圖前文也有提到,見 0 至 5000 行總結):

將 Vue 的源碼的「數據監聽」、「虛擬 DOM」、「Render 函數」、「組件編譯」、結合好,則算是融會貫通了!

一圖勝萬言

好好把上面的三張圖看懂,便能作到「胸有成竹」,走遍天下的 VUE 原理面試都不用慌了。框架就在這裏,細化的東西就須要多多記憶了!

第 9916 行至第 10435 行

🙌 到 1w 行了,自我慶祝一下!

  • processRawAttrs // parse 方法裏用到的工具函數 用於將特性保存到AST對象的attrs屬性上
  • processElement// parse 方法工具函數 元素填充
export function processElement (
  element: ASTElement,
  options: CompilerOptions
) {
  processKey(element)

  // determine whether this is a plain element after
  // removing structural attributes
  element.plain = (
    !element.key &&
    !element.scopedSlots &&
    !element.attrsList.length
  )

  processRef(element)
  processSlotContent(element)
  processSlotOutlet(element)
  processComponent(element)
  for (let i = 0; i < transforms.length; i++) {
    element = transforms[i](element, options) || element
  }
  processAttrs(element)
  return element
}
複製代碼

能夠看到主要函數包括:processKey、processRef、processSlotContent、processSlotOutlet、processComponent、processAttrs 和最後遍歷執行的transforms。

processElement完成的slotTarget的賦值,這裏則是將全部的slot建立的astElement以對象的形式賦值給currentParent的scopedSlots。以便後期組件內部實例話的時候能夠方便去使用vm.$$slot。

  • processKey
  • processRef
  1. 首先最爲簡單的是processKey和processRef,在這兩個函數處理以前,咱們的key屬性和ref屬性都是保存在astElement上面的attrs和attrsMap,通過這兩個函數以後,attrs裏面的key和ref會被幹掉,變成astElement的直屬屬性。

  2. 探討一下slot的處理方式,咱們知道的是,slot的具體位置是在組件中定義的,而須要替換的內容又是組件外面嵌套的代碼,Vue對這兩塊的處理是分開的。

先說組件內的屬性摘取,主要是slot標籤的name屬性,這是processSlotOutLet完成的。

  • processFor
  • parseFor
  • processIf
  • processIfConditions
  • findPrevElement
  • addIfCondition
  • processOnce
  • processSlotContent
  • getSlotName
  • processSlotOutlet
// handle <slot/> outlets
function processSlotOutlet (el) {
  if (el.tag === 'slot') {
    el.slotName = getBindingAttr(el, 'name') // 就是這一句了。
    if (process.env.NODE_ENV !== 'production' && el.key) {
      warn(
        `\`key\` does not work on <slot> because slots are abstract outlets ` +
        `and can possibly expand into multiple elements. ` +
        `Use the key on a wrapping element instead.`,
        getRawBindingAttr(el, 'key')
      )
    }
  }
}
// 其次是摘取須要替換的內容,也就是 processSlotContent,這是是處理展現在組件內部的slot,可是在這個地方只是簡單的將給el添加兩個屬性做用域插槽的slotScope和 slotTarget,也就是目標slot。
複製代碼
  • processComponent // processComponent 並非處理component,而是摘取動態組件的is屬性。 processAttrs是獲取全部的屬性和動態屬性。
  • processAttrs
  • checkInFor
  • parseModifiers
  • makeAttrsMap

這一部分還是銜接這 parse function 裏的具體實現:start、end、comment、chars四大函數。

流程再回顧一下:

1、普通標籤處理流程描述

  1. 識別開始標籤,生成匹配結構match。

const match = { // 匹配startTag的數據結構 tagName: 'div', attrs: [ { 'id="xxx"','id','=','xxx' }, ... ], start: index, end: xxx } 複製代碼 2. 處理attrs,將數組處理成 {name:'xxx',value:'xxx'} 3. 生成astElement,處理for,if和once的標籤。 4. 識別結束標籤,將沒有閉合標籤的元素一塊兒處理。 5. 創建父子關係,最後再對astElement作全部跟Vue 屬性相關對處理。slot、component等等。

2、文本或表達式的處理流程描述。

  1. 截取符號<以前的字符串,這裏必定是全部的匹配規則都沒有匹配上,只多是文本了。
  2. 使用chars函數處理該字符串。
  3. 判斷字符串是否含有delimiters,默認也就是${},有的話建立type爲2的節點,不然type爲3.

3、註釋流程描述

  1. 匹配註釋符號。
  2. 使用comment函數處理。
  3. 直接建立type爲3的節點。

參考 link

階段小結

parseHTML() 和 parse() 這兩個函數佔了很大的篇幅,值得重點去看看。的確也不少細節,一些正則的匹配,字符串的操做等。從宏觀上把握從 template 到 vnode 的 parse 流程也無大問題。

第 10437 行至第 10605 行

  • isTextTag // function chars() 裏的工具函數
  • isForbiddenTag // function parseHTML() 用到的工具函數用於檢查元素標籤是否合法(不是保留命名)
  • guardIESVGBug // parse start() 中用到的工具函數
  • checkForAliasModel // checkForAliasModel用於檢查v-model的參數是不是v-for的迭代對象
  • preTransformNode // preTransformNode 方法對el進行預處理,便於後續對標籤上的指令和屬性進行處理,而後進行樹結構的構建,肯定el的root, parent, children等屬性。總結下來就是生成樹節點,構建樹結構(關聯樹節點)。
  • cloneASTElement // 轉換屬性,把數組屬性轉換成對象屬性,返回對象 AST元素
  • text // 爲虛擬dom添加textContent 屬性
  • html // 爲虛擬dom添加innerHTML 屬性
  • baseOptions
var baseOptions = {
  expectHTML: true, //標誌 是html
  modules: modules$1, //爲虛擬dom添加staticClass,classBinding,staticStyle,styleBinding,for,
                      //alias,iterator1,iterator2,addRawAttr ,type ,key, ref,slotName
                      //或者slotScope或者slot,component或者inlineTemplate ,plain,ifelse,elseif 屬性
  directives: directives$1, //根據判斷虛擬dom的標籤類型是什麼?給相應的標籤綁定 相應的 v-model 雙數據綁定代碼函數,
                            //爲虛擬dom添加textContent 屬性,爲虛擬dom添加innerHTML 屬性
  isPreTag: isPreTag, // 判斷標籤是不是 pre
  isUnaryTag: isUnaryTag, // 匹配標籤是不是area,base,br,col,embed,frame,hr,img,input,
                          // isindex,keygen, link,meta,param,source,track,wbr
  mustUseProp: mustUseProp,
  canBeLeftOpenTag: canBeLeftOpenTag,// 判斷標籤是不是 colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source
  isReservedTag: isReservedTag, // 保留標籤 判斷是否是真的是 html 原有的標籤 或者svg標籤
  getTagNamespace: getTagNamespace, // 判斷 tag 是不是svg或者math 標籤
  staticKeys: genStaticKeys(modules$1) // 把數組對象 [{ staticKeys:1},{staticKeys:2},{staticKeys:3}]鏈接數組對象中的 staticKeys key值,鏈接成一個字符串 str=‘1,2,3’
};
複製代碼
  • genStaticKeysCached

第 10607 行至第 10731 行

/**
  * Goal of the optimizer: walk the generated template AST tree
  * and detect sub-trees that are purely static, i.e. parts of
  * the DOM that never needs to change.
  *
  * Once we detect these sub-trees, we can:
  *
  * 1. Hoist them into constants, so that we no longer need to
  *    create fresh nodes for them on each re-render;
  * 2. Completely skip them in the patching process.
  */
  // 優化器的目標:遍歷生成的模板AST樹檢測純靜態的子樹,即永遠不須要更改的DOM。
  // 一旦咱們檢測到這些子樹,咱們能夠:
  // 1。把它們變成常數,這樣咱們就不須要了
  // 在每次從新渲染時爲它們建立新的節點;
  // 2。在修補過程當中徹底跳過它們。
複製代碼
  • optimize // !important:過 parse 過程後,會輸出生成 AST 樹,接下來須要對這顆樹作優化。即這裏的 optimize // 循環遞歸虛擬node,標記是否是靜態節點 // 根據node.static或者 node.once 標記staticRoot的狀態
  • genStaticKeys$1
  • markStatic$1 // 標準靜態節點
  • markStaticRoots // 標註靜態根(重要)
  • isStatic // isBuiltInTag(即tag爲component 和slot)的節點不會被標註爲靜態節點,isPlatformReservedTag(即平臺原生標籤,web 端如 h1 、div標籤等)也不會被標註爲靜態節點。
  • isDirectChildOfTemplateFor

階段小結

簡單來講:整個 optimize 的過程實際上就幹 2 件事情,markStatic(root) 標記靜態節點 ,markStaticRoots(root, false) 標記靜態根節點。

那麼被判斷爲靜態根節點的條件是什麼?

  1. 該節點的全部子孫節點都是靜態節點(判斷爲靜態節點要知足 7 個判斷,詳見

  2. 必須存在子節點

  3. 子節點不能只有一個純文本節點

其實,markStaticRoots()方法針對的都是普通標籤節點。表達式節點與純文本節點都不在考慮範圍內。

markStatic()得出的static屬性,在該方法中用上了。將每一個節點都判斷了一遍static屬性以後,就能夠更快地肯定靜態根節點:經過判斷對應節點是不是靜態節點 且 內部有子元素 且 單一子節點的元素類型不是文本類型。

只有純文本子節點時,他是靜態節點,但不是靜態根節點。靜態根節點是 optimize 優化的條件,沒有靜態根節點,說明這部分不會被優化。

Q:爲何子節點的元素類型是靜態文本類型,就會給 optimize 過程加大成本呢?

A:optimize 過程當中作這個靜態根節點的優化目是:在 patch 過程當中,減小沒必要要的比對過程,加速更新。可是須要如下成本

  1. 維護靜態模板的存儲對象 一開始的時候,全部的靜態根節點 都會被解析生成 VNode,而且被存在一個緩存對象中,就在 Vue.proto._staticTree 中。 隨着靜態根節點的增長,這個存儲對象也會愈來愈大,那麼佔用的內存就會愈來愈多 勢必要減小一些沒必要要的存儲,全部只有純文本的靜態根節點就被排除了

  2. 多層render函數調用 這個過程涉及到實際操做更新的過程。在實際render 的過程當中,針對靜態節點的操做也須要調用對應的靜態節點渲染函數,作必定的判斷邏輯。這裏須要必定的消耗。

純文本直接對比便可,不進行 optimize 將會更高效。

參考link

第 10733 行至第 10915 行

// KeyboardEvent.keyCode aliases

  • keyCodes // 內置按鍵
  • keyNames
  • genGuard // genGuard = condition => if(${condition})return null;
  • modifierCode //m odifierCode生成內置修飾符的處理
  • genHandlers
  • genHandler // 調用genHandler處理events[name],events[name]多是數組也多是獨立對象,取決於name是否有多個處理函數。
  • genKeyFilter // genKeyFilter用於生成一段過濾的字符串:
  • genFilterCode // 在 genKeyFilter 裏被調用
  • on
  • bind$1
  • baseDirectives // CodegenState 裏的工具函數

無論是組件仍是普通標籤,事件處理代碼都在genData的過程當中,和以前分析原生事件一致,genHandlers用來處理事件對象並拼接成字符串。

第 10921 行至第 11460 行

// generate(ast, options)

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}
複製代碼
  • CodegenState
  • generate // !important
  • genElement
export function genElement (el: ASTElement, 
state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }

  if (el.staticRoot && !el.staticProcessed) {
    // 若是是一個靜態的樹, 如 <div id="app">123</div>
    // 生成_m()方法
    // 靜態的渲染函數被保存至staticRenderFns屬性中
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    // v-once 轉化爲_o()方法
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    // _l()
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    // v-if 會轉換爲表達式
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    // 若是是template,處理子節點
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    // 若是是插槽,處理slot
    return genSlot(el, state)
  } else {
    // component or element
    let code
    // 若是是組件,處理組件
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el, state)
      }

      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

複製代碼
  • genStatic // genStatic會將ast轉化爲_m()方法
  • genOnce // 若是v-once在v-for中,那麼就會生成_o()方法, 不然將其視爲靜態節點
  • genIf // genIf會將v-if轉換爲表達式,示例以下
  • genIfConditions
  • genFor // v-for會轉換爲_l()
  • genData$2
  • genDirectives // genData() 裏調用
  • genInlineTemplate // genData() 裏調用
  • genScopedSlots // genData() 裏調用
  • genScopedSlot
  • genChildren // 處理子節點
  • getNormalizationType // 用於判斷是否須要規範化
  • genNode // 處理 Node
  • genText // 處理 Text
  • genComment
  • genSlot // 處理插槽
  • genComponent // 處理組件
  • genProps // 處理 props
  • transformSpecialNewlines

這裏面的邏輯、細節太多了,不作贅述,有興趣瞭解的童鞋能夠去看推薦閱讀

階段小結

generate方法內部邏輯仍是很複雜的,但僅作了一件事情,就是將ast轉化爲render函數的字符串,造成一個嵌套結構的方法,模版編譯生成的_c(),_m(),_l等等其實都是生成vnode的方法,在執行vue.$mount方法的時候,會調用vm._update(vm._render(), hydrating)方法,此時_render()中方法會執行生成的render()函數,執行後會生成vnode,也就是虛擬dom節點。

第 11466 行至第 11965 行

  • prohibitedKeywordRE // 正則校驗:禁止關鍵字
  • unaryOperatorsRE // 正則校驗:一元表達式操做
  • stripStringRE // 正則校驗:腳本字符串
  • detectErrors // 檢測錯誤工具函數
  • checkNode // 檢查 Node
  • checkEvent // 檢查 Event
  • checkFor // 檢查 For 循環
  • checkIdentifier // 檢查 Identifier
  • checkExpression // 檢查表達式
  • checkFunctionParameterExpression // 檢查函數表達式
  • generateCodeFrame
  • repeat$1
  • createFunction // 構建函數
  • createCompileToFunctionFn // 構建編譯函數
  • compile // !important
return function createCompiler (baseOptions) {
function compile (
    template,
    options
) {
    var finalOptions = Object.create(baseOptions);
    var errors = [];
    var tips = [];

    var warn = function (msg, range, tip) {
    (tip ? tips : errors).push(msg);
    };

    if (options) {
    if (options.outputSourceRange) {
        // $flow-disable-line
        var leadingSpaceLength = template.match(/^\s*/)[0].length;

        warn = function (msg, range, tip) {
        var data = { msg: msg };
        if (range) {
            if (range.start != null) {
            data.start = range.start + leadingSpaceLength;
            }
            if (range.end != null) {
            data.end = range.end + leadingSpaceLength;
            }
        }
        (tip ? tips : errors).push(data);
        };
    }
    // merge custom modules
    if (options.modules) {
        finalOptions.modules =
        (baseOptions.modules || []).concat(options.modules);
    }
    // merge custom directives
    if (options.directives) {
        finalOptions.directives = extend(
        Object.create(baseOptions.directives || null),
        options.directives
        );
    }
    // copy other options
    for (var key in options) {
        if (key !== 'modules' && key !== 'directives') {
        finalOptions[key] = options[key];
        }
    }
    }

    finalOptions.warn = warn;

    var compiled = baseCompile(template.trim(), finalOptions);
    {
    detectErrors(compiled.ast, warn);
    }
    compiled.errors = errors;
    compiled.tips = tips;
    return compiled
}
複製代碼

再看這張圖,對於「模板編譯」是否是有一種新的感受了。

  • compileToFunctions

// 最後的最後

return Vue;
複製代碼

哇!歷時一個月左右,我終於完成啦!!!

完結撒花🎉🎉🎉!激動 + 釋然 + 感恩 + 小知足 + ...... ✿✿ヽ(°▽°)ノ✿

這生啃給我牙齒都啃酸了!!

總結

emmm,原本打算再多修補一下,可是看到 vue3 的源碼解析已有版本出來啦(扶朕起來,朕還能學),時不我待,Vue3 奧利給,幹就完了!

後續仍會完善此文,您的點贊是我最大的動力!也望你們不吝賜教,不吝讚美~

最最最最後,仍是那句老話,與君共勉:

紙上得來終覺淺 絕知此事要躬行

相關文章
相關標籤/搜索