前言
本文通过vue2
源码,浅析vue2
中的new Vue()
发生了什么,涉及了初始化、挂载、虚拟dom
、真实dom
的生成。
js
// vue2中new Vue()使用
new Vue({
router,
render: h => h(App),
}).$mount('#app')
初始化
入口文件,传入配置项,调用_init
方法进行初始化。
js
// src\core\instance\index.ts
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
//@ts-expect-error Vue has function type
initMixin(Vue) // 初始化Vue构造函数中,调用的this._init()方法
//@ts-expect-error Vue has function type
stateMixin(Vue)
//@ts-expect-error Vue has function type
eventsMixin(Vue)
//@ts-expect-error Vue has function type
lifecycleMixin(Vue)
//@ts-expect-error Vue has function type
renderMixin(Vue)
this._init()
方法主要步骤:
- 合并配置项;
- 初始化生命周期、初始化事件中心、初始化渲染、调用
beforeCreate
钩子,初始化injection
、state
(data
、props
、computed
...)、provide
、调用create
钩子; - 最后通过判断有无
vm.$options.el
进行vm.$mount
挂载。
this._init()
源码:
js
// 初始化混入
export function initMixin(Vue: typeof Component) {
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
// 性能检测
if (__DEV__ && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// 一个标志,将其标记为 Vue 实例,而无需执行实例
vm._isVue = true
// 避免实例添加observer
vm.__v_skip = true
// 影响范围
vm._scope = new EffectScope(true /* detached */)
vm._scope._vm = true
// 合并配置:业务逻辑以及组件的一些特性全都放到了vm.$options中
if (options && options._isComponent) {// 普通组件
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options as any)
} else {// 顶层的vm
vm.$options = mergeOptions(
/*一些内置组件(keep-alive...)和指令(show、model...)...*/
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
/* istanbul ignore else */
// 初始化vm._renderProxy 为后期渲染做准备
// 开发环境并支持proxy vm._renderProxy = new Proxy(vm)
if (__DEV__) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化生命周期
initLifecycle(vm)
// 初始化事件
initEvents(vm)
// 初始化渲染
initRender(vm)
// 调用beforeCreate回调
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
// 初始化injections
initInjections(vm) // resolve injections before data/props
// 初始化state
initState(vm)
// 初始化provide
initProvide(vm) // resolve provide after data/props
// 调用create回调
callHook(vm, 'created')
/* istanbul ignore if */
// 性能检测
if (__DEV__ && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 如果有el 没有el 不挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
挂载
生成render函数------vm.prototype.$mount()
这段代码是上面_init()
方法最后调用的vm.$mount()
方法。这个方法主要作用是若配置项中没有render
方法则将template
作为编译函数的参数生成该方法,最后调用runtime
时的mount
方法(后面会讲到)。
mount
方法主要步骤如下:
- 判断
el
是否是body
或html
标签,如果是直接返回,中断挂载。若是开发环境,会发出警告。 - 如果配置项中不存在
render
函数,接下来会通过template
得到html
的内容,若配置中没有template
,则通过el
来获取。最后根据上面获取到的html
内容对其进行编译,最终生成render
函数并将其挂载在vm.$options.render
中。 编译主要通过三个方法。parse
方法是通过大量的正则表达式对html
字符串进行解析,将标签、指令、属性等转化为抽象语法树AST
。optimize
方法是遍历AST
,标记节点类型,比如对静态节点进行标记,方便在页面更新渲染时进行diff
比较时,直接跳过这些静态节点,优化runtime
的性能。generate
方法是最终的AST
转化为render
函数字符串。
- 调用
runtime
时的mount
方法。
mount
源码:
js
// src\platforms\web\runtime-with-compiler.ts
// runtime moute函数
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
// 1. 不允许挂载在html 或者 body身上
if (el === document.body || el === document.documentElement) {
__DEV__ &&
warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
// 获得配置项
const options = this.$options
// resolve template/el and convert to render function
// 不存在render
if (!options.render) {
let template = options.template
/*---------------------------- 2. 初始化template ------------------------------*/
if (template) {
if (typeof template === 'string') {
// 如果模板开头是# 说明是一个id选择器 通过idToTemplate获取相应的innerHtml
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (__DEV__ && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {// 是node对象 直接获取innerHTML
template = template.innerHTML
} else {// 不合法配置项
if (__DEV__) {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// @ts-expect-error
template = getOuterHTML(el)
}
/*----------------------------- 3. 将模板转化为render函数 --------------------------- */
if (template) {
/* istanbul ignore if */
// 性能检测
if (__DEV__ && config.performance && mark) {
mark('compile')
}
// 3.1把模板转换成render函数 得到ast抽象树获得的render函数
const { render, staticRenderFns } = compileToFunctions(
template,
{
outputSourceRange: __DEV__,
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
},
this
)
// 3.2保存渲染函数 with(this){return _c('div',{attrs:{\"id\":\"app\"}},[_v(\"\\n......
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
// 性能检测
if (__DEV__ && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 4. 调用$mount方法
return mount.call(this, el, hydrating)
}
完成挂载------mountComponent
这是runtime
时的mount
方法。最后直接调用了mountComponent
方法。所以直接看mountComponent
方法吧。
js
// 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)
}
mountComponent
方法完成了vue
从beforeMount
到mounted
的过程。主要步骤如下:
render
函数是生成dom
的关键,所以在一开始会先判断是否存在,如果不存在创建一个空的虚拟dom
;- 调用生命周期钩子
beforeMount
; - 生成
updateComponent
方法,用于触发页面的更新; - 生成
watcherOptions
配置,里面before
方法会触发beforeUpdate
钩子,将会在触发updateComponent
前调用; - 生成组件更新的
watcher
,前面两部分都是为了这部分做准备。new Watcher()
后会触发updateComponent
方法的调用,生成页面虚拟dom
,将watcher
加入到影响页面变化data
的依赖收集器中,这样当data
发生变化时,就会触发页面更新,最终进行dom diff
,生成真实dom
(详细的后面会说到); - 调用生命周期钩子
mounted
。
mountComponent
方法源码如下:
js
// src\core\instance\lifecycle.ts
export function mountComponent(
vm: Component,
el: Element | null | undefined,
hydrating?: boolean
): Component {
vm.$el = el
// 不存在render 创建一个空的vnode 开发环境给出警告
if (!vm.$options.render) {
// @ts-expect-error invalid type
vm.$options.render = createEmptyVNode
if (__DEV__) {
// 省略vue警告
}
}
// 调用beforeMount钩子
callHook(vm, 'beforeMount')
// 初始化updateComponent值
let updateComponent
if (__DEV__ && config.performance && mark) {
// 省略开发环境下性能检测代码
} else {
updateComponent = () => {
// vm._render()得到vnode,vm._update更新页面
vm._update(vm._render(), hydrating)
}
}
// 页面更新前会调用before函数,触发beforeUpdate钩子
const watcherOptions: WatcherOptions = {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}
if (__DEV__) {
watcherOptions.onTrack = e => callHook(vm, 'renderTracked', [e])
watcherOptions.onTrigger = e => callHook(vm, 'renderTriggered', [e])
}
// 创建组件更新的watcher,后面会加入到影响页面变化data的deps中
new Watcher(
vm,
updateComponent,
noop,
watcherOptions,
true /* isRenderWatcher */
)
hydrating = false
// flush buffer for flush: "pre" watchers queued in setup()
const preWatchers = vm._preWatchers
if (preWatchers) {
for (let i = 0; i < preWatchers.length; i++) {
preWatchers[i].run()
}
}
// 完成挂载 触发钩子mounted
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
更新机制形成------new Watcher()
这里主要分析上面new Watcher()
后,如何触发updateComponent
方法的调用。因此只涉及部分Watcher
类的代码。
在new Watcher()
构造方法里会初始化一些属性,最重要的是将this.getter = expOrFn
,将updateComponent
方法赋给了this.getter
。最后初始化this.value
时,调用了this.get()
。
new Watcher()
构造方法源码如下:
js
/**
* vm:实例
* expOrFn:updateComponent方法(触发页面更新)
* cb: function noop(a?: any, b?: any, c?: any) {}
* options:上面的watcherOptions
* isRenderWatcher:渲染类的watcher
*/
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null,
isRenderWatcher?: boolean
) {
// 将watcher挂载到vm._watcher上
if ((this.vm = vm) && isRenderWatcher) {
vm._watcher = this
}
// w记录所有的watcher
w.push(this)
// 根据options初始化一些属性
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
if (__DEV__) {
this.onTrack = options.onTrack
this.onTrigger = options.onTrigger
}
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.post = false
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = __DEV__ ? expOrFn.toString() : ''
// render类型expOrFn肯定是一个方法 所以this.getter就是页面更新方法了
if (isFunction(expOrFn)) {
this.getter = expOrFn
} else {
//...
}
// 因为render类型的watcher lazy值不会是true(只有computed才会是) 所以接下来会调用get方法
this.value = this.lazy ? undefined : this.get()
}
在get
方法中,可以看到这行代码value = this.getter.call(vm, vm)
。就是这里调用了updateComponent
方法。调用了updateComponent
方法会触发vm._update(vm._render(), hydrating)
。所以接下来分析vm._render
方法。
get()
方法如下:
js
get() {
// 相当于Dep.target=this
pushTarget(this)
let value
const vm = this.vm
try {
// 调用this.getter方法,即调用了`updateComponent`方法
value = this.getter.call(vm, vm)
} catch (e: any) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
生成虚拟dom------vm._render()
_render()
函数,这里就不展示完整源码了,只看重点部分。这里主要是调用了vm.$options.render
的方法,该方法用于生成虚拟dom
。它可以是用户自定义的,也可以是编译而成的。最后返回虚拟dom
。
js
// 主要作用执行render 得到vnode
Vue.prototype._render = function (): VNode {
// ...
const vm: Component = this
const { render, _parentVnode } = vm.$options
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
setCurrentInstance(vm)
currentRenderingInstance = vm
// 重点!!!调用render函数 得到vnode
// vm._renderProxy在支持proxy且开发环境下是new Proxy(vm) 其他情况为vm
// vm.$createElement是真正生成虚拟dom的函数
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e: any) {
// ...
} finally {
currentRenderingInstance = null
setCurrentInstance()
}
// ...
return vnode
}
用户自定义render函数
这种情况是用户自定义render
函数。上面调用render
方法处,传了一个vm.$createElement
函数作为参数,所以h
即vm.$createElement
。
js
new Vue({
router,
render: h => h(App),
}).$mount('#app')
找到vm.$createElement
源码发现最终调用的是createElement
方法。
js
// src\core\instance\render.ts
// 这段代码在initRender中
// 用于编译后产生的render函数
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// @ts-expect-error
// 用于用户自己手写的render函数
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
编译生成的render
函数
这段html
代码通过编译后生成的render
函数如下。
js
<div id="app">
{{num1}}
{{hello}}
<button @click="changeNum">num1</button>
<button @click="changeNum2">num2</button>
<div v-for="(item,idx) of arr" :key="item">
{{item}}
</div>
<comp :msg="msg" @log-msg="logMsg"></comp>
</div>
代码中,使用了with(this){}
说明{}
所有引用都指向this
对象(即vm
)。所以代码中的_c = vm._c
,因此最终其实也是调用了createElement
函数生成虚拟dom
。在执行这段代码时,会读取到data
中的属性,比如num1
、arr
等等,读取时会调用这些属性的getter
方法,在getter
方法中会判断到Dep.target
中存在值,会将该值存入依赖收集器,从而完成依赖收集。属性发生改变时,触发setter
方法,通过dep
和watcher
最终会调用updateComponent
方法进行页面的更新。
js
// src\core\instance\render.ts
// 这段代码在initRender中
// 用于编译后产生的render函数
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
如果对里面_s
、_l
函数感兴趣可以找到这段代码来看看。
js
// src\core\instance\render-helpers\index.ts
export function installRenderHelpers(target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
createElement
js
const SIMPLE_NORMALIZE = 1 // 浅扁平
const ALWAYS_NORMALIZE = 2 // 深扁平
export function createElement(
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,/** 使用哪种扁平化方式 */
alwaysNormalize: boolean/** 当render函数是手写的时候为true */
): VNode | Array<VNode> {
// 实际上是判断data是否存在,如果不存在参数向前进一个
if (isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
_creatElement
方法是创建vnode
的核心。
- 规范
children
:children
按照normalizationType
类型进行扁平化处理,这个目的是规范children
,对于同一深度层次的元素,不管是单个元素还是该元素在数组中,只要是属于同一深度层次,都扁平化于一个一维数组中。 - 生成
vnode
:如果是普通标签,直接new Vnode()
。Vue
组件则需要通过createComponent
生成vnode
。
js
// src\core\vdom\create-element.ts
/**
* 生成虚拟dom
* @param context vm
* @param tag 标签(div、p、ul...)
* @param data 标签上的属性
* @param children 子节点
* @param normalizationType
* @returns Vnode 虚拟dom
*/
export function _createElement(
context: Component,
tag?: string | Component | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// vnode data不能为响应式对象,有__ob__代表这个对象为响应式对象
if (isDef(data) && isDef((data as any).__ob__)) {
__DEV__ &&
warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(
data
)}\n` + 'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
// <Component :is=""/>
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (__DEV__ && isDef(data) && isDef(data.key) && !isPrimitive(data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
// support single function children as default scoped slot
if (isArray(children) && isFunction(children[0])) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 根据normalizationType对应不同扁平化处理方式
if (normalizationType === ALWAYS_NORMALIZE) {
// 深层扁平化
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 浅层扁平化
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)) {
// platform built-in elements
if (
__DEV__ &&
isDef(data) &&
isDef(data.nativeOn) &&
data.tag !== 'component'
) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
// 创建普通标签的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
// 为组件,调用createComponent创建vnode
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 直接创建vnode
vnode = new VNode(tag, data, children, undefined, undefined, context)
}
} else {
// direct component options / constructor
// tag为组件构造函数或组件选项,创建Vue组件
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()
}
}
创建Vue组件------createComponent
createComponent
作用是返回Vue
组件的虚拟dom
。同时在这过程中,会构造子类构造函数(这里会调用_init
方法完成组件初始化)、安装组件钩子函数。
源码位置src\core\vdom\create-component.ts
这部分可以直接看这篇文章【Vue源码】8.组件化-createComponent
生成真实dom------vm._update(vnode)
vm._update
方法源码如下,在源码写了注释,这里就不展开了。
js
// src\core\instance\lifecycle.ts
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
// 存储前面的el
const prevEl = vm.$el
// 存储前面的vnode
const prevVnode = vm._vnode
// 存储活动的实例
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates 更新
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
}
}
总结
总体来说,整个过程可以分为四部分,分别是初始化、挂载、生成虚拟节点、生成真实节点,最后调用mounted
钩子。
- 初始化:合并配置项,初始化生命周期、事件、
data
、computed
、watch
等等。完成了beforeCreate
到created
的整个过程。 - 挂载:如果
options.render
不存在,将template
编译生成render
函数。接下来就是建立更新机制,创建watcher
,通过watcher
中的get
方法,调用组件更新updateComponent
函数。 - 生成虚拟
dom
:调用更新函数,会调用vm._render
方法,这个方法会调用vm.$options.render
方法用于生成虚拟dom
。 - 生成真实
dom
:通过vm._render
方法得到虚拟dom
后,会作为vm._update()
方法的参数去生成vnode
相应的真实dom
。