在通過初始化階段以後,即將開始組件的掛載,不過在掛載以前頗有必要提一下虛擬Dom
的概念。這個想必你們有所耳聞,咱們知道vue@2.0
開始引入了虛擬Dom
,主要解決的問題是,大部分狀況下能夠下降使用JavaScript
去操做跨線程的龐大Dom
所須要的昂貴性能,讓Dom
操做的性能更高;以及虛擬Dom
能夠用於SSR
以及跨端使用。虛擬Dom
,顧名思義並非真實的Dom
,而是使用JavaScript
的對象來對真實Dom
的一個描述。一個真實的Dom
也無非是有標籤名,屬性,子節點等這些來描述它,如頁面中的真實Dom
是這樣的:vue
<div id='app' class='wrap'>
<h2>
hello
</h2>
</div>
複製代碼
咱們能夠在render
函數內這樣描述它:node
new Vue({
render(h) {
return h('div', {
attrs: {
id: 'app',
class: 'wrap'
}
}, [
h('h2', 'hello')
])
}
})
複製代碼
這個時候它並非用對象來描述的,使用的是render
函數內的數據結構去描述的真實Dom
,而如今咱們須要將這段描述轉爲用對象的形式,render
函數使用的是參數h
方法並用VNode
這個類來實例化它們,因此咱們再瞭解h
的實現原理前,首先來看下VNode
類是什麼,找到它定義的地方:面試
export default class VNode {
constructor (
tag
data
children
text
elm
context
componentOptions
asyncFactory
) {
this.tag = tag // 標籤名
this.data = data // 屬性 如id/class
this.children = children // 子節點
this.text = text // 文本內容
this.elm = elm // 該VNode對應的真實節點
this.ns = undefined // 節點的namespace
this.context = context // 該VNode對應實例
this.fnContext = undefined // 函數組件的上下文
this.fnOptions = undefined // 函數組件的配置
this.fnScopeId = undefined // 函數組件的ScopeId
this.key = data && data.key // 節點綁定的key 如v-for
this.componentOptions = componentOptions // 組件VNode的options
this.componentInstance = undefined // 組件的實例
this.parent = undefined // vnode組件的佔位符節點
this.raw = false // 是否爲平臺標籤或文本
this.isStatic = false // 靜態節點
this.isRootInsert = true // 是否做爲根節點插入
this.isComment = false // 是不是註釋節點
this.isCloned = false // 是不是克隆節點
this.isOnce = false // 是不是v-noce節點
this.asyncFactory = asyncFactory // 異步工廠方法
this.asyncMeta = undefined // 異步meta
this.isAsyncPlaceholder = false // 是否爲異步佔位符
}
get child () { // 別名
return this.componentInstance
}
}
複製代碼
這是VNode
類定義的地方,挺嚇人的,它支持一共最多八個參數,其實常常用到的並很少。如tag
是元素節點的名稱,children
爲它的子節點,text
是文本節點內的文本。實例化後的對象就有二十三個屬性做爲在vue
的內部一個節點的描述,它描述的是將它建立爲一個怎樣的真實Dom
。大部分屬性默認是false
或undefined
,而經過這些屬性有效的值就能夠組裝出不一樣的描述,如真實的Dom
中會有元素節點、文本節點、註釋節點等。而經過這樣一個VNode
類,也能夠描述出相應的節點,部分節點vue
內部還作了相應的封裝:數組
註釋節點 ↓瀏覽器
export const createEmptyVNode = (text = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
複製代碼
VNode
,有效屬性只有text
和isComment
來表示一個註釋節點。真實的註釋節點:
<!-- 註釋節點 -->
VNode描述:
createEmptyVNode ('註釋節點')
{
text: '註釋節點',
isComment: true
}
複製代碼
文本節點 ↓緩存
export function createTextVNode (val) {
return new VNode(undefined, undefined, undefined, String(val))
}
複製代碼
text
屬性,描述的是標籤內的文本VNode描述:
createTextVNode('文本節點')
{
text: '文本節點'
}
複製代碼
克隆節點 ↓bash
export function cloneVNode (vnode) {
const cloned = new VNode(
vnode.tag,
vnode.data,
vnode.children,
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
)
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key
cloned.isComment = vnode.isComment
cloned.fnContext = vnode.fnContext
cloned.fnOptions = vnode.fnOptions
cloned.fnScopeId = vnode.fnScopeId
cloned.asyncMeta = vnode.asyncMeta
cloned.isCloned = true
return cloned
}
複製代碼
VNode
節點拷貝一份,只是被拷貝節點的isCloned
屬性爲false
,而拷貝獲得的節點的isCloned
屬性爲true
,除此以外它們徹底相同。元素節點 ↓數據結構
真實的元素節點:
<div>
hello
<span>Vue!</span>
</div>
VNode描述:
{
tag: 'div',
children: [
{
text: 'hello'
},
{
tag: 'span',
children: [
{
text: Vue!
}
]
}
],
}
複製代碼
組件節點 ↓app
渲染App組件:
new Vue({
render(h) {
return h(App)
}
})
VNode描述:
{
tag: 'vue-component-2',
componentInstance: {...},
componentOptions: {...},
context: {...},
data: {...}
}
複製代碼
VNode
會和元素節點相比會有兩個特有的屬性componentInstance
和componentOptions
。VNode
的類型有不少,它們都是從這個VNode
類中實例化出來的,只是屬性不一樣。開始掛載階段
this._init() 方法的最後:
... 初始化
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
複製代碼
若是用戶有傳入el
屬性,就執行vm.$mount
方法並傳入el
開始掛載。這裏的$mount
方法在完整版和運行時版本又會有點不一樣,他們區別以下:
運行時版本:
Vue.prototype.$mount = function(el) { // 最初的定義
return mountComponent(this, query(el));
}
完整版:
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function(el) { // 拓展編譯後的
if(!this.$options.render) { ---|
if(this.$options.template) { ---|
...通過編譯器轉換後獲得render函數 ---| 編譯階段
} ---|
} ---|
return mount.call(this, query(el))
}
-----------------------------------------------
export function query(el) { // 獲取掛載的節點
if(typeof el === 'string') { // 好比#app
const selected = document.querySelector(el)
if(!selected) {
return document.createElement('div')
}
return selected
} else {
return el
}
}
複製代碼
完整版有一個騷操做,首先將$mount
方法緩存到mount
變量上,而後使用函數劫持的手段從新定義$mount
函數,並在其內部增長編譯相關的代碼,最後仍是使用原來定義的$mount
方法掛載。因此核心是要了解最初定義$mount
方法時內的mountComponent
方法:
export function mountComponent(vm, el) {
vm.$el = el
...
callHook(vm, 'beforeMount')
...
const updateComponent = function () {
vm._update(vm._render())
}
...
}
複製代碼
首先將傳入的el
賦值給vm.$el
,這個時候el
是一個真實dom
,接着會執行用戶本身定義的beforeMount
鉤子。接下來會定義一個重要的函數變量updateComponent
,它的內部首先會執行vm._render()
方法,將返回的結果傳入vm._update()
內再執行。咱們這章主要就來分析這個vm._render()
方法作了什麼事情,來看下它的定義:
Vue.prototype._render = function() {
const vm = this
const { render } = vm.$options
const vnode = render.call(vm, vm.$createElement)
return vnode
}
複製代碼
首先會獲得自定義的render
函數,傳入vm.$createElement
這個方法(也就是上面例子內的h
方法),將執行的返回結果賦值給vnode
,這裏也就完成了render
函數內數據結構轉爲vnode
的操做。而這個vm.$createElement
是在以前初始化initRender
方法內掛載到vm
實例下的:
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) // 編譯
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) // 手寫
複製代碼
不管是編譯而來仍是手寫的render
函數,它們都是返回了createElement
這個函數,繼續查找它的定義:
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
export default createElement(
context,
tag,
data,
children,
normalizationType,
alwaysNormalize) {
if(Array.isArray(data) || isPrimitive(data)) { // data是數組或基礎類型
normalizationType = children --|
children = data --| 參數移位
data = undefined --|
}
if (isTrue(alwaysNormalize)) { // 若是是手寫render
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(contenxt, tag, data, children, normalizationType)
}
複製代碼
這裏是對傳入的參數處理,若是第三個參數傳入的是數組(子元素)或者是基礎類型的值,就將參數位置改變。而後對傳入的最後一個參數是true
仍是false
作處理,這會決定以後對children
屬性的處理方式。這裏又是對_createElement
作的封裝,因此咱們還要繼續看它的定義:
export function _createElement(
context, tag, data, children, normalizationType
) {
if (normalizationType === ALWAYS_NORMALIZE) { // 手寫render函數
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) { //編譯render函數
children = simpleNormalizeChildren(children)
}
if(typeof tag === 'string') { // 標籤
let vnode, Ctor
if(config.isReservedTag(tag)) { // 若是是html標籤
vnode = new VNode(tag, data, children, undefined, undefined, context)
}
...
} else { // 就是組件了
vnode = createComponent(tag, data, context, children)
}
...
return vnode
}
複製代碼
首先咱們會看到針對最後一個參數的布爾值對children
作不一樣的處理,若是是編譯的render
函數,就將children
格式化爲一維數組:
function simpleNormalizeChildren(children) { // 編譯render的處理函數
for (let i = 0; i < children.length; i++) {
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children)
}
}
return children
}
複製代碼
咱們如今主要看下手寫的render
函數是怎麼處理的,從接下來的_createElement
方法咱們知道,轉化VNode
是分爲兩種狀況的:
1. 普通的元素節點轉化爲
VNode
以一段children
是二維數組代碼爲示例,咱們來講明普通元素是如何轉VNode
的:
render(h) {
return h(
"div",
[
[
[h("h1", "title h1")],
[h('h2', "title h2")]
],
[
h('h3', 'title h3')
]
]
);
}
複製代碼
由於_createElement
方法是對h
方法的封裝,因此h
方法的第一個參數對應的就是_createElement
方法內的tag
,第二個參數對應的是data
。又由於h
方法是遞歸的,因此首先從h('h1', 'title h1')
開始解析,通過參數上移以後children
就是title h1
這段文本了,因此會在normalizeChildren
方法將它轉爲[createTextVNode(children)]
一個文本的VNode
節點:
function normalizeChildren(children) { // 手寫`render`的處理函數
return isPrimitive(children) //原始類型 typeof爲string/number/symbol/boolean之一
? [createTextVNode(children)] // 轉爲數組的文本節點
: Array.isArray(children) // 若是是數組
? normalizeArrayChildren(children)
: undefined
}
複製代碼
接着會知足_createElement
方法內的這個條件:
if(typeof tag === 'string'){ tag爲h1標籤
if(config.isReservedTag(tag)) { // 是html標籤
vnode = new VNode(
tag, // h1
data, // undefined
children, 轉爲了 [{text: 'title h1'}]
undefined,
undefined,
context
)
}
}
...
return vnode
返回的vnode結構爲:
{
tag: h1,
children: [
{ text: title h1 }
]
}
複製代碼
而後依次處理h('h2', "title h2")
,h('h3', 'title h3')
會獲得三個VNode
實例的節點。接着會執行最外層的h(div, [[VNode,VNode],[VNode]])
方法,注意它的結構是二維數組,這個時候它就知足normalizeChildren
方法內的Array.isArray(children)
這個條件了,會執行normalizeArrayChildren
這個方法:
function normalizeArrayChildren(children) {
const res = [] // 存放結果
for(let i = 0; i < children.length; i++) { // 遍歷每一項
let c = children[i]
if(isUndef(c) || typeof c === 'boolean') { // 若是是undefined 或 布爾值
continue // 跳過
}
if(Array.isArray(c)) { // 若是某一項是數組
if(c.length > 0) {
c = normalizeArrayChildren(c) // 遞歸結果賦值給c,結果就是[VNode]
... 合併相鄰的文本節點
res.push.apply(res, c) //小操做
}
} else {
...
res.push(c)
}
}
return res
}
複製代碼
若是children
內的某一項是數組就遞歸調用本身,將自身傳入並將返回的結果覆蓋自身,遞歸內的結果就是res.push(c)
獲得的,這裏c
也是[VNode]
數組結構。覆蓋本身以後執行res.push.apply(res, c)
,添加到res
內。這裏vue
秀了一個小操做,在一個數組內push
一個數組,原本應該是二維數組的,使用這個寫法後res.push.apply(res, c)
後,結果最後是就是一維數組了。res
最後返回的結果[VNode, VNode, VNode]
,這也是children
最終的樣子。接着執行h('div', [VNode, VNode, VNode])
方法,又知足了以前一樣的條件:
if (config.isReservedTag(tag)) { // 標籤爲div
vnode = new VNode(
tag, data, children, undefined, undefined, context
)
}
return vnode
複製代碼
因此最終獲得的vnode
結構就是這樣的:
{
tag: 'div',
children: [VNode, VNode, VNode]
}
複製代碼
以上就是普通元素節點轉VNode
的具體過程。
2. 組件轉化爲
VNode
接下來咱們來了解組件VNode
的建立過程,常見示例以下:
main.js
new Vue({
render(h) {
return h(App)
}
})
app.vue
import Child from '@/pages/child'
export default {
name: 'app',
components: {
Child
}
}
複製代碼
不知道你們有將引入的組件直接打印出來過沒有,咱們在main.js
內打印下App
組件:
{
beforeCreate: [ƒ]
beforeDestroy: [ƒ]
components: {Child: {…}}
name: "app"
render: ƒ ()
staticRenderFns: []
__file: "src/App.vue"
_compiled: true
}
複製代碼
咱們只是定義了name
和components
屬性,打印出來爲何會多了這麼多屬性?這是vue-loader
解析後添加的,例如render: ƒ ()
就是將App
組件的template
模板轉換而來的,咱們記住這個一個組件對象便可。
讓咱們簡單看一眼以前_createElement
函數:
export function _createElement(
context, tag, data, children, normalizationType
) {
...
if(typeof tag === 'string') { // 標籤
...
} else { // 就是組件了
vnode = createComponent(
tag, // 組件對象
data, // undefined
context, // 當前vm實例
children // undefined
)
}
...
return vnode
}
複製代碼
很明顯這裏的tag
並不一個string
,轉而會調用createComponent()
方法:
export function createComponent ( // 上
Ctor, data = {}, context, children, tag
) {
const baseCtor = context.$options._base
if (isObject(Ctor)) { // 組件對象
Ctor = baseCtor.extend(Ctor) // 轉爲Vue的子類
}
...
}
複製代碼
這裏要補充一點,在new Vue()
以前定義全局API
時:
export function initGlobalAPI(Vue) {
...
Vue.options._base = Vue
Vue.extend = function(extendOptions){...}
}
複製代碼
通過初始化合並options
以後當前實例就有了context.$options._base
這個屬性,而後執行它的extend
這個方法,傳入咱們的組件對象,看下extend
方法的定義:
Vue.cid = 0
let cid = 1
Vue.extend = function (extendOptions = {}) {
const Super = this // Vue基類構造函數
const name = extendOptions.name || Super.options.name
const Sub = function (options) { // 定義構造函數
this._init(options) // _init繼承而來
}
Sub.prototype = Object.create(Super.prototype) // 繼承基類Vue初始化定義的原型方法
Sub.prototype.constructor = Sub // 構造函數指向子類
Sub.cid = cid++
Sub.options = mergeOptions( // 子類合併options
Super.options, // components, directives, filters, _base
extendOptions // 傳入的組件對象
)
Sub['super'] = Super // Vue基類
// 將基類的靜態方法賦值給子類
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
ASSET_TYPES.forEach(function (type) { // ['component', 'directive', 'filter']
Sub[type] = Super[type]
})
if (name) { 讓組件能夠遞歸調用本身,因此必定要定義name屬性
Sub.options.components[name] = Sub // 將子類掛載到本身的components屬性下
}
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
return Sub
}
複製代碼
仔細觀察extend
這個方法不難發現,咱們傳入的組件對象至關於就是以前new Vue(options)
裏面的options
,也就是用戶自定義的配置,而後和vue
以前就定義的原型方法以及全局API
合併,而後返回一個新的構造函數,它擁有Vue
完整的功能。讓咱們繼續createComponent
的其餘邏輯:
export function createComponent ( // 中
Ctor, data = {}, context, children, tag
) {
...
const listeners = data.on // 父組件v-on傳遞的事件對象格式
data.on = data.nativeOn // 組件的原生事件
installComponentHooks(data) // 爲組件添加鉤子方法
...
}
複製代碼
以前說明初始化事件initEvents
時,這裏的data.on
就是父組件傳遞給子組件的事件對象,賦值給變量listeners
;data.nativeOn
是綁定在組件上有native
修飾符的事件。接着會執行一個組件比較重要的方法installComponentHooks
,它的做用是往組件的data
屬性下掛載hook
這個對象,裏面有init
,prepatch
,insert
,destroy
四個方法,這四個方法會在以後的將VNode
轉爲真實Dom
的patch
階段會用到,當咱們使用到時再來看它們的定義是什麼。咱們繼續createComponent
的其餘邏輯:
export function createComponent ( // 下
Ctor, data = {}, context, children, tag
) {
...
const name = Ctor.options.name || tag // 拼接組件tag用
const vnode = new VNode( // 建立組件VNode
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, // 對應tag屬性
data, // 有父組件傳遞自定義事件和掛載的hook對象
undefined, // 對應children屬性
undefined, // 對應text屬性
undefined, // 對應elm屬性
context, // 當前實例
{ // 對應componentOptions屬性
Ctor, // 子類構造函數
propsData, // props具體值的對象集合
listeners, // 父組件傳遞自定義事件對象集合
tag, // 使用組件時的名稱
children // 插槽內的內容,也是VNode格式
},
asyncFactory
)
return vnode
}
複製代碼
組件生成的VNode
以下:
{
tag: 'vue-component-1-app',
context: {...},
componentOptions: {
Ctor: function(){...},
propsData: undefined,
children: undefined,
tag: undefined,
children: undefined
},
data: {
on: undefined, // 爲原生事件
data: {
init: function(){...},
insert: function(){...},
prepatch: function(){...},
destroy: function(){...}
}
}
}
複製代碼
若是看到tag
屬性是vue-component
開頭就是組件了,以上就組件VNode
的初始化。簡單理解就是若是h
函數的參數是組件對象,就將它轉爲一個Vue
的子類,雖然組件VNode
的children
,text
,ele
爲undefined
,但它的獨有屬性componentOptions
保存了組件須要的相關信息。它們的VNode
生成了,接下來的章節咱們將使用它們,將它們變爲真實的Dom
~。
最後咱們仍是以一道vue
可能會被問到的面試題做爲本章的結束吧~
面試官微笑而又不失禮貌的問道:
vue@2
爲何要引入虛擬Dom
,談談對虛擬Dom
的理解?懟回去:
JavaScript
線程去頻繁操做GUI
線程的碩大Dom
,對性能會有很大的損耗,並且也會形成狀態難以管理,邏輯混亂等狀況。引入虛擬Dom
後,在框架的內部就將虛擬Dom
樹形結構與真實Dom
作了映射,讓咱們不用在命令式的去操做Dom
,能夠將重心轉爲去維護這棵樹形結構內的狀態便可,狀態的變化就會驅動Dom
發生改變,具體的Dom
操做vue
幫咱們完成,並且這些大部分能夠在JavaScript
線程完成,性能更高。Dom
只是一種數據結構,可讓它不只僅使用在瀏覽器環境,還能夠用與SSR
以及Weex
等場景。順手點個贊或關注唄,找起來也方便~