目錄結構
├── benchmarks 效能、基準測試
├── dist 構建打包的輸出目錄
├── examples 案例目錄
├── flow flow 語法的型別宣告
├── packages 一些額外的包,比如:負責服務端渲染的包 vue-server-renderer、配合 vue-loader 使用的 vue-template-compiler,還有 weex 相關的
│ ├── vue-server-renderer
│ ├── vue-template-compiler
│ ├── weex-template-compiler
│ └── weex-vue-framework
├── scripts 所有的配置檔案的存放位置,比如 rollup 的配置檔案
├── src vue 原始碼目錄
│ ├── compiler 編譯器
│ ├── core 執行時的核心包
│ │ ├── components 全域性元件,比如 keep-alive
│ │ ├── config.js 一些預設配置項
│ │ ├── global-api 全域性 API,比如熟悉的:Vue.use()、Vue.component() 等
│ │ ├── instance Vue 例項相關的,比如 Vue 建構函式就在這個目錄下
│ │ ├── observer 響應式原理
│ │ ├── util 工具方法
│ │ └── vdom 虛擬 DOM 相關,比如熟悉的 patch 演算法就在這兒
│ ├── platforms 平臺相關的編譯器程式碼
│ │ ├── web
│ │ └── weex
│ ├── server 服務端渲染相關
├── test 測試目錄
├── types TS 型別宣告
Vue 的初始化過程(new Vue(options))都做了什麼?
1.處理元件配置項
初始化根元件時進行了選項合併操作,將全域性配置合併到根元件的區域性配置上
初始化每個子元件時做了一些效能優化,將元件配置物件上的一些深層次屬性放到 vm.$options 選項中,以提高程式碼的執行效率
2.初始化元件例項的關係屬性,比如 $parent、$children、$root、$refs 等
3.處理自定義事件
4.呼叫 beforeCreate 鉤子函式
5.初始化元件的 inject 配置項,得到 ret[key] = val 形式的配置物件,然後對該配置物件進行淺層的響應式處理(只處理了物件第一層資料),並代理每個 key 到 vm 例項上
6.資料響應式,處理 props、methods、data、computed、watch 等選項
7.解析元件配置項上的 provide 物件,將其掛載到 vm._provided 屬性上
8.呼叫 created 鉤子函式
9.如果發現配置項上有 el 選項,則自動呼叫 $mount 方法,也就是說有了 el 選項,就不需要再手動呼叫 $mount 方法,反之,沒提供 el 選項則必須呼叫 $mount
10.接下來則進入掛載階段
總結:Vue 初始化主要就幹了幾件事情,合併配置,初始化生命週期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。
Vue 例項掛載的實現
Vue 中我們是通過 $mount 例項方法去掛載 vm 的
$mount 方法實際上會去呼叫 mountComponent 方法
mountComponent 核心就是先例項化一個渲染 Watcher,在它的回撥函式中會呼叫 updateComponent 方法,在此方法中呼叫 vm._render 方法先生成虛擬 Node,最終呼叫 vm._update 更新 DOM
Watcher 起到兩個作用,一個是初始化的時候會執行回撥函式,另一個是當 vm 例項中的監測的資料發生變化的時候執行回撥函式
最後判斷為根節點的時候設定 vm._isMounted 為 true, 表示這個例項已經掛載了,同時執行 mounted 鉤子函式
render
Vue 的 _render 方法是例項的一個私有方法,它用來把例項渲染成一個虛擬 Node
寫的比較多的是 template 模板,在 mounted 方法的實現中,會把 template 編譯成 render 方法
Virtual DOM
真正的 DOM 元素是非常龐大的,因為瀏覽器的標準就把 DOM 設計的非常複雜。當我們頻繁的去做 DOM 更新,會產生一定的效能問題
而 Virtual DOM 就是用一個原生的 JS 物件去描述一個 DOM 節點,所以它比建立一個 DOM 的代價要小很多
在 Vue.js 中,Virtual DOM 是用 VNode 這麼一個 Class 去描述
VNode 是對真實 DOM 的一種抽象描述,它的核心定義無非就幾個關鍵屬性,標籤名、資料、子節點、鍵值等,其它屬性都是用來擴充套件 VNode 的靈活性以及實現一些特殊 feature 的。
由於 VNode 只是用來對映到真實 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常輕量和簡單的
Virtual DOM 除了它的資料結構的定義,對映到真實的 DOM 實際上要經歷 VNode 的 create、diff、patch 等過程
Vue.js 利用 createElement 方法建立 VNode
createElement 建立 VNode 的過程,每個 VNode 有 children,children 每個元素也是一個 VNode,這樣就形成了一個 VNode Tree,它很好的描述了我們的 DOM Tree。
update
Vue 的 _update 是例項的一個私有方法,它被呼叫的時機有 2 個,一個是首次渲染,一個是資料更新的時候
從初始化 Vue 到最終渲染的整個過程(配圖)
生命週期
beforeCreate & created
beforeCreate 和 created 函式都是在例項化 Vue 的階段,在 _init 方法中執行的
在這兩個鉤子函式執行的時候,並沒有渲染 DOM,所以我們也不能夠訪問 DOM,一般來說,如果元件在載入的時候需要和後端有互動,放在這倆個鉤子函式執行都可以,如果是需要訪問 props、data 等資料的話,就需要使用 created 鉤子函式
beforeMount & mounted
beforeMount 鉤子函式發生在 mount,也就是 DOM 掛載之前,它的呼叫時機是在 mountComponent 函式中
在執行 vm._render() 函式渲染 VNode 之前,執行了 beforeMount 鉤子函式,在執行完 vm._update() 把 VNode patch 到真實 DOM 後,執行 mounted 鉤子
beforeUpdate & updated
beforeUpdate 和 updated 的鉤子函式執行時機都應該是在資料更新的時候
beforeUpdate 的執行時機是在渲染 Watcher 的 before 函式中
注意,在元件已經 mounted 之後,才會去呼叫 beforeUpdate 這個鉤子函式
beforeDestroy & destroyed
beforeDestroy 和 destroyed 鉤子函式的執行時機在元件銷燬的階段,最終會呼叫 $destroy 方法
在 $destroy 的執行過程中,它又會執行 vm.__patch__(vm._vnode, null) 觸發它子元件的銷燬鉤子函式,這樣一層層的遞迴呼叫,所以 destroy 鉤子函式執行順序是先子後父,和 mounted 過程一樣
activated & deactivated
activated 和 deactivated 鉤子函式是專門為 keep-alive 元件定製的鉤子
keep-alive 參考:https://www.jianshu.com/p/952...
響應式
初始化的過程,把原始的資料最終對映到 DOM 中,資料的變更會觸發 DOM 的變化
1.資料渲染到頁面
2.處理使用者互動
原生js做法
監聽點選事件,修改資料,手動操作 DOM 重新渲染
vue做法
利用 Object.defineProperty 原理,具體詳見:https://developer.mozilla.org...
Object.defineProperty(obj, prop, descriptor)
對於 descriptor 中的 get 和 set,get 是一個給屬性提供的 getter 方法,當我們訪問了該屬性的時候會觸發 getter 方法;set 是一個給屬性提供的 setter 方法,當我們對該屬性做修改的時候會觸發 setter 方法
一旦物件擁有了 getter 和 setter,我們可以簡單地把這個物件稱為響應式物件
proxy
代理的作用是把 props 和 data 上的屬性代理到 vm 例項上。定義了 props,可以通過 vm 例項訪問到它
let comP = {
props: {
msg: 'hello'
},
methods: {
say() {
console.log(this.msg)
}
}
}
在 say 函式中通過 this.msg 訪問到定義在 props 中的 msg,這個過程發生在 proxy 階段, 實現原理如下:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
observe
observe 方法的作用就是給非 VNode 的物件型別資料新增一個 Observer,如果已經新增過則直接返回,否則在滿足一定條件下去例項化一個 Observer 物件例項
Observer
Observer 是一個類,它的作用是給物件的屬性新增 getter 和 setter,用於依賴收集和派發更新
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
// 對 value 做判斷,對於陣列會呼叫 observeArray 方法,否則對純物件呼叫 walk 方法
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// defineReactive 的功能就是定義一個響應式物件,給物件動態新增 getter 和 setter
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
// observe 的功能就是用來監測資料的變化, 詳細程式碼看原始碼
observe(items[i])
}
}
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
響應式原理總結:核心就是利用 Object.defineProperty 給資料新增了 getter 和 setter,目的就是為了在我們訪問資料以及寫資料的時候能自動執行一些邏輯:getter 做的事情是依賴收集,setter 做的事情是派發更新