Vue是如何挂载Dom的
虚拟Dom
在挂载之前首先要了解虚拟Dom的概念。虚拟Dom,顾名思义并不是真实的Dom,而是使用JavaScript的对象来对真实Dom的一个描述。
那为什么要用虚拟Dom呢?
- 因为浏览器中真实的DOM节点对象上的属性和方法比较多,如果每次都生成新的DOM对象,对性能是一种浪费。而如果用虚拟Dom,当数据更新时,会先比较相应的VNode的数据,然后对虚拟Dom进行创建节点,修改节点和删除节点等操作,最后才更新真实的Dom,可以大大的提升性能。
- 可以跨平台。
一个真实的Dom也无非是有标签名,属性,子节点等这些来描述它,如页面中的真实Dom是这样的:
<div id='app' class='wrap'>
<h2>
hello
</h2>
</div>
我们可以在render函数内这样描述它:
new Vue({
render(h) {
return h('div', {
attrs: {
id: 'app',
class: 'wrap'
}
}, [
h('h2', 'hello')
])
}
})
render
函数最终返回的是一个VNode
类,也就是上面说的虚拟DOm,找到它定义的地方:
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
}
}
它支持接收8个参数,内置23个属性。很多,看着都吓人,大概知道这些属性是啥意思就行了。
开始挂载阶段
this._init() 方法的最后:
... 初始化
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
vm.$mount
如果用户有传入el属性,就执行vm.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) { ---| 这一段主要作用就是把template转换成render函数
if(this.$options.template) { ---|
...经过编译器转换后得到render函数 ---| 编译阶段
} ---|
} ---|
return mount.call(this, query(el))
}
-----------------------------------------------
这里返回的是一个真实的Dom
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
}
}
tips:
vue.js : vue.js则是直接用在<script>
标签中的,完整版本,直接就可以通过script引用。
vue.common.js :预编译调试时,CommonJS规范的格式,可以使用require("")引用的NODEJS格式。
vue.esm.js:预编译调试时, EcmaScript Module(ES MODULE),支持import from 最新标准的。
vue.runtime.js :生产的运行时,需要预编译,比完整版小30%左右,前端性能最优
vue.runtime.esm.js:生产运行时,esm标准。
vue.runtime.common.js:生产运行时,commonJS标准。
这里主要看完整版,有个小操作,首先将$mount
方法缓存到mount
变量上,然后使用函数劫持的手段重新定义$mount
函数,并在其内部增加编译相关的代码,最后还是使用原来定义的$mount
方法挂载。所以核心是要了解最初定义$mount
方法时内的mountComponent
方法:
初定义的$mount方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
初定义$mount方法里的mountComponent函数
mountComponent
方法,主要看里面的updateComponent
的方法。它是一个更新组件的方法,把它传给Watcher
,之后数据变了之后再通知给它更新。
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el // el 为一个真实的Dom
if (!vm.$options.render) { // 没有render的时候,设置createEmptyVNode为render方法
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')
let updateComponent = () => {
// 这里的vm._render()返回的是一个vnode,下面会说
vm._update(vm._render(), hydrating)
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
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
}
mountComponent
方法里最重要的方法是updateComponent
,它的内部首先会执行vm._render()
方法,将返回的结果传入vm._update()
内再执行,来看下它的定义:
_render
Vue.prototype._render = function() {
const vm = this
const { render } = vm.$options
const vnode = render.call(vm, vm.$createElement)
return vnode
}
_render里的$createElement
先获取到用户传进来的render
方法,然后传入vm.$createElement
这个方法(也就是上面例子内的h
方法),将返回的vnode
返回出去。这里要了解render
是怎样转换成vnode
的,跳到之前初始化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) // 手写
_c
和$createElement
只有最后一个参数是true/false
的区别。
我们平时其实很少写render
函数,这是因为vue-loader
帮我们把template
编译成了render
函数。
$createElement/_c里的createElement
再看createElement
这个函数,这里现在我们只关注手写的
const SIMPLE_NORMALIZE = 1 // 编译render函数
const ALWAYS_NORMALIZE = 2 // 手写render函数
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) { // data是数组或基础类型
// tag 相当于是 h 函数的第一个参数
// data 是第二个
// children 是第三个
// normalizationType 是第四个
// 参数移位
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) { // alwaysNormalize 等于true就是手写的render
normalizationType = ALWAYS_NORMALIZE // 代表手写render函数
}
return _createElement(context, tag, data, children, normalizationType)
}
createElement
主要是对传入的参数做处理,实际操作都在_createElement
这个函数里:
createElement里的_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) // 将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
}
上面是简化的_createElement
,先对传进来的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) // 小操作,利用apply接收的第二个参数为数组实现拼接
}
}
return children
}
再看对手写的render
的children
的处理:
假设render函数是这样的
render(h) {
return h(
"div",
[
[
[h("h1", "title h1")],
[h('h2', "title h2")]
],
[
h('h3', 'title h3')
]
]
);
}
// 假设参数是上面这段
function normalizeChildren(children) { // 手写`render`的处理函数
return isPrimitive(children) //原始类型 typeof为string/number/symbol/boolean之一
? [createTextVNode(children)] // 转为数组的文本节点
: Array.isArray(children) // 如果是数组
? normalizeArrayChildren(children)
: undefined
}
因为_createElement
方法是对h方法的封装,所以h方法的第一个参数对应的就是_createElement
方法内的tag
,第二个参数对应的是data
。又因为h方法是递归的,所以首先从h('h1', 'title h1')
开始解析,经过参数上移之后children
就是title h1
这段文本了。
举个简单的例子理解上面这段话:
let fn = (a,b)=>{
return a + b
}
fn(fn(1,2),3) // 这段肯定是先执行 fn(1,2),然后再执行外层的对叭
如果再复杂一点
fn(fn(fn(1,fn(3,4)),2),3) // 那就是从 fn(3,4这里执行),因为这是递归的
普通的元素节点转化为VNode
接着会满足_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
}
组件转化为VNode
接下来我们来了解组件VNode
的创建过程,常见示例如下:
// main.js
new Vue({
render(h) {
return h(App)
}
})
// app.vue
import Child from '@/pages/child'
export default {
name: 'app',
components: {
Child
}
}
按照上面的流程,是组件的话走的就是下面这个条件:
export function _createElement(
context, tag, data, children, normalizationType
) {
...
if(typeof tag === 'string') { // 标签
...
} else { // 就是组件了
vnode = createComponent(
tag, // 组件对象
data, // undefined
context, // 当前vm实例
children // undefined
)
}
...
return vnode
}
createComponent
如果是创建组件节点,会调用createComponent()
方法:
export function createComponent ( // 上
Ctor, data = {}, context, children, tag
) {
// _base为在initGlobalAPI里定义的属性 Vue.options._base = Vue
const baseCtor = context.$options._base // Vue
if (isObject(Ctor)) { // 组件对象
Ctor = baseCtor.extend(Ctor) // 转为Vue的子类
}
...
}
extend
_base
属性和extend
方法都是在定义全局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
}
我们传入的组件对象相当于就是之前new Vue(options)
里面的options
,然后和vue
之前就定义的原型方法以及全局API
合并,然后返回一个新的构造函数,它拥有Vue
完整的功能。
继续createComponent
的逻辑:
export function createComponent ( // 中
Ctor, data = {}, context, children, tag
) {
...
// let asyncFactory
// if (isUndef(Ctor.cid)) { // 异步占位符相关
// asyncFactory = Ctor
// Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
// if (Ctor === undefined) {
// // return a placeholder node for async component, which is rendered
// // as a comment node but preserves all the raw information for the node.
// // the information will be used for async server-rendering and hydration.
// return createAsyncPlaceholder(
// asyncFactory,
// data,
// context,
// children,
// tag
// )
// }
// }
// data = data || {}
// // resolve constructor options in case global mixins are applied after
// // component constructor creation
// resolveConstructorOptions(Ctor)
// // transform component v-model data into props & events
// if (isDef(data.model)) {
// transformModel(Ctor.options, data)
// }
...
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag) // 获取组件中props的数据
const listeners = data.on // 父组件v-on传递的事件对象格式
data.on = data.nativeOn // 组件的原生事件
---- 举个例子
<comp :list="list" @comfirm="comfirm" @click.native="testTap"></comp>
Vue.component('comp', {
template: '<h1>自定义组件!</h1>',
props:{
list:{
type:Array,
}
}
})
new Vue({
el: '#app',
data () {
return {
list:[1,2,3]
}
},
methods: {
testTap(){
console.log('testTap')
},
comfirm(){
console.log('comfirm')
}
},
})
这里通过解析出来的
propsData = {list: Array(3)}
data = {attrs: {}, on: {comfirm: ƒ}, nativeOn: {click: ƒ}}
listeners = {comfirm: ƒ}
data.nativeOn = {click: ƒ}
最后 data.on = data.nativeOn 走完后
data.on = {click: ƒ}
----
/*
* 重点:installComponentHooks
* 它的作用是往组件的data属性下挂载hook这个对象,
* 里面有init,prepatch,insert,destroy四个方法,这四个方法会在之后的将VNode转为真实Dom的patch阶段会用到
*/
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, // 为原生事件
hook: {
init: function(){...},
insert: function(){...},
prepatch: function(){...},
destroy: function(){...}
}
}
}
这就是组件的VNode
,如果看到tag
属性是vue-component
开头就是组件了,虽然组件VNode
的children
,text
,ele
为undefined
,但它的独有属性componentOptions
保存了组件需要的相关信息。
最后总结一下挂载流程:
-
Vue
初始化的最后一步,vm.$mount(vm.$options.el)
-
const mount = Vue.prototype.$mount
获取最初定义的$mount
-
拓展编译后的
$mount
,完整版会增加一个编译函数,主要是判断是否有render
函数,有就直接用上一步获取到的初定义的$mount
进行挂载,没有就把用户传的template
转换成render
函数,再通过初定义的$mount
进行挂载。mount.call(this, query(el))
-
其实无论是运行时版还是完整版,都是用的最初定义的$mount方法,里面是通过
mountComponent
方法挂载。 -
mountComponent
方法里先调用beforeMount
钩子函数,再定义了一个updateComponent
函数,
let updateComponent = () => {
// 这里的vm._render()返回的是一个vnode,下面会说
vm._update(vm._render(), hydrating) // patch 发生在_update阶段
}
这里会先执行vm._render()
方法,然后把返回结果传到_update
方法里。
-
5.1
vm._render()
_render
主要作用是执行vm.$options
里的render
方法(可能是用户传的也可能是编译后得到的),会把h
方法传进去,render
方法最终返回的是一个VNode
-
5.2
h
函数
h
函数里首先会对满足条件的参数做移位处理,然后会分别对手写的render
和编译的render
做children
参数的处理,处理完后会分别对html
标签和组件做创建VNode
的操作,最后把VNode
返回- 5.2.1
html
标签生成VNode
- 5.2.2 组件对象生成
VNode
- 5.2.1
- 再把这个函数放到
Watcher
里
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
- 最后调用
mounted
钩子函数