組件化
組件化,就是把頁面拆分成多個組件 (component),每個組件依賴的 CSS、JavaScript、模板、圖片等資源放在一起開發和維護。組件是資源獨立的,組件在系統內部可復用,組件和組件之間可以嵌套。
import Vue from 'vue'
import App from './App.vue'
var app = new Vue({
el: '#app',
// 這里的 h 是 createElement 方法
render: h => h(App)
})
初始化一個 vue 頁面也是 通過 render 函數去渲染的,不同的這次通過 createElement 傳的參數是一個組件而不是一個原生的標簽
組件 轉 vNode
createComponent
如果是一個普通的 html 標簽,像上一章的例子那樣是一個普通的 div,則會實例化一個普通 VNode 節點,否則通過 createComponent 方法創建一個組件 VNode
一個 App 對象,它本質上是一個 Component 類型,那么它會走到上述代碼的 else 邏輯,直接通過 createComponent 方法來創建 vnode。
針對組件渲染這個 case 主要就 3 個關鍵步驟:
構造子類構造函數,安裝組件鉤子函數和實例化 vnode。
構造子類構造函數
Vue.extend 的作用就是構造一個 Vue 的子類,它使用一種非常經典的原型繼承的方式把一個純對象轉換一個繼承于 Vue 的構造器 Sub 并返回,然后對 Sub 這個對象本身擴展了一些屬性,如擴展 options、添加全局 API 等;并且對配置中的 props 和 computed 做了初始化工作;最后對于這個 Sub 構造函數做了緩存,避免多次執行 Vue.extend 的時候對同一個子組件重復構造。(這里理解比較蛋疼)
安裝組件鉤子函數
Vue.js 使用的 Virtual DOM 參考的是開源庫 snabbdom,它的一個特點是在 VNode 的 patch 流程中對外暴露了各種時機的鉤子函數,方便我們做一些額外的事情,Vue.js 也是充分利用了這一點
實例化 VNode
最后一步非常簡單,通過 new VNode 實例化一個 vnode 并返回。需要注意的是和普通元素節點的 vnode 不同,組件的 vnode 是沒有 children 的
組件生成DOM的區別 patch
當我們通過 createComponent 創建了組件 VNode,接下來會走到 vm._update,執行 vm. _patch _ 去把 VNode 轉換成真正的 DOM 節點。在 組件中創建方式與創建普通節點不太一樣。
之前先 將組件 VUE實例化 通過 new Vue(options) 實例化子組件
因為實際上 JavaScript 是一個單線程,Vue 整個初始化是一個深度遍歷的過程,在實例化子組件的過程中,它需要知道當前上下文的 Vue 實例是什么,并把它作為子組件的父 Vue 實例。
再創建一個父節點占位符,然后再遍歷所有子 VNode 遞歸調用 createElm,在遍歷的過程中,如果遇到子 VNode 是一個組件的 VNode,則重復本節開始的過程,這樣通過一個遞歸的方式就可以完整地構建了整個組件樹。
編寫一個組件實際上是編寫一個 JavaScript 對象,對象的描述就是各種配置,之前我們提到在 _init 的最初階段執行的就是 merge options 的邏輯
合并配置
到底啥是合并配置?
個人可以簡單理解為 將Vue 的內置組件目前有 <keep-alive>、<transition> 和 <transition-group> 組件 與 自己用戶編寫的組件合并 (把一些內置組件擴展到 Vue.options.components 上)
對于鉤子函數,他們的合并策略都是 mergeHook 函數。一旦 parent 和 child 都定義了相同的鉤子函數,那么它們會把 2 個鉤子函數合并成一個數組。
通過執行 mergeField 函數,把合并后的結果保存到 options 對象中,最終返回它。
vm.$options 的值差不多是如下這樣
vm.$options = {
components: { },
created: [
function created() {
console.log('parent created')
}
],
directives: { },
filters: { },
_base: function Vue(options) {
// ...
},
el: "#app",
render: function (h) {
//...
}
}
關于合并$options
Vue 初始化階段對于 options 的合并過程就是這樣,我們需要知道對于 options 的合并有 2 種方式,子組件初始化過程通過 initInternalComponent 方式要比外部初始化 Vue 通過 mergeOptions 的過程要快,合并完的結果保留在 vm.$options 中。
縱觀一些庫、框架的設計幾乎都是類似的,自身定義了一些默認配置,同時又可以在初始化階段傳入一些定義配置,然后去 merge 默認配置,來達到定制化不同需求的目的。
生命周期
每個 Vue 實例在被創建之前都要經過一系列的初始化過程。例如需要設置數據監聽、編譯模板、掛載實例到 DOM、在數據變化時更新 DOM 等。同時在這個過程中也會運行一些叫做生命周期鉤子的函數,給予用戶機會在一些特定的場景下添加他們自己的代碼。
callHook
源碼中最終執行生命周期的函數都是調用 callHook 方法,它的定義在 src/core/instance/lifecycle 中:
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
callHook 函數的邏輯很簡單,根據傳入的字符串 hook,去拿到 vm.$options[hook] 對應的回調函數數組,然后遍歷執行,執行的時候把 vm 作為函數執行的上下文
在上一節中,我們詳細地介紹了 Vue.js 合并 options 的過程,各個階段的生命周期的函數也被合并到 vm.$options 里,并且是一個數組。因此 callhook 函數的功能就是調用某個生命周期鉤子注冊的所有回調函數。
beforeCreate & created
beforeCreate 和 created 函數都是在實例化 Vue 的階段,在 _init 方法中執行的,它的定義在 src/core/instance/init.js 中:
Vue.prototype._init = function (options?: Object) {
// ...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate') // beforeCreate
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化 props、data、methods、watch、computed 等屬性
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // created
// ...
}
beforeCreate 和 created 的鉤子調用是在 initState 的前后,initState 的作用是初始化 props、data、methods、watch、computed 等屬性
那么顯然 beforeCreate 的鉤子函數中就不能獲取到 props、data 中定義的值,也不能調用 methods 中定義的函數
在這倆個鉤子函數執行的時候,并沒有渲染 DOM,所以我們也不能夠訪問 DOM
如果是需要訪問 props、data 等數據的話,就需要使用 created 鉤子函數。
等下要說的 vue-router 和 vuex 的時候會發現它們都混合了 beforeCreatd 鉤子函數。
beforeMount & mounted
顧名思義,beforeMount 鉤子函數發生在 mount,也就是 DOM 掛載之前,它的調用時機是在 mountComponent 函數中,定義在 src/core/instance/lifecycle.js 中
在執行 vm._render() 函數渲染 VNode 之前,執行了 beforeMount 鉤子函數,在執行完 vm._update() 把 VNode patch 到真實 DOM 后,執行 mouted 鉤子。
這里對 mouted 鉤子函數執行有一個判斷邏輯,vm.$vnode 如果為 null,則表明這不是一次組件的初始化過程,而是我們通過外部 new Vue 初始化過程
beforeUpdate & updated
beforeUpdate 和 updated 的鉤子函數執行時機都應該是在數據更新的時候。 這個以后再章節中再提
beforeDestroy & destroyed
beforeDestroy 和 destroyed 鉤子函數的執行時機在組件銷毀的階段。
beforeDestroy 鉤子函數的執行時機是在 destroy 函數執行最開始的地方,接著執行了一系列的銷毀動作,包括從 parent 的 $children 中刪掉自身,刪除 watcher,當前渲染的 VNode 執行銷毀鉤子函數等,執行完畢后再調用 destroy 鉤子函數。
組件注冊
在 Vue.js 中,除了它內置的組件如 keep-alive、component、transition、transition-group 等,其它用戶自定義組件在使用前必須注冊。
全局注冊
Vue.component('my-component', {
// 選項
// ***要注冊一個全局組件,可以使用 Vue.component(tagName, options)***
})
局部注冊
import HelloWorld from './components/HelloWorld'
export default {
components: {
HelloWorld
}
}
異步組件
在我們平時的開發工作中,為了減少首屏代碼體積,往往會把一些非首屏的組件設計成異步組件,按需加載。Vue 也原生支持了異步組件的能力。
Vue.component('async-example', function (resolve, reject) {
// 這個特殊的 require 語法告訴 webpack
// 自動將編譯后的代碼分割成不同的塊,
// 這些塊將通過 Ajax 請求自動下載。
require(['./my-async-component'], resolve)
})
示例中可以看到,Vue 注冊的組件不再是一個對象,而是一個工廠函數,函數有兩個參數 resolve 和 reject,函數內部用 setTimout 模擬了異步,實際使用可能是通過動態請求異步組件的 JS 地址,最終通過執行 resolve 方法,它的參數就是我們的異步組件對象。
高級異步組件 可以擴展為3種方式 暫時先不搞~~~