初始化
我们在实例化一个Vue
实例,也就是new Vue()
时,实际上是执行src/core/instance/index.ts
文件中定义的Vue
函数
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
通过查看Vue
的function
,我们知道Vue
只能通过new
关键字初始化,然后调用this._init
方法,该方法在src/core/instance/init.ts
文件中定义
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
// 省略一系列其它初始化的代码
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
Vue
初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化data
、props
、computed
、watcher
等等
Vue实例挂载
Vue
中是通过$mount
实例方法去挂载 dom 的,下面我们通过分析compiler
版本的mount
实现,相关源码在目录src/platforms/web/runtime-with-compiler.ts
文件中定义:
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component {
el = el && query(el)
// 省略一系列初始化以及逻辑判断代码
return mount.call(this, el, hydrating)
}
我们发现最终还是调用了原先原型上的$mount
方法挂载,原先原型上的$mount
方法在src/platforms/web/runtime/index.ts
文件中定义
Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
我们发现$mount
方法实际上会去调用mountComponent
方法,这个方法定义在src/core/instance/lifecycle.ts
文件中
export function mountComponent(
vm: Component,
el: Element | null | undefined,
hydrating?: boolean
): Component {
vm.$el = el
// 省略一系列其它代码
let updateComponent
if (__DEV__ && config.performance && mark) {
updateComponent = () => {
// ...
// 生成虚拟 vnode
const vnode = vm._render()
// ..
// 更新 DOM
vm._update(vnode, hydrating)
// ..
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
const watcherOptions: WatcherOptions = {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
},
}
// 省略一系列其它代码
// 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法
new Watcher(vm, updateComponent, noop, watcherOptions, true /* isRenderWatcher */)
// 省略一系列其它代码
return vm
}
从上面的代码可以看到,mountComponent
核心就是先实例化一个渲染Watcher
,在它的回调函数中会调用updateComponent
方法,在此方法中调用vm._render
方法先生成虚拟VNode
,最终调用vm._update
更新 DOM
创建虚拟VNode
Vue
的_render
方法是实例的一个私有方法,它用来把实例渲染成一个虚拟VNode
。它的定义在src/core/instance/render.ts
文件中
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// 省略一系列代码
let vnode
try {
setCurrentInstance(vm)
currentRenderingInstance = vm
// 调用 createElement 方法来返回 vnode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e: any) {
handleError(e, vm, `render`)
// ...
} finally {
currentRenderingInstance = null
setCurrentInstance()
}
// 省略一系列代码
vnode.parent = _parentVnode
return vnode
}
vm._render
最终是通过执行createElement
方法并返回的是vnode
,它是一个虚拟 Node
Vue
利用_createElement
方法创建VNode
,它定义在src/core/vdom/create-elemenet.ts
文件中
export function _createElement(
context: Component,
tag?: string | Component | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 省略一系列非主线代码
if (normalizationType === ALWAYS_NORMALIZE) {
// 场景是 render 函数不是编译生成的
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 场景是 render 函数是编译生成的
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
//...
// 创建虚拟 vnode
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined,
undefined,
context
)
} else if (
(!data || !data.pre) &&
isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(tag, data, children, undefined, undefined, context)
}
} else {
vnode = createComponent(tag as any, data, context, children)
}
if (isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
_createElement
方法有 5 个参数,context
表示VNode
的上下文环境,它是Component
类型;tag
表示标签,它可以是一个字符串,也可以是一个Component
;data
表示VNode
的数据,它是一个VNodeData
类型,可以在types/vnode.ts
中找到它的定义;children
表示当前VNode
的子节点,它是任意类型的,需要被规范为标准的VNode
数组
更新视图
完成视图的更新工作事实上就是调用了vm._update
方法,这个方法接收的第一个参数是刚生成的Vnode
,调用的vm._update
方法定义在src/core/instance/lifecycle.ts
文件中
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
// 第一个参数为真实的node节点,则为初始化
if (!prevVnode) {
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 如果需要diff的prevVnode存在,那么对prevVnode和vnode进行diff
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
let wrapper: Component | undefined = vm
while (
wrapper &&
wrapper.$vnode &&
wrapper.$parent &&
wrapper.$vnode === wrapper.$parent._vnode
) {
wrapper.$parent.$el = wrapper.$el
wrapper = wrapper.$parent
}
}
往后参考Diff 算法篇章
总结
那么至此我们从主线上把模板和数据如何渲染成最终的 DOM 的过程分析完毕了,我们可以通过下图更直观地看到从初始化Vue
到最终渲染的整个过程