閱讀資源推薦
【讀vue 源碼】溯源 import Vue from 'vue' 到底做了什么?
前言
Vue.js 一個(gè)核心思想是數(shù)據(jù)驅(qū)動。也就是說視圖是由數(shù)據(jù)驅(qū)動生成的,我們對視圖的修改,不會直接操作 DOM,而是通過修改數(shù)據(jù)。當(dāng)交互復(fù)雜的時(shí)候,只關(guān)心數(shù)據(jù)的修改會讓代碼的邏輯變的非常清晰,因?yàn)?DOM 變成了數(shù)據(jù)的映射,我們所有的邏輯都是對數(shù)據(jù)的修改,而不用碰觸 DOM,這樣的代碼非常利于維護(hù)。
在 Vue.js 中我們可以采用簡潔的模板語法來聲明式的將數(shù)據(jù)渲染為 DOM:
<div id="app">
{{ msg }}
</div>
var app = new Vue({
el: '#app',
data: {
msg: 'Hello world!'
}
})
結(jié)果頁面上會展示出Hello world!。這是入門vue.js的時(shí)候就知道的知識。那么現(xiàn)在要問vue.js的源碼到底做了什么,才能讓模版和數(shù)據(jù)最終被渲染成了DOM???
從 new Vue() 開始
在寫vue 項(xiàng)目的時(shí)候,會在項(xiàng)目的入口文件 main.js文件里實(shí)例化一個(gè)vue 。
如下:
var app = new Vue({
el: '#app',
data: {
msg: 'Hello world!'
},
})
由上一篇文章最后的結(jié)論可知,Vue 就是一個(gè)用 Function 實(shí)現(xiàn)的類。源碼如下:在src/core/instance/index.js中
// _init 方法所在的位置
import { initMixin } from './init'
// Vue就是一個(gè)用 Function 實(shí)現(xiàn)的類,所以才通過 new Vue 去實(shí)例化它。
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)
}
當(dāng)我們在項(xiàng)目中 new Vue({})傳入一個(gè)對象的時(shí)候,其實(shí)就是執(zhí)行的上面的方法,并傳入?yún)?shù)為 options ,然后調(diào)用了this._init(options)方法。該方法在src/core/instance/init.js文件中。代碼如下:
import { initState } from './state'
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// 定義了uid
vm._uid = uid++
let startTag, endTag
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
vm._isVue = true
// 合并options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
// 這里將傳入的options全部合并在$options上。
// 因此我們可以通過$el訪問到 vue 項(xiàng)目中new Vue 中的el
// 通過$options.data 訪問到 vue 項(xiàng)目中new Vue 中的data
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// 初始化函數(shù)
vm._self = vm
initLifecycle(vm) // 生命周期函數(shù)
initEvents(vm) // 初始化事件鏈
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 判斷當(dāng)前的$options.el是否有el 也就是說是否傳入掛載的DOM對象
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
由以上代碼可知 this._init(options)主要是合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。重要的部分在代碼里做里注釋。
那么接下來依然從其中一個(gè)功能為例進(jìn)行分析:以initState(vm)為例:
為什么在鉤子函數(shù)里可以訪問到 data 里定義的數(shù)據(jù)?
vue 項(xiàng)目中,當(dāng)定義了 data 就可以在組件的鉤子函數(shù) 或者 在 methods 函數(shù)里都可以訪問到data 里定義的屬性。這是為什么??
var app = new Vue({
el: '#app',
data:(){
return{
msg: 'Hello world!'
}
},
mounted(){
console.log(this.msg) // logs 'Hello world!'
},
分析源碼:可以看到this._init(options)方法,在初始化函數(shù)部分有一個(gè) initState(vm)函數(shù)。該方法實(shí)在./state.js中:具體代碼如下:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 如果定義了 props 就初始化props;
if (opts.props) initProps(vm, opts.props)
// 如果定義了methods 就初始化methods;
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
// 如果定義了data,就初始化data;(要分析的內(nèi)容從這里開始)
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
在initState方法中判斷:如果定義了data,就初始化data;繼續(xù)看初始化data 的函數(shù):initData(vm)。代碼如下:
function initData (vm: Component) {
/*
這個(gè)data 就是 我們vue 項(xiàng)目中定義的data。也就是上面例子中的
data(){
return {
msg: 'Hello world!'
}
}
*/
let data = vm.$options.data
// 拿到data 后,做了判斷,判斷它是不是一個(gè)function
data = vm._data = typeof data === 'function'
? getData(data, vm) // 如果是 執(zhí)行了getData()方法 ,這個(gè)方法就是返回data
: data || {}
// 如果不是一個(gè)對象則在開發(fā)環(huán)境報(bào)出一個(gè)警告
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// 拿到data 定義的屬性
const keys = Object.keys(data)
// 拿到props
const props = vm.$options.props
// 拿到 methods
const methods = vm.$options.methods
let i = keys.length
// 做了一個(gè)循環(huán)對比,如果在data 上定義的屬性,就不能在props與methods在定義該屬性。因?yàn)椴还苁莇ata里定義的,在props里定義的,還是在medthods里定義的,最終都掛載在vm實(shí)例上了。見proxy(vm, `_data`, key)
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key) // 代理 定義了Getter 和 Setter
}
}
// observe data
observe(data, true /* asRootData */)
}
// proxy 代理
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
// 通過對象 sharedPropertyDefinition 定義了Getter 和 Setter
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
// 當(dāng)訪問vm.key 的時(shí)候其實(shí)訪問的是 vm[sourceKey][key]
// 以上述開始的問題,當(dāng)訪問this.msg 實(shí)際是訪問 this._data.msg
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
// 對vm 的 key 做了一次Getter 和 Setter
Object.defineProperty(target, key, sharedPropertyDefinition)
}
綜上:初始化 data 實(shí)在./state.js文件里。執(zhí)行initState() 方法,該方法判斷如果定義了data,就初始化data。
如果data 是一個(gè)function,就執(zhí)行了getData()方法return data.call(vm, vm)。然后對 vm 上的 data 里定義的屬性、vm上的 props 、vm上的methods里的屬性進(jìn)行循環(huán)比對,如果在data 上定義的屬性,就不能在props與methods在定義該屬性。因?yàn)椴还苁莇ata里定義的,在props里定義的,還是在medthods里定義的,最終都掛載在vm實(shí)例上了。見proxy(vm, _data, key)。
然后通過proxy 方法給vm 上的屬性做了Getter 和 Setter 方法的綁定?;氐缴鲜龅膯栴},當(dāng)訪問this.msg 實(shí)際是訪問 vm._data.msg。因此在鉤子函數(shù)里確實(shí)可以訪問到 data 里定義的數(shù)據(jù)了。
不得不在說一遍,Vue 的初始化邏輯寫的非常清楚,把不同的功能邏輯拆成一些單獨(dú)的函數(shù)執(zhí)行,讓主線邏輯一目了然,這樣的編程思想是非常值得借鑒和學(xué)習(xí)的。
其它初始化的內(nèi)容大家可以自己補(bǔ)充,接下來看掛載vm。在初始化的最后,檢測到如果有 el 屬性,則調(diào)用 vm.$mount 方法掛載 vm,掛載的目標(biāo)就是把模板渲染成最終的 DOM,那么接下來探究 Vue 的掛載過程吧
Vue 實(shí)例掛載的實(shí)現(xiàn)
Vue 中我們是通過 $mount 實(shí)例方法去掛載 vm 的。接下來要探究執(zhí)行$mount('#app')的時(shí)候,源碼都干了什么???
new Vue({
render: h => h(App),
}).$mount('#app')
$mount 方法在多個(gè)文件中都有定義,如 src/platform/web/entry-runtime-with-compiler.js、src/platform/web/runtime/index.js、src/platform/weex/runtime/index.js。因?yàn)?$mount 這個(gè)方法的實(shí)現(xiàn)是和平臺、構(gòu)建方式都有關(guān)系。
就選取 compiler 版本的 $mount 分析吧,文件地址在src/platform/web/entry-runtime-with-compiler.js,代碼如下:
// 獲取vue 原型上的 $mount 方法, 存在變量 mount 上。
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// query 定義在 './util/index'文件中
// 調(diào)用原生的DOM api querySelector() 方法。最后將el轉(zhuǎn)化為一個(gè)DOM 對象。
el = el && query(el)
...
return mount.call(this, el, hydrating)
}
讀代碼可知,代碼首先獲取了 vue 原型上的 $mount 方法,將其存在變量mount中,然后重新定義了該方法。該方法對傳入的el做了處理,el 可以是個(gè)字符串,也可以是DOM 對象。然后調(diào)用了 query()方法,該方法在./util/index文件中。主要是調(diào)用原生的DOM api querySelector() 方法。最后將el轉(zhuǎn)化為一個(gè)DOM 對象返回。上述只貼出了主要的代碼部分。
源碼了還對el進(jìn)行了判斷,判斷傳入的el 是否為body 或者 html ,如果是,就會在開發(fā)環(huán)境報(bào)一個(gè)警告。vue 不可以直接掛載到body 和html上 ,因?yàn)闀桓采w,當(dāng)覆蓋了 html 或 body 整個(gè)文檔就會報(bào)錯(cuò)。
源碼還獲取到 $options 判斷是否定義render方法。如果沒有定義 render 方法,則會把 el 或者 template 字符串最終將編譯為render()函數(shù)。
最后 return mount.call(this, el, hydrating)。此處的mount是vue 原型上的 $mount 方法。在文件./runtime/index。代碼如下:
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
其中參數(shù) el 表示掛載的元素,它可以是字符串,也可以是一個(gè)DOM 對象。如果是字符串在瀏覽器環(huán)境下會調(diào)用 query() 方法轉(zhuǎn)換成 DOM 對象。第二個(gè)參數(shù)是和服務(wù)端渲染相關(guān),在瀏覽器環(huán)境下我們不需要傳第二個(gè)參數(shù)。最后return 的時(shí)候調(diào)用了mountComponent()方法。該方法定義在src/core/instance/lifecycle.js,代碼如下:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
...
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
讀代碼可知,該方法首先實(shí)例化一個(gè)渲染Watcher,在它的回調(diào)函數(shù)中會調(diào)用 updateComponent 方法,在此方法中調(diào)用 vm._render() 方法先生成虛擬DOM節(jié)點(diǎn),最終調(diào)用 vm._update 更新 DOM。
最后判斷為根節(jié)點(diǎn)的時(shí)候設(shè)置 vm._isMounted 為 true, 表示這個(gè)實(shí)例已經(jīng)掛載了,同時(shí)執(zhí)行 mounted 鉤子函數(shù)。 vm.$vnode 表示 Vue 實(shí)例的父虛擬節(jié)點(diǎn),所以它為 Null 則表示當(dāng)前是根 Vue 的實(shí)例。
那么vm._render()是怎樣生成虛擬DOM節(jié)點(diǎn)的呢?
_render()渲染虛擬DOM 節(jié)點(diǎn)
在 Vue 2.0 版本中,所有 Vue 的組件的渲染最終都需要 render()。Vue 的 _render() 是實(shí)例的一個(gè)私有方法,它用來把實(shí)例渲染成一個(gè)虛擬DOM節(jié)點(diǎn)。它的定義在 src/core/instance/render.js 文件中,代碼如下:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
...
let vnode
try {
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
}
}
上述代碼 從vue實(shí)例的 $options 上獲取到 render 函數(shù)。通過call()調(diào)用了_renderProxy和 createElement()方法,先來探索createElement()方法。
createElement()
createElement()是在initRender()中。如下:
// 該函數(shù)是在 _init() 過程中執(zhí)行 initRender()
// 見 './init.js' 文件中的 initRender(vm) 傳入vm。就執(zhí)行到下面的方法。
export function initRender (vm: Component) {
// 被編譯后生成的render函數(shù)
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// 手寫render函數(shù) 創(chuàng)建 vnode 的方法。
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}
initRender()是在 _init過程中執(zhí)行了initRender()見 ./init.js 文件中的 initRender(vm)傳入vm。
在 vue 項(xiàng)目實(shí)際開發(fā)中,手寫 render 函數(shù) 案例如下:
new Vue({
render(createElement){
return createElement('div',{
style:{color:'red'}
},this.msg)
},
data(){
return{
msg:"hello world"
}
}
}).$mount('#app')
因?yàn)槭鞘謱懙膔ender函數(shù)省去了將 template 編譯為 render函數(shù)的過程,因此性能更好。
接下來看_renderProxy方法:
_renderProxy
_renderProxy方法,也是在 init 過程中執(zhí)行的。見文件./init.js中,代碼如下:
import { initProxy } from './proxy'
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
如果當(dāng)前環(huán)境為生產(chǎn)環(huán)境 就將 vm 直接賦值給 vm._renderProxy;
如果當(dāng)前環(huán)境為開發(fā)環(huán)境,則執(zhí)行initProxy()。
該函數(shù)在./proxy.js文件中,代碼如下:
initProxy = function initProxy (vm) {
// 判斷瀏覽器是否支持 proxy 。
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
首先判斷瀏覽器是否支持 proxy。它是ES6 新增的,用于給目標(biāo)對象之前架設(shè)一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機(jī)制,可以對外界的訪問進(jìn)行過濾和改寫。
如果瀏覽器不支持 proxy, 就將 vm 直接賦值給 vm._renderProxy;
如果瀏覽器支持 proxy,就執(zhí)行new Proxy()。
綜上所述:vm._render 是通過執(zhí)行 createElement 方法并返回虛擬的DOM 節(jié)點(diǎn)。那么什么是虛擬的DOM呢???
虛擬的DOM
在探究vue 的虛擬DOM 之前,先推薦一個(gè)虛擬DOM開源庫。有時(shí)間,有興趣的朋友可以去深入了解。它是用一個(gè)函數(shù)去表示一個(gè)應(yīng)用程序的視圖層。view.js 是借鑒它實(shí)現(xiàn)了虛擬DOM。從而大大的提升了程序的性能。接下來我們就來看vue.js是怎么做的。
vnode 的定義在 src/core/vdom/vnode.js文件中,如下:
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
...
}
虛擬DOM 是個(gè)js對象,是對真實(shí)DOM 的一種抽象描述,比如標(biāo)簽名、數(shù)據(jù)、子節(jié)點(diǎn)名等。因?yàn)樘摂MDOM只是用來映射真實(shí)DOM的渲染,所以不包含操作DOM的方法操作DOM的方法。因此更加的輕量,更加的簡單。因?yàn)樘摂MDOM 的創(chuàng)建是通過createElement方法,那這個(gè)環(huán)節(jié)又是如何實(shí)現(xiàn)的呢???
createElement
Vue.js 利用 createElement 方法創(chuàng)建 DOM節(jié)點(diǎn),它定義在 src/core/vdom/create-elemenet.js文件中,代碼如下:
export function createElement (
context: Component, // vm 實(shí)例
tag: any, // 標(biāo)簽
data: any, // 數(shù)據(jù)
children: any,// 子節(jié)點(diǎn) 可以構(gòu)造DOM 樹
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 對參數(shù)不一致的處理
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
// 處理好參數(shù),則調(diào)用 _createElement() 去真正的創(chuàng)建節(jié)點(diǎn)。
return _createElement(context, tag, data, children, normalizationType)
}
createElement 方法是對 _createElement 方法的封裝,它允許傳入的參數(shù)更加靈活,在處理這些參數(shù)后,調(diào)用真正創(chuàng)建 DOM 節(jié)點(diǎn)的函數(shù)_createElement,代碼如下:
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
...
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
...
}
_createElement 方法提供 5 個(gè)參數(shù)如下:
-
context表示DOM節(jié)點(diǎn)的上下文環(huán)境,它是 Component 類型; -
tag表示標(biāo)簽,它可以是一個(gè)字符串,也可以是一個(gè) Component; -
data表示 DOM節(jié)點(diǎn)上的數(shù)據(jù),它是一個(gè) VNodeData 類型,可以在flow/vnode.js中找到它的定義; -
children表示當(dāng)前DOM節(jié)點(diǎn)的子節(jié)點(diǎn),它是任意類型的,它接下來需要被規(guī)范為標(biāo)準(zhǔn)的 VNode 數(shù)組; -
normalizationType表示子節(jié)點(diǎn)規(guī)范的類型,類型不同規(guī)范的方法也就不一樣,它主要是參考 render 函數(shù)是編譯生成的還是手寫的 render 函數(shù)。
createElement 函數(shù)的流程略微有點(diǎn)多,本文將重點(diǎn)探究 children 的規(guī)范化以及 VNode 的創(chuàng)建。
children 的規(guī)范化
虛擬DOM(Virtual DOM)實(shí)際上是一個(gè)樹狀結(jié)構(gòu),每一個(gè)DOM 節(jié)點(diǎn)都可能會有若干個(gè)子節(jié)點(diǎn),這些子節(jié)點(diǎn)應(yīng)該也是 VNode 的類型。
_createElement 接收的第 4 個(gè)參數(shù) children 是任意類型的,因此我們需要把它們規(guī)范成 VNode 類型。
它是根據(jù) normalizationType 的不同,調(diào)用了 normalizeChildren(children) 和 simpleNormalizeChildren(children) 方法,它們的定義都在 src/core/vdom/helpers/normalzie-children.js文件 中,代碼如下:
// render 函數(shù)是編譯生成的時(shí)候調(diào)用
// 拍平數(shù)組為一維數(shù)組
export function simpleNormalizeChildren (children: any) {
for (let i = 0; i < children.length; i++) {
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children)
}
}
return children
}
// 返回一維數(shù)組
export function normalizeChildren (children: any): ?Array<VNode> {
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
simpleNormalizeChildren 方法調(diào)用場景是 render 函數(shù)是編譯生成的。但是當(dāng)子節(jié)點(diǎn)為一個(gè)組件的時(shí)候,函數(shù)式組件返回的是一個(gè)數(shù)組而不是一個(gè)根節(jié)點(diǎn),所以會通過 Array.prototype.concat 方法把整個(gè) children 數(shù)組拍平,讓它的深度只有一層。
normalizeChildren 方法的調(diào)用場景有 2 種,一個(gè)場景是手寫 render 函數(shù),當(dāng) children 只有一個(gè)節(jié)點(diǎn)的時(shí)候,Vue.js 從接口層面允許用戶把 children 寫成基礎(chǔ)類型用來創(chuàng)建單個(gè)簡單的文本節(jié)點(diǎn),這種情況會調(diào)用 createTextVNode 創(chuàng)建一個(gè)文本節(jié)點(diǎn)的DOM 節(jié)點(diǎn);另一個(gè)場景是當(dāng)編譯 slot、v-for 的時(shí)候會產(chǎn)生嵌套數(shù)組的情況,會調(diào)用 normalizeArrayChildren 方法,代碼如下:
function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
const res = []
let i, c, lastIndex, last
for (i = 0; i < children.length; i++) {
c = children[i]
if (isUndef(c) || typeof c === 'boolean') continue
lastIndex = res.length - 1
last = res[lastIndex]
// nested
if (Array.isArray(c)) {
if (c.length > 0) {
c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
// merge adjacent text nodes
if (isTextNode(c[0]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
c.shift()
}
res.push.apply(res, c)
}
} else if (isPrimitive(c)) {
if (isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + c)
} else if (c !== '') {
res.push(createTextVNode(c))
}
} else {
// 如果兩個(gè)節(jié)點(diǎn)都為文本節(jié)點(diǎn),則合并他們。
if (isTextNode(c) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
if (isTrue(children._isVList) &&
isDef(c.tag) &&
isUndef(c.key) &&
isDef(nestedIndex)) {
c.key = `__vlist${nestedIndex}_${i}__`
}
res.push(c)
}
}
}
return res
}
normalizeArrayChildren 接收 2 個(gè)參數(shù)。
-
children表示要規(guī)范的子節(jié)點(diǎn); -
nestedIndex表示嵌套的索引;
因?yàn)閱蝹€(gè)child可能是一個(gè)數(shù)組類型。normalizeArrayChildren主要是遍歷children,獲得單個(gè)節(jié)點(diǎn)c,然后對c的類型判斷,如果是一個(gè)數(shù)組類型,則遞歸調(diào)用normalizeArrayChildren; 如果是基礎(chǔ)類型,則通過createTextVNode方法轉(zhuǎn)換成 VNode 類型;否則就已經(jīng)是 VNode 類型了,如果children是一個(gè)列表并且列表還存在嵌套的情況,則根據(jù)nestedIndex去更新它的key。
在遍歷的過程中,對這 3 種情況都做了如下處理:如果存在兩個(gè)連續(xù)的 text 節(jié)點(diǎn),會把它們合并成一個(gè) text 節(jié)點(diǎn)。
到此,children 變成了一個(gè)類型為 VNode 的 Array。這就是children 的規(guī)范化。
虛擬的DOM節(jié)點(diǎn)的創(chuàng)建
回到 createElement 函數(shù),規(guī)范化 children 后,接下來就要創(chuàng)建一個(gè)DOM實(shí)例,代碼如下:
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 不認(rèn)識的節(jié)點(diǎn)的處理
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
這里先對 tag 做判斷,如果是 string 類型,則接著判斷如果是內(nèi)置的一些節(jié)點(diǎn),則直接創(chuàng)建一個(gè)普通 VNode,如果是為已注冊的組件名,則通過 createComponent 創(chuàng)建一個(gè)組件類型的 VNode,否則創(chuàng)建一個(gè)未知的標(biāo)簽的 VNode。 如果 tag是一個(gè) Component 類型,則直接調(diào)用 createComponent 創(chuàng)建一個(gè)組件類型的 VNode 節(jié)點(diǎn)。
到這一步,createElement方法就創(chuàng)建好了一個(gè)虛擬DOM樹的實(shí)例,它用來描述了真實(shí)DOM 樹,那么如何渲染為真實(shí)的DOM 樹呢???其實(shí)它是由 vm._update 完成的。
update把虛擬DOM 渲染為真實(shí)DOM
_update 方法是如何把虛擬DOM 渲染為真實(shí)DOM 的。這部分代碼在 src/core/instance/lifecycle.js文件中,代碼如下:
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
if (!prevVnode) {
// 數(shù)據(jù)的首次渲染時(shí)候執(zhí)行
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
}
...
}
讀代碼可知,當(dāng)數(shù)據(jù)首次渲染的時(shí)候,調(diào)用了vm.__patch__()的方法,他接收了四個(gè)參數(shù),結(jié)合我們實(shí)際vue項(xiàng)目的開發(fā)過程。vm.$el就是 id 為 app 的 DOM 對象,即:<div id="app"></div>;vnode 對應(yīng)的是調(diào)用 render 函數(shù)的返回值;hydrating 在非服務(wù)端渲染情況下為 false,removeOnly 為 false。
vm.__patch__ 方法在不同的平臺的定義是不一樣的,對 web 平臺的定義在 src/platforms/web/runtime/index.js 中,代碼如下:
// 是否在瀏覽器環(huán)境
Vue.prototype.__patch__ = inBrowser ? patch : noop
在 web 平臺上,是否是服務(wù)端渲染也會對這個(gè)方法產(chǎn)生影響。因?yàn)樵诜?wù)端渲染中,沒有真實(shí)的瀏覽器 DOM 環(huán)境,所以不需要把 VNode 最終轉(zhuǎn)換成 DOM,因此是一個(gè)空函數(shù),而在瀏覽器端渲染中,它指向了 patch 方法,它的定義在 src/platforms/web/runtime/patch.js文件中,代碼如下:
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
讀代碼可知 createPatchFunction 方法的返回值被傳入了一個(gè)對象,其中,
-
nodeOps封裝了一系列 DOM 操作的方法; -
modules定義了模塊的鉤子函數(shù)的實(shí)現(xiàn);
createPatchFunction方法的定義在src/core/vdom/patch.js文件中,代碼如下:
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
// ...
// 定義了一些輔助函數(shù)
// 當(dāng)調(diào)用 vm.__dispatch__時(shí),其實(shí)就是調(diào)用下面的 patch 方法
// 這塊應(yīng)用了函數(shù)柯理化的技巧
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
return vnode.elm
}
}
createPatchFunction 內(nèi)部定義了一系列的輔助方法,最終返回了一個(gè) patch 方法,這個(gè)方法就賦值給了 vm._update函數(shù)里調(diào)用的 vm.__patch__。也就是說當(dāng)調(diào)用 vm.__dispatch__時(shí),其實(shí)就是調(diào)用patch (oldVnode, vnode, hydrating, removeOnly) 方法,這塊其實(shí)是應(yīng)用了函數(shù)柯理化的技巧。
patch 方法接收 4個(gè)參數(shù),如下:
-
oldVnode表示舊的 VNode 節(jié)點(diǎn),它也可以不存在或者是一個(gè) DOM 對象; -
vnode表示執(zhí)行 _render 后返回的 VNode 的節(jié)點(diǎn); -
hydrating表示是否是服務(wù)端渲染; -
removeOnly是給 transition-group 用的。
分析patch方法,因?yàn)閭魅氲?code>oldVnode實(shí)際上是一個(gè) DOM container,所以 isRealElement 為 true,然后調(diào)用 emptyNodeAt 方法把 oldVnode 轉(zhuǎn)換成 虛擬DOM節(jié)點(diǎn)(一個(gè)js對象),然后再調(diào)用 createElm 方法。代碼如下:
if (isRealElement) {
oldVnode = emptyNodeAt(oldVnode)
}
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
// 接下來判斷 vnode 是否包含 tag,
// 如果包含,先對tag的合法性在非生產(chǎn)環(huán)境下做校驗(yàn),看是否是一個(gè)合法標(biāo)簽;
// 然后再去調(diào)用平臺 DOM 的操作去創(chuàng)建一個(gè)占位符元素。
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
// 調(diào)用 createChildren 方法去創(chuàng)建子元素:
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// ...
} else {
// 調(diào)用 createChildren 方法去創(chuàng)建子元素
// 用 createChildren 方法遍歷子虛擬節(jié)點(diǎn),遞歸調(diào)用 createElm
// 在遍歷過程中會把 vnode.elm 作為父容器的 DOM 節(jié)點(diǎn)占位符傳入。
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
createElm方法的作用是通過虛擬節(jié)點(diǎn)創(chuàng)建真實(shí)的 DOM 并插入到它的父節(jié)點(diǎn)中。判斷 vnode 是否包含 tag,如果包含,先對 tag 的合法性在非生產(chǎn)環(huán)境下做驗(yàn)證,看是否是一個(gè)合法標(biāo)簽;然后再去調(diào)用平臺 DOM 的操作去創(chuàng)建一個(gè)占位符元素。然后調(diào)用 createChildren 方法去創(chuàng)建子元素,createChildren方法代碼如下:
createChildren(vnode, children, insertedVnodeQueue)
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(children)
}
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
createChildren方法遍歷子虛擬節(jié)點(diǎn),遞歸調(diào)用 createElm,在遍歷過程中會把 vnode.elm 作為父容器的 DOM 節(jié)點(diǎn)占位符傳入。然后調(diào)用 invokeCreateHooks方法執(zhí)行所有的 create 的鉤子并把 vnode push 到 insertedVnodeQueue 中。最后調(diào)用 insert 方法把 DOM 插入到父節(jié)點(diǎn)中,因?yàn)槭沁f歸調(diào)用,子元素會優(yōu)先調(diào)用 insert,所以整個(gè) vnode 樹節(jié)點(diǎn)的插入順序是先子后父。insert 方法定義在 src/core/vdom/patch.js 文件中,代碼如下:
insert(parentElm, vnode.elm, refElm)
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (ref.parentNode === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
讀代碼可知,insert方法調(diào)用一些輔助方法把子節(jié)點(diǎn)插入到父節(jié)點(diǎn)中(其實(shí)就是調(diào)用原生 DOM 的 API 進(jìn)行 DOM 操作),這些輔助方法定義在 src/platforms/web/runtime/node-ops.js 文件中。到此,Vue 動態(tài)創(chuàng)建的 DOM節(jié)點(diǎn)就完成了。emm~~ 回頭在看看這個(gè)圖。
結(jié)束
最近一段時(shí)間都會認(rèn)真的去看vue.js的源碼?!咀xvue 源碼】會按照一個(gè)系列去更新。分享自己學(xué)習(xí)的同時(shí),也希望與更多的同行交流所得,如此而已。
第一篇:【讀vue 源碼】溯源 import Vue from 'vue' 到底做了什么?
第二篇:【讀vue源碼】探究模版和數(shù)據(jù)是如何被渲染成DOM的? 【當(dāng)前在讀】