閱讀資源推薦
【讀vue 源碼】溯源 import Vue from 'vue' 到底做了什么?
前言
Vue.js 一個(gè)核心思想是數(shù)據(jù)驅(qū)動(dòng)濒持。也就是說視圖是由數(shù)據(jù)驅(qū)動(dòng)生成的哼御,我們對(duì)視圖的修改腊状,不會(huì)直接操作 DOM纳本,而是通過修改數(shù)據(jù)。當(dāng)交互復(fù)雜的時(shí)候土思,只關(guān)心數(shù)據(jù)的修改會(huì)讓代碼的邏輯變的非常清晰务热,因?yàn)?DOM 變成了數(shù)據(jù)的映射,我們所有的邏輯都是對(duì)數(shù)據(jù)的修改己儒,而不用碰觸 DOM崎岂,這樣的代碼非常利于維護(hù)。
在 Vue.js 中我們可以采用簡(jiǎn)潔的模板語法來聲明式的將數(shù)據(jù)渲染為 DOM:
<div id="app">
{{ msg }}
</div>
var app = new Vue({
el: '#app',
data: {
msg: 'Hello world!'
}
})
結(jié)果頁面上會(huì)展示出Hello world!
闪湾。這是入門vue.js的時(shí)候就知道的知識(shí)冲甘。那么現(xiàn)在要問vue.js的源碼到底做了什么,才能讓模版和數(shù)據(jù)最終被渲染成了DOM?江醇?濒憋?
從 new Vue()
開始
在寫vue 項(xiàng)目的時(shí)候,會(huì)在項(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)我們?cè)陧?xiàng)目中 new Vue({})
傳入一個(gè)對(duì)象的時(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對(duì)象
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è)對(duì)象則在開發(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)對(duì)比螺捐,如果在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) {
// 通過對(duì)象 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
}
// 對(duì)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)
席噩。然后對(duì) vm 上的 data 里定義的屬性班缰、vm上的 props 、vm上的methods里的屬性進(jìn)行循環(huán)比對(duì)悼枢,如果在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渺贤。在初始化的最后,檢測(cè)到如果有 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)是和平臺(tái)、構(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 對(duì)象霎匈。
el = el && query(el)
...
return mount.call(this, el, hydrating)
}
讀代碼可知,代碼首先獲取了 vue 原型上的 $mount
方法送爸,將其存在變量mount中铛嘱,然后重新定義了該方法。該方法對(duì)傳入的el做了處理袭厂,el 可以是個(gè)字符串墨吓,也可以是DOM 對(duì)象。然后調(diào)用了 query()
方法纹磺,該方法在./util/index
文件中帖烘。主要是調(diào)用原生的DOM api querySelector() 方法。最后將el轉(zhuǎn)化為一個(gè)DOM 對(duì)象返回橄杨。上述只貼出了主要的代碼部分蚓让。
源碼了還對(duì)el進(jìn)行了判斷,判斷傳入的el 是否為body 或者 html ,如果是讥珍,就會(huì)在開發(fā)環(huán)境報(bào)一個(gè)警告历极。vue 不可以直接掛載到body 和html上 ,因?yàn)闀?huì)被覆蓋,當(dāng)覆蓋了 html 或 body 整個(gè)文檔就會(huì)報(bào)錯(cuò)衷佃。
源碼還獲取到 $options 判斷是否定義render方法趟卸。如果沒有定義 render 方法,則會(huì)把 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 對(duì)象克婶。如果是字符串在瀏覽器環(huán)境下會(huì)調(diào)用 query()
方法轉(zhuǎn)換成 DOM 對(duì)象筒严。第二個(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ù)中會(huì)調(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)對(duì)象之前架設(shè)一層“攔截”,外界對(duì)該對(duì)象的訪問守呜,都必須先通過這層攔截型酥,因此提供了一種機(jī)制山憨,可以對(duì)外界的訪問進(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對(duì)象拯杠,是對(duì)真實(shí)DOM 的一種抽象描述,比如標(biāo)簽名啃奴、數(shù)據(jù)潭陪、子節(jié)點(diǎn)名等。因?yàn)樘摂MDOM只是用來映射真實(shí)DOM的渲染最蕾,所以不包含操作DOM的方法操作DOM的方法依溯。因此更加的輕量,更加的簡(jiǎn)單瘟则。因?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> {
// 對(duì)參數(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
方法是對(duì) _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)都可能會(huì)有若干個(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)用場(chǎng)景是 render 函數(shù)是編譯生成的疮丛。但是當(dāng)子節(jié)點(diǎn)為一個(gè)組件的時(shí)候幔嫂,函數(shù)式組件返回的是一個(gè)數(shù)組而不是一個(gè)根節(jié)點(diǎn),所以會(huì)通過 Array.prototype.concat
方法把整個(gè) children
數(shù)組拍平誊薄,讓它的深度只有一層履恩。
normalizeChildren
方法的調(diào)用場(chǎng)景有 2 種,一個(gè)場(chǎng)景是手寫 render 函數(shù)呢蔫,當(dāng) children
只有一個(gè)節(jié)點(diǎn)的時(shí)候切心,Vue.js 從接口層面允許用戶把 children
寫成基礎(chǔ)類型用來創(chuàng)建單個(gè)簡(jiǎn)單的文本節(jié)點(diǎn),這種情況會(huì)調(diào)用 createTextVNode
創(chuàng)建一個(gè)文本節(jié)點(diǎn)的DOM 節(jié)點(diǎn)片吊;另一個(gè)場(chǎng)景是當(dāng)編譯 slot
绽昏、v-for
的時(shí)候會(huì)產(chǎn)生嵌套數(shù)組的情況,會(huì)調(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
卷员,然后對(duì)c
的類型判斷盈匾,如果是一個(gè)數(shù)組類型,則遞歸調(diào)用normalizeArrayChildren
; 如果是基礎(chǔ)類型毕骡,則通過createTextVNode
方法轉(zhuǎn)換成 VNode 類型削饵;否則就已經(jīng)是 VNode 類型了,如果children
是一個(gè)列表并且列表還存在嵌套的情況挺峡,則根據(jù)nestedIndex
去更新它的key
葵孤。
在遍歷的過程中,對(duì)這 3 種情況都做了如下處理:如果存在兩個(gè)連續(xù)的 text
節(jié)點(diǎn)橱赠,會(huì)把它們合并成一個(gè) text
節(jié)點(diǎn)尤仍。
到此,children 變成了一個(gè)類型為 VNode 的 Array狭姨。這就是children 的規(guī)范化宰啦。
虛擬的DOM節(jié)點(diǎn)的創(chuàng)建
回到 createElement
函數(shù),規(guī)范化 children
后饼拍,接下來就要?jiǎng)?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)識(shí)的節(jié)點(diǎn)的處理
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
這里先對(duì) tag
做判斷,如果是 string
類型师抄,則接著判斷如果是內(nèi)置的一些節(jié)點(diǎn)漓柑,則直接創(chuàng)建一個(gè)普通 VNode,如果是為已注冊(cè)的組件名叨吮,則通過 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 對(duì)象谬盐,即:<div id="app"></div>
甸私;vnode
對(duì)應(yīng)的是調(diào)用 render 函數(shù)的返回值;hydrating
在非服務(wù)端渲染情況下為 false
,removeOnly
為 false飞傀。
vm.__patch__
方法在不同的平臺(tái)的定義是不一樣的皇型,對(duì) web 平臺(tái)的定義在 src/platforms/web/runtime/index.js
中,代碼如下:
// 是否在瀏覽器環(huán)境
Vue.prototype.__patch__ = inBrowser ? patch : noop
在 web 平臺(tái)上,是否是服務(wù)端渲染也會(huì)對(duì)這個(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è)對(duì)象,其中门粪,
-
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 對(duì)象辜御; -
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對(duì)象),然后再調(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场绿,
// 如果包含,先對(duì)tag的合法性在非生產(chǎn)環(huán)境下做校驗(yàn)嫉入,看是否是一個(gè)合法標(biāo)簽焰盗;
// 然后再去調(diào)用平臺(tái) 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
// 在遍歷過程中會(huì)把 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,如果包含澎粟,先對(duì) tag 的合法性在非生產(chǎn)環(huán)境下做驗(yàn)證蛀序,看是否是一個(gè)合法標(biāo)簽;然后再去調(diào)用平臺(tái) 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
,在遍歷過程中會(huì)把 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)用回懦,子元素會(huì)優(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 動(dòng)態(tài)創(chuàng)建的 DOM節(jié)點(diǎn)就完成了。emm~~ 回頭在看看這個(gè)圖稚晚。
結(jié)束
最近一段時(shí)間都會(huì)認(rèn)真的去看vue.js的源碼崇堵。【讀vue 源碼】會(huì)按照一個(gè)系列去更新客燕。分享自己學(xué)習(xí)的同時(shí)鸳劳,也希望與更多的同行交流所得,如此而已也搓。
第一篇:【讀vue 源碼】溯源 import Vue from 'vue' 到底做了什么?
第二篇:【讀vue源碼】探究模版和數(shù)據(jù)是如何被渲染成DOM的赏廓? 【當(dāng)前在讀】