源碼閱讀可能會遲到,可是必定不會缺席!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++
// 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對象。不污染全局變量,也不會被別的代碼污染
複製代碼
// 工具代碼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
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
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
}
}
複製代碼
這三個函數要重點細品!主要的點是:閉包、類型判斷,函數之間的互相調用。也便是這部分工具函數的精華!
// 定義常量和配置
//設置warn,tip等全局變量 TODO:
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 < l; i++) {
subs[i].update();
}
};
複製代碼
強烈推薦閱讀:link
Dep 至關於把 Observe 監聽到的信號作一個收集(collect dependencies),而後經過dep.notify()再通知到對應 Watcher ,從而進行視圖更新。
Vue核心:視圖更新最重要的 VNode( Virtual DOM)
把你的 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
將數組的基本操做方法拓展,實現響應式,視圖更新。
由於:對於對象的修改是能夠直接觸發響應式的,可是對數組直接賦值,是沒法觸發的,可是用到這裏通過改造的方法。咱們能夠明顯的看到 ob.dep.notify() 這一核心。
這一 part 最重要的,毋庸置疑是:Dep 和 VNode,需重點突破!!!
Vue核心:數據監聽最重要之一的 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);
}
};
複製代碼
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。
// 配置選項合併策略
ar strats = config.optionMergeStrategies;
複製代碼
這一部分代碼寫的就是父子組件配置項的合併策略,包括:默認的合併策略、鉤子函數的合併策略、filters/props、data合併策略,且包括標準的組件名、props寫法有一個統一化規範要求。
一圖以蔽之
強烈推薦閱讀:link
這一部分最重要的就是 Observer(觀察者) ,這也是 Vue 核心中的核心!其次是 mergeOptions(組件配置項的合併策略),可是一般在用的過程當中,就已經瞭解到了大部分的策略規則。
e.g.
咱們的調用 resolveAsset(context.options.components[tag],這樣咱們就能夠在 resolveAsset 的時候拿到這個組件的構造函數,並做爲 createComponent 的鉤子的參數。
校驗prop:
獲取 prop 的默認值 && 建立觀察者對象
// 在非生產環境下(除去 Weex 的某種狀況),將對prop進行驗證,包括驗證required、type和自定義驗證函數。
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`函數,驗證`prop`的值符合指定的`type`類型,分爲三類:
- 第一類:經過`typeof`判斷的類型,如`String`、`Number`、`Boolean`、`Function`、`Symbol`
- 第二類:經過`Object.prototype.toString`判斷`Object`/`Array`
- 第三類:經過`instanceof`判斷自定義的引用類型
複製代碼
// 輔助函數:檢測內置類型
// 輔助函數:處理錯誤、錯誤打印
精髓中的精髓 —— 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
複製代碼
備註:前文提過,在依賴收集原理的響應式化方法 defineReactive 中的 setter 訪問器中有派發更新 dep.notify() 方法,這個方法會挨個通知在 dep 的 subs 中收集的訂閱本身變更的 watchers 執行 update。
強烈推薦閱讀:link
0 至 2000 行主要的內容是:
//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
複製代碼
traverse 對一個對象作深層遞歸遍歷,由於遍歷過程當中就是對一個子對象的訪問,會觸發它們的 getter 過程,這樣就能夠收集到依賴,也就是訂閱它們變化的 watcher,且遍歷過程當中會把子響應式對象經過它們的 dep id 記錄到 seenObjects,避免之後重複訪問。
// 把 hook 函數合併到 def.data.hook[hookey] 中,生成新的 invoker,createFnInvoker 方法
// vnode 本來定義了 init、prepatch、insert、destroy 四個鉤子函數,而 mergeVNodeHook 函數就是把一些新的鉤子函數合併進來,例如在 transition 過程當中合併的 insert 鉤子函數,就會合併到組件 vnode 的 insert 鉤子函數中,這樣當組件插入後,就會執行咱們定義的 enterHook 了。
// 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 中歸一化分爲三個級別,
不須要進行歸一化
只須要簡單的歸一化處理,將數組打平一層
徹底歸一化,將一個 N 層的 children 徹底打平爲一維數組
利用遞歸來處理的,同時處理了一些邊界狀況。
<slot>
這一部分講的是輔助程序 —— Vue 的各種渲染方法,從字面意思中能夠知道一些方法的用途,這些方法用在Vue生成的渲染函數中。
函數式組件的實現
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去實例化子組件的緣由。(無實例)
// 建立組件的 VNode 時,若組件是函數式組件,則其 VNode 的建立過程將與普通組件有所區別。
推薦閱讀:link
installComponentHooks // installComponentHooks就是把 componentVNodeHooks的鉤子函數合併到data.hook中,,在合併過程當中,若是某個時機的鉤子已經存在data.hook中,那麼經過執行mergeHook函數作合併勾子。
mergeHook$1
transformModel
createElement// 建立元素
_createElement
applyNS
registerDeepBindings
initRender // 初識渲染
這一部分主要是圍繞 Vue 的組件的建立。Vue 將頁面劃分紅各種的組件,組件思想是 Vue 的精髓之一。
renderMixin // 引入視圖渲染混合函數
ensureCtor
createAsyncPlaceholder
resolveAsyncComponent
isAsyncPlaceholder
getFirstComponentChild
initEvents// 初始化事件
add
remove$1
createOnceHandler
updateComponentListeners
eventsMixin // 掛載事件響應相關方法
幾乎全部JS框架或插件的編寫都有一個相似的模式,即向全局輸出一個類或者說構造函數,經過建立實例來使用這個類的公開方法,或者使用類的靜態全局方法輔助實現功能。相信精通Jquery或編寫過Jquery插件的開發者會對這個模式很是熟悉。Vue.js也一模一樣,只是一開始接觸這個框架的時候對它所能實現的功能的感嘆蓋過了它也不過是一個內容較爲豐富和精緻的大型類的本質。
這裏要對 js 的繼承有一個深入的理解。 link
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
複製代碼
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"}
複製代碼
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}
複製代碼
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)
複製代碼
vue中dom的更像並非實時的,當數據改變後,vue會把渲染watcher添加到異步隊列,異步執行,同步代碼執行完成後再統一修改dom。
這一 part 在 Watcher 的原型鏈上定義了get、addDep、cleanupDeps、update、run、evaluate、depend、teardown 方法,即 Watcher 的具體實現的一些方法,好比新增依賴、清除、更新試圖等。
每一個Vue組件都有一個對應的watcher,這個watcher將會在組件render的時候收集組件所依賴的數據,並在依賴有更新的時候,觸發組件從新渲染。
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訪問到。
從 0 至 5000 行咱們能夠清晰看到 Vue 模板編譯的輪廓了。
咱們能夠總結:Vue 的核心就是 VDOM !對 DOM 對象的操做調整爲操做 VNode 對象,採用 diff 算法比較差別,一次 patch。
render 的流程是:
推薦閱讀:link
// 定義 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」的錯誤。
// 版本
Vue.version = '2.6.11';
複製代碼
這一部分是 Vue index.js 的內容,包括 Vue 的整個掛在過程
Vue.prototype._init = function (options) {}
複製代碼
Vue.prototype.$props
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function(){}
複製代碼
Vue.prototype.$on
Vue.prototype.$once
Vue.prototype.$off
Vue.prototype.$emit
複製代碼
Vue.prototype._update
Vue.prototype.$forceUpdate
Vue.prototype.$destroy
複製代碼
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繼承了全局的, 這就是爲何全局註冊的能夠在任何地方使用,由於每一個實例都繼承了全局的, 因此都能找到。
推薦閱讀:
new 一個 Vue 對象發生了什麼:
// these are reserved for web because they are directly compiled away
// during template compilation
// 這些是爲web保留的,由於它們是直接編譯掉的
// 在模板編譯期間
複製代碼
這一 part 沒有特別要說的,主要是對 class 的轉碼、合併和其餘二次封裝的工具函數。實際上咱們在 Vue 源碼不少地方看到了這樣的封裝,在日常的開發中,咱們也得要求本身封裝基本的函數。若是能造成本身習慣用的函數的庫,會方便不少,且對本身能力也是一個提高。
// 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」 了
在絕大多數狀況下,咱們最好不要觸達另外一個組件實例內部或手動操做 DOM 元素。不過也確實在一些狀況下作這些事情是合適的。ref 爲咱們提供瞭解決途徑。
ref屬性不是一個標準的HTML屬性,只是Vue中的一個屬性。
Virtual DOM !
沒錯,這裏就是 虛擬 dom 生成的源碼相關。
createElement方法接收一個tag參數,在內部會去判斷tag標籤的類型,從而去決定是建立一個普通的VNode仍是一個組件類VNode;
createComponent 的實現,在渲染一個組件的時候的 3 個關鍵邏輯:
咱們傳入的 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);
// 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)
}
}
複製代碼
reactivateComponent 承接上文 createComponent
Vue 的核心思想:組件化。
這一部分是關於構建組件樹,造成虛擬 dom ,以及很是重要的 patch 方法。
再來億遍:
緣由:當修改某條數據的時候,這時候js會將整個DOM Tree進行替換,這種操做是至關消耗性能的。因此在Vue中引入了Vnode的概念:Vnode是對真實DOM節點的模擬,能夠對Vnode Tree進行增長節點、刪除節點和修改節點操做。這些過程都只須要操做VNode Tree,不須要操做真實的DOM,大大的提高了性能。修改以後使用diff算法計算出修改的最小單位,在將這些小單位的視圖進行更新。
原理: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 是這樣子的。
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 添加屬性、事件等具體實現函數。
/*
* Parse a v-model expression into a base path and a final key segment.
* Handles both dot-path and possible square brackets.
* 將 v-model 表達式解析爲基路徑和最後一個鍵段。
* 處理點路徑和可能的方括號。
*/
複製代碼
// 若是數據是object.info.name的狀況下 則返回是 {exp: "object.info",key: "name"} // 若是數據是object[info][name]的狀況下 則返回是 {exp: "object[info]",key: "name"}
這一部分包括:原生指令 v-bind 和爲虛擬 dom 添加 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 來進行。
/**
* parent component style should be after child's * so that parent component's style could override it
* 父組件樣式應該在子組件樣式以後
* 這樣父組件的樣式就能夠覆蓋它
* 循環子組件和組件的樣式,把它所有合併到一個樣式對象中返回 樣式對象 如{width:100px,height:200px} 返回該字符串。
*/
複製代碼
這一部分關於:對真實 dom 的操做,包括樣式的增刪、事件的增刪、動畫類等。
回過頭再理一下宏觀上的東西,再來億遍-虛擬DOM:模板 → 渲染函數 → 虛擬DOM樹 → 真實DOM
那麼這一部分則處在「虛擬DOM樹 → 真實DOM」這個階段
// 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、指令的更新。
// 定義在組件根內部遞歸搜索可能存在的 transition
// 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>
// 咱們要遞歸地檢索要渲染的實際組件
複製代碼
前部分以及此部分大部分圍繞 Transition 這個關鍵對象。即迎合官網 「過渡 & 動畫」 這一節,是咱們須要關注的重點!
Vue 在插入、更新或者移除 DOM 時,提供多種不一樣方式的應用過渡效果。包括如下工具:
- 在 CSS 過渡和動畫中自動應用 class
- 能夠配合使用第三方 CSS 動畫庫,如 Animate.css
- 在過渡鉤子函數中使用 JavaScript 直接操做 DOM
- 能夠配合使用第三方 JavaScript 動畫庫,如 Velocity.js
在這裏,咱們只會講到進入、離開和列表的過渡,你也能夠看下一節的管理過渡狀態。
vue - transition 裏面大有東西,這裏有一篇「細談」推薦閱讀。
// we divide the work into three loops to avoid mixing DOM reads and writes
// in each iteration - which helps prevent layout thrashing.
//咱們將工做分爲三個 loops,以免將 DOM 讀取和寫入混合在一塊兒
//在每次迭代中-有助於防止佈局衝撞。
複製代碼
// 安裝平臺運行時指令和組件
extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);
複製代碼
Q: vue自帶的內置組件有什麼?
A: Vue中內置的組件有如下幾種:
component組件:有兩個屬性---is inline-template
渲染一個‘元組件’爲動態組件,按照'is'特性的值來渲染成那個組件
transition組件:爲組件的載入和切換提供動畫效果,具備很是強的可定製性,支持16個屬性和12個事件
transition-group:做爲多個元素/組件的過渡效果
keep-alive:包裹動態組件時,會緩存不活動的組件實例,而不是銷燬它們
slot:做爲組件模板之中的內容分發插槽,slot元素自身將被替換
// install platform specific utils // 安裝平臺特定的工具
Vue.config.mustUseProp = mustUseProp;
Vue.config.isReservedTag = isReservedTag;
Vue.config.isReservedAttr = isReservedAttr;
Vue.config.getTagNamespace = getTagNamespace;
Vue.config.isUnknownElement = isUnknownElement;
複製代碼
// 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
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);
}
}
}
}
複製代碼
Regular Expressions // 相關正則
parse 函數從 9593 行至 9914 行,共三百多行。核心嗎?固然核心!
引自 wikipedia:
在計算機科學和語言學中,語法分析(英語:syntactic analysis,也叫 parsing)是根據某種給定的形式文法對由單詞序列(如英語單詞序列)構成的輸入文本進行分析並肯定其語法結構的一種過程。
語法分析器(parser)一般是做爲編譯器或解釋器的組件出現的,它的做用是進行語法檢查、並構建由輸入的單詞組成的數據結構(通常是語法分析樹、抽象語法樹等層次化的數據結構)。語法分析器一般使用一個獨立的詞法分析器從輸入字符流中分離出一個個的「單詞」,並將單詞流做爲其輸入。實際開發中,語法分析器能夠手工編寫,也可使用工具(半)自動生成。
parse 的總體流程實際上就是先處理了一些傳入的options,而後執行了parseHTML 函數,傳入了template,options和相關鉤子。
具體實現這裏盜一個圖:
parse中的語法分析能夠看這一篇這一節
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 原理面試都不用慌了。框架就在這裏,細化的東西就須要多多記憶了!
🙌 到 1w 行了,自我慶祝一下!
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,在這兩個函數處理以前,咱們的key屬性和ref屬性都是保存在astElement上面的attrs和attrsMap,通過這兩個函數以後,attrs裏面的key和ref會被幹掉,變成astElement的直屬屬性。
探討一下slot的處理方式,咱們知道的是,slot的具體位置是在組件中定義的,而須要替換的內容又是組件外面嵌套的代碼,Vue對這兩塊的處理是分開的。
先說組件內的屬性摘取,主要是slot標籤的name屬性,這是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。
複製代碼
這一部分還是銜接這 parse function 裏的具體實現:start、end、comment、chars四大函數。
流程再回顧一下:
1、普通標籤處理流程描述
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、文本或表達式的處理流程描述。
3、註釋流程描述
parseHTML() 和 parse() 這兩個函數佔了很大的篇幅,值得重點去看看。的確也不少細節,一些正則的匹配,字符串的操做等。從宏觀上把握從 template 到 vnode 的 parse 流程也無大問題。
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,if ,else,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’
};
複製代碼
/**
* 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 的過程實際上就幹 2 件事情,markStatic(root) 標記靜態節點 ,markStaticRoots(root, false) 標記靜態根節點。
那麼被判斷爲靜態根節點的條件是什麼?
該節點的全部子孫節點都是靜態節點(判斷爲靜態節點要知足 7 個判斷,詳見)
必須存在子節點
子節點不能只有一個純文本節點
其實,markStaticRoots()方法針對的都是普通標籤節點。表達式節點與純文本節點都不在考慮範圍內。
markStatic()得出的static屬性,在該方法中用上了。將每一個節點都判斷了一遍static屬性以後,就能夠更快地肯定靜態根節點:經過判斷對應節點是不是靜態節點 且 內部有子元素 且 單一子節點的元素類型不是文本類型。
只有純文本子節點時,他是靜態節點,但不是靜態根節點。靜態根節點是 optimize 優化的條件,沒有靜態根節點,說明這部分不會被優化。
Q:爲何子節點的元素類型是靜態文本類型,就會給 optimize 過程加大成本呢?
A:optimize 過程當中作這個靜態根節點的優化目是:在 patch 過程當中,減小沒必要要的比對過程,加速更新。可是須要如下成本
維護靜態模板的存儲對象 一開始的時候,全部的靜態根節點 都會被解析生成 VNode,而且被存在一個緩存對象中,就在 Vue.proto._staticTree 中。 隨着靜態根節點的增長,這個存儲對象也會愈來愈大,那麼佔用的內存就會愈來愈多 勢必要減小一些沒必要要的存儲,全部只有純文本的靜態根節點就被排除了
多層render函數調用 這個過程涉及到實際操做更新的過程。在實際render 的過程當中,針對靜態節點的操做也須要調用對應的靜態節點渲染函數,作必定的判斷邏輯。這裏須要必定的消耗。
純文本直接對比便可,不進行 optimize 將會更高效。
// KeyboardEvent.keyCode aliases
if(${condition})return null;
無論是組件仍是普通標籤,事件處理代碼都在genData的過程當中,和以前分析原生事件一致,genHandlers用來處理事件對象並拼接成字符串。
// 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
}
}
複製代碼
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
}
}
複製代碼
這裏面的邏輯、細節太多了,不作贅述,有興趣瞭解的童鞋能夠去看推薦閱讀
generate方法內部邏輯仍是很複雜的,但僅作了一件事情,就是將ast轉化爲render函數的字符串,造成一個嵌套結構的方法,模版編譯生成的_c(),_m(),_l等等其實都是生成vnode的方法,在執行vue.$mount方法的時候,會調用vm._update(vm._render(), hydrating)方法,此時_render()中方法會執行生成的render()函數,執行後會生成vnode,也就是虛擬dom節點。
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
}
複製代碼
再看這張圖,對於「模板編譯」是否是有一種新的感受了。
// 最後的最後
return Vue;
複製代碼
哇!歷時一個月左右,我終於完成啦!!!
完結撒花🎉🎉🎉!激動 + 釋然 + 感恩 + 小知足 + ...... ✿✿ヽ(°▽°)ノ✿
這生啃給我牙齒都啃酸了!!
emmm,原本打算再多修補一下,可是看到 vue3 的源碼解析已有版本出來啦(扶朕起來,朕還能學),時不我待,Vue3 奧利給,幹就完了!
後續仍會完善此文,您的點贊是我最大的動力!也望你們不吝賜教,不吝讚美~
最最最最後,仍是那句老話,與君共勉:
紙上得來終覺淺 絕知此事要躬行