携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情
前言
源码分析文章看了很多,也阅读了至少两遍源码。终归还是想自己写写,作为自己的一种记录和学习。重点看注释部分和总结,其余不用太关心,通过总结对照源码回看过程和注释收获更大
组件挂载入口
compiler版会将only版的$mount
方法先缓存起来,扩展完功能后再调用,compiler版将template
转化成render函数就会执行mountComponent
方法挂载组件
// /core/platforms/web/runtime/index.js
Vue.prototype.$mount = function ( //公共的$mount
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating) //组件挂载
}
// /core/platforms/web/entry-runtime-with-compiler.js
//缓存mount方法
const mount = Vue.prototype.$mount
// 做扩展
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
...
// 调用组件挂载方法
return mount.call(this, el, hydrating)
}
复制代码
组件挂载的方法
// /src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// 对el做缓存
vm.$el = el
callHook(vm, 'beforeMount') //beforeMount生命周期函数
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
...
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () { //更新钩子
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true )// 参数true表示是一个渲染watcher
hydrating = false //不是服务端渲染
if (vm.$vnode == null) {
vm._isMounted = true //已经渲染过了
callHook(vm, 'mounted')//当前组件挂载完毕 mounted声明周期函数
}
return vm
}
复制代码
render函数生虚拟节点
在模板编译时会将ast生成render函数,其中函数部分形式为_c('div', {id: 'app'}, _c('span', {}, 'world'), _v('hello'))
,其中包括_c、_v、_s
三个函数,用于创建不同的元素
_c
函数: 主要是对解析出来的tag及属性做处理
// /src/core/instance/render.js
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// /src/core/vdom/create-element.js
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
...
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
...
// 如果没有tag返回空vnode
if (!tag) {
return createEmptyVNode()
}
...
let vnode
if (typeof tag === 'string') {
let Ctor
// 如果是html标签,创建vnode,否则创建组件
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
vnode = createComponent(tag, data, context, children)
}
}
复制代码
_v
函数: 主要用于创建文本vnode
// /src/core/instance/render-helpers/index.js
target._v = createTextVNode
// /src/core/vdom/vnode.js
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
复制代码
_s
函数: 主要对模板里的内容做格式化
// /src/core/instance/render-helpers/index.js
target._s = toString
// /shared/util.js
export function toString (val: any): string {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2)
: String(val)
}
复制代码
虚拟dom转化成真实dom
在初始化时,会将初次生成的vnode储存起来,以便更新的时候做diff对比,初次diff是真实的dom节点和生成的vnode做对比,之后根据vnode调用js方法创建真实dom并替换掉el的位置,初始渲染完成
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// /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 // 当前render函数产生的虚拟节点
if (!prevVnode) {
// 初次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
} else {
// diff比较更新
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
复制代码
更新
在初始化渲染挂载时候,会新建一个渲染watcher
,在new watcher
过程中,会去执行updateComponent方法进行模板渲染,同时会将当前的渲染watcher储存在访问模板值得dep中,当模板值发生变化时,会调用dep.notify
方法通知收集的watcher进行更新,也就是会再次调用updateComponent方法从而重新渲染
// /src/core/instance/lifecycle.js
new Watcher(vm, updateComponent, noop, {
before () { //更新钩子
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true )
复制代码
总结
compiler版$mount
方法在only版的基础上做了扩展,将template
转化成ast后,再将ast转化成render函数,然后调用_update
方法进行初始化渲染,此时会进行diff算法比较后创建真实dom,初次比较的节点是options中的el
节点和生成的虚拟节点作比较,并将虚拟节点保存起来作为下次的比较对象。由于初次挂载是会创建一个渲染watcher,所以再访问模板值得时候会将当前渲染watcher收集在响应的dep中,当模板值发生变化时,会触发setter拦截,调用notify
方法通知所有收集的watcher进行更新,会再次调用updateComponent
方法,经diff比较后更新视图
系列链接
【Vue2.x原理剖析一】响应式原理
【Vue2.x原理剖析二】计算属性原理
【Vue2.x原理剖析三】侦听属性原理
【Vue2.x原理剖析四】模板编译原理
【Vue2.x原理剖析五】初始渲染及更新原理