上文详细解析了模版编译generate函数的内部实现,是如何拼接render字符串的。
本文将回归到模版编译的上层方法中,解析如何执行render函数生成VNode对象,利用patch函数将VNode对象渲染成DOM节点
执行renderString生成render函数
上文解析完generate函数之后,回归到baseCompile方法中,实际此方法的返回即是generate函数的返回(包含ast、code等属性的对象)。回归到调用baseCompile方法的compile函数中,此函数也是直接返回baseCompile函数的返回值。最后回归到调用compile方法的compileToFunction函数中
// compileToFunction函数中 模版编译完的后续实现
const render = (__GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom)) as RenderFunction
// mark the function as runtime compiled
;(render as InternalRenderFunction)._rc = true
return (compileCache[key] = render)
后续的实现中将生成的render字符串作为参数传入到new Funtion中生成函数,然后执行这个函数。依上文最终得到的字符串,生成函数之后执行得到如下真正的render方法:
function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock(_Fragment, null, [
_createTextVNode(_toDisplayString(message) + " ", 1 /* TEXT */),
_createVNode("button", { onClick: modifyMessage }, "修改数据", 8 /* PROPS */, ["onClick"])
], 64 /* STABLE_FRAGMENT */)
}
}
然后将render函数的_rc属性设为true,将render函数存入缓存对象compileCache中,key为template模版字符串,value为render函数,并且返回render函数。回归到finishComponentSetup方法中
// finishComponentSetup方法模版编译之后的后续实现
instance.render = (Component.render || NOOP) as InternalRenderFunction
if (instance.render._rc) {
instance.withProxy = new Proxy(
instance.ctx,
RuntimeCompiledPublicInstanceProxyHandlers
)
}
// support for 2.x options ...
将返回的render函数赋值给instance对象的render属性。判断render._rc为true(在上面方法的结尾处赋值_rc属性为true),将instance.ctx对象进行Proxy代理,并将代理返回的对象赋值给instance.withProxy属性,看下Proxy代理的钩子对象(RuntimeCompiledPublicInstanceProxyHandlers)的内部实现
export const RuntimeCompiledPublicInstanceProxyHandlers = extend({}, PublicInstanceProxyHandlers,
{
get(target: ComponentRenderContext, key: string) {
// fast path for unscopables when using `with` block
},
has(_: ComponentRenderContext, key: string) {
//...has
}
}
)
此对象中主要是设置了get和has的陷阱钩子函数(后续使用到withProxy属性触发钩子函数时再详细解析)。随着finishComponentSetup函数的执行完成,最终回归到mountComponent函数中(调用链setupComponent -> setupStatefulComponent -> handleSetupResult -> finishComponentSetup,这里相关方法的调用关系可以去看下文章二"双向数据绑定"),当setupComponent函数执行完成之后,开始执行setupRenderEffect函数,解析下此方法的内部实现
全局依赖activeEffect(reactiveEffect)
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
instance.update = effect(function componentEffect() {/*...*/},
__DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
}
此方法主要是执行effect方法,传参是componentEffect函数 和 createDevEffectOptions(instance)函数的返回值(一个包含{scheduler, allowRecurse: true, onTrack, onTrigger}四个属性的对象,后续使用到具体属性时再详细解析),先来看下effect方法的内部实现
// effect方法定义
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}
effect方法内部主要是执行createReactiveEffect函数,参数仍然是fn和options
// createReactiveEffect方法定义
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {/* ... */} as ReactiveEffect
effect.id = uid++
effect.allowRecurse = !!options.allowRecurse
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
此函数内部定义了reactiveEffect函数,并在函数上新增了一些属性(active=true、deps=[])。然后将这个函数返回了。回归到上层的effect函数中,effect变量等于createReactiveEffect函数的返回值,就是reactiveEffect方法,然后判断options.lazy(为false,options中并无lazy属性),开始执行effect方法(即reactiveEffect函数)。看下reactiveEffect函数内部的具体实现
const effectStack: ReactiveEffect[] = []
// cleanup 方法定义
function cleanup(effect: ReactiveEffect) {
const { deps } = effect
// createReactiveEffect函数执行时设置deps为[]
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
let activeEffect: ReactiveEffect | undefined
// reactiveEffect 函数内部实现
function reactiveEffect(): unknown {
if (!effect.active) {
// createReactiveEffect方法执行时已经将active属性置为true
return options.scheduler ? undefined : fn()
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
}
let shouldTrack = true
const trackStack: boolean[] = []
// enableTracking 方法定义
export function enableTracking() {
trackStack.push(shouldTrack)
shouldTrack = true
}
reactiveEffect内部判断全局的effectStack数组中是否已经存在effect方法,再调用cleanup方法将effect.deps清空(初始化时为空数组)。之后调用enableTracking方法,此方法的作用是在全局跟踪数组trackStack中添加全局标识位shouldTrack(初始化为true),并将shouldTrack置为true。回归到reactiveEffect方法中,随后将effect方法存入全局effectStack数组中,并将effect方法赋值给全局的activeEffect变量。然后执行fn方法。fn方法就是调用createReactiveEffect函数传入的fn,就是componentEffect函数,看下此函数的内部实现。
执行render函数生成vnode对象
// componentEffect函数的内部实现
function componentEffect() {
if (!instance.isMounted) {
// 初始化为挂载过,所以isMounted属性为false
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance
// ...
const subTree = (instance.subTree = renderComponentRoot(instance))
// ...
}
else {/*...*/}
}
此方法内部主要是执行了renderComponentRoot函数,传参为instance对象
export function renderComponentRoot(
instance: ComponentInternalInstance
): VNode {
const {
type: Component,
vnode,
proxy,
withProxy,
props,
propsOptions: [propsOptions],
slots,
attrs,
emit,
render,
renderCache,
data,
setupState,
ctx
} = instance
let result
currentRenderingInstance = instance
// ...
try {
let fallthroughAttrs
if (vnode.shapeFlag/* 4 */ & ShapeFlags.STATEFUL_COMPONENT/* 4 */) {
const proxyToUse = withProxy || proxy
result = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
fallthroughAttrs = attrs
} else {} //...
} catch(err) {
// ...
}
}
renderComponentRoot函数内部首先通过render!.call(proxyToUse, ...)方法执行instance.render函数(本文开头已经展示了依本例模版解析后的render函数),传参是proxyToUse(就是withProxy对象)和renderCache(空数组[]),下面详细解析render函数的执行过程:
1、整个函数体都在with(_ctx){}中,如果对with的用法不熟悉,可以了解下。简单来说就是在with花括号里面的属性不需要指定命名空间,会自动指向_ctx对象;with(Proxy){key}会触发Proxy代理的has钩子函数(_ctx对象就是withProxy对象,本文上面提到了withProxy就是instance.ctx对象通过Proxy代理后的对象)
2、const { ... } = _Vue对_Vue对象进行结构,首先会触发_ctx的has钩子函数(因为ctx上并没有_Vue属性,这里就忽略,后续再详细解析has钩子函数)。回顾到之前解析完成的render String开头部分,定义const _Vue = Vue,也就是_Vue就是全局的Vue对象。那解构出来的一系列方法就是全局的Vue暴露的方法(toDisplayString, createVNode, createTextVNode, Fragment, openBlock, createBlock)
执行openBlock函数(生成存放子vnode对象的数组)
3、后续执行render函数中return的内容。首先是执行openBlock函数(无参数)
export const blockStack: (VNode[] | null)[] = []
let currentBlock: VNode[] | null = null
// openBlock 函数定义
export function openBlock(disableTracking = false) {
blockStack.push((currentBlock = disableTracking ? null : []))
}
openBlock函数内部给全局的currentBlock变量赋值空数组[],然后将这个变量push到另一个全局的空数组blockStack中,即blockStack=[[]],后续创建VNode会使用这个全局数组
解析动态数据(依赖收集至全局targetMap对象)
4、然后执行createBlock方法,其中第三个参数是数组,数组的第一项是createTextVNode函数的返回值,执行createTextVNode函数时参数有两个,第一个参数是toDisplayString方法的执行结果,参数是message。这里因为with(_ctx){message}会触发has钩子函数,看下has钩子函数的具体内部实现
// RuntimeCompiledPublicInstanceProxyHandlers 对象中的get、has钩子函数的内部实现
get(target: ComponentRenderContext, key: string) {
// fast path for unscopables when using `with` block
if ((key as any) === Symbol.unscopables) {
return
}
return PublicInstanceProxyHandlers.get!(target, key, target)
},
has(_: ComponentRenderContext, key: string) {
const has = key[0] !== '_' && !isGloballyWhitelisted(key)
if (__DEV__ && !has && PublicInstanceProxyHandlers.has!(_, key)) {
warn(
`Property ${JSON.stringify(
key
)} should not start with _ which is a reserved prefix for Vue internals.`
)
}
return has
}
判断属性名称
key值不是以'_'开头的,并且不是特定的一些字符串,类似Object、Boolean等(具体可以去看下isGloballyWhitelisted方法的内部实现),此时key值为message,所以has为true
之后获取
messgae的值,_ctx.message会触发get钩子函数,先判断属性名是否等于Symbol.unscopables,此时key值为message,所以执行PublicInstanceProxyHandlers的get方法。一起看下PublicInstanceProxyHandlers内部get方法的具体实现
// PublicInstanceProxyHandlers内部get钩子函数的具体实现
get({ _: instance }: ComponentRenderContext, key: string) {
const {
ctx,
setupState,
data,
props,
accessCache,
type,
appContext
} = instance
// ...
let normalizedProps
if (key[0] !== '$') {
const n = accessCache![key]
if (n !== undefined) {}
else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
accessCache![key] = AccessTypes.SETUP // 0
return setupState[key]
}
// ...
}
}
首先会对target对象进行结构,获取'_'属性的值,target就是_ctx对象,本文前面提到了_ctx是instance.ctx的Proxy代理对象(关于instance.ctx对象上的_属性的值,在文章二"双向数据绑定"中提到了instance对象的创建,并新增_属性值为instance)。回归到get钩子函数中,判断属性名key(message)不是以'$'开头的,并且不存在于instance的accessCache缓存对象中,再判断instance.setupState属性不是空对象,并且message存在于setupState对象中(在文章三"双向数据绑定"中提到instance.setupState就是setup函数执行完成之后返回的结果再通过Proxy代理的对象)。本例中setupState不是空对象并且message也是此对象的属性,所以设置accessCache[message] = 0,最终返回setupState[message]的值。
因为
setupState对象是setup函数返回值的Proxy对象,所以执行setupState[message]时会触发get钩子函数
// unref方法定义
export function unref<T>(ref: T): T extends Ref<infer V> ? V : T {
return isRef(ref) ? (ref.value as any) : ref
}
// shallowUnwrapHandlers对象中 get钩子函数的实现
const shallowUnwrapHandlers: ProxyHandler<any> = {
get: (target, key, receiver) => unref(Reflect.get(target, key, receiver))
}
首先通过Reflect方法获取setupState.message的值(文章三"双向数据绑定"中解析了message属性值是调用ref方法返回的RefImpl实例对象)。然后调用unref方法,判断入参的__v_isRef属性是否为true,本例中message符合,所以返回ref.value(message.value)。因为message是RefImpl的实例对象,所以获取属性时会触发get钩子函数
// RefImpl类 内部的get钩子函数
get value() {
track(toRaw(this), TrackOpTypes.GET/* "get" */, 'value')
return this._value
}
const targetMap = new WeakMap<any, KeyToDepMap>()
// track方法定义
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!shouldTrack || activeEffect === undefined) {
return
}
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}
钩子函数内部先调用track函数收集依赖,函数内部先判断,targetMap(WeakMap对象)全局对象中是否存在target属性(初始化挂载是不存在的),若不存在则执行targetMap.set(target, (depsMap = new Map())),设置key=target,value=new Map()(空的Map对象),然后获取depsMap(Map对象)中key=message的属性值,因为depsMap是新建的空Map对象,所以也不存在message属性,固执行depsMap.set(key, (dep = new Set())),设置key=message,value=new Set()(空的Set对象)。因为dep是空的Set对象,所以往dep对象中新增activeEffect全局变量(本文上述解析过activeEffect就是reactiveEffect函数),然后在reactiveEffect方法的deps数组中添加dep对象(Set对象)。后续在修改message的值之后触发set钩子函数时会执行依赖,更新DOM
createTextVNode创建文本vnode对象
5、回归到render函数中,根据一系列的Proxy代理得到message="测试数据"(以本例为模版解析),开始执行toDisplayString('测试数据')
export const toDisplayString = (val: unknown): string => {
return val == null
? ''
: isObject(val)
? JSON.stringify(val, replacer, 2)
: String(val)
}
toDisplayString函数最终返回String(val)就是'测试数据'
6、接着开始执行createTextVNode函数,参数为"测试数据 " 和 1
// createTextVNode 函数定义
export function createTextVNode(text: string = ' ', flag: number = 0): VNode {
return createVNode(Text, null, text, flag)
}
createTextVNode函数内部调用createVNode方法,参数为Symbol('Text')、null、'测试数据 '、1,在文章二"数据双向绑定"中已经简单解析了createVNode方法的作用,主要是生成一个VNode对象,在初始化执行app.mount时会使用,初始化执行时type是调用createApp传入的参数。现在是用来生成一个文本节点,看下createVNode内部的具体实现
首先根据
type类型给shapeFlag赋值,因为type是Symbol('Text'),所以shapeFlag=0
创建
vnode对象,其中patchFlag属性值为1
接着调用
normalizeChildren函数,此函数主要是用来处理节点的children属性
export function normalizeChildren(vnode: VNode, children: unknown) {
let type = 0
const { shapeFlag } = vnode
if (children == null) {}
else if (isArray(children)) {}
else if (typeof children === 'object') {}
else if (isFunction(children)) {}
else {
children = String(children)
// force teleport children to array so it can be moved around
if (shapeFlag & ShapeFlags.TELEPORT) {
type = ShapeFlags.ARRAY_CHILDREN
children = [createTextVNode(children as string)]
} else {
type = ShapeFlags.TEXT_CHILDREN // 8
}
}
vnode.children = children as VNodeNormalizedChildren
vnode.shapeFlag |= type
}
因为本例中文本节点的子节点是字符串并且shapeFlag=0,所以vnode.children='测试数据',vnode.shapeFlag = 0 | 8 = 8。之后回归到createVNode方法中,执行currentBlock.push(vnode)将vnode存放到全局数组currentBlock中(本文上述解析openBlock方法时将currentBlock赋值为空数组),然后返回vnode对象。
createVNode创建元素vnode对象
7、回归到render方法中,执行完文本节点之后开始执行元素节点(button节点,直接调用createVNode方法),参数为"button", { onClick: modifyMessage }, '修改数据', 8。
根据
type是字符串类型,所以shapeFlag赋值为1
创建
vnode对象,赋值props属性为{ onClick: modifyMessage },patchFlag为8
调用
normalizeChildren处理children属性,此元素节点的children也是字符串,所以vnode.children='修改数据',vnode.shapeFlag = 1 | 8 = 9,最后将vnode对象存入currentBlock数组中并返回vnode对象
createBlock创建根vnode对象
8、回归到render函数中,当中括号的两个方法(createTextVNode、createVNode)执行完成后,最后执行createBlock方法生成根vnode对象
export function createBlock(
type: VNodeTypes | ClassComponent,
props?: Record<string, any> | null,
children?: any,
patchFlag?: number,
dynamicProps?: string[]
): VNode {
const vnode = createVNode(
type,
props,
children,
patchFlag,
dynamicProps,
true /* isBlock: prevent a block from tracking itself */
)
// save current block children on the block vnode
vnode.dynamicChildren = currentBlock || (EMPTY_ARR as any)
// close block
closeBlock()
// a block is always going to be patched, so track it as a child of its
// parent block
if (shouldTrack > 0 && currentBlock) {
currentBlock.push(vnode)
}
return vnode
}
在createBlock内部首先是调用createVNode方法创建vnode节点,参数是Symbol('Fragment')、null、[文本vnode对象, 元素vnode对象]、64、true
createVNode方法中首先根据type是Symbol类型,shapeFlag赋值为0。创建vnode对象,patchFlag值为64
执行
normalizeChildren函数,处理children属性时,因为children是数组,所以vnode.shapeFlag = 0 | 16 = 16
因为传入的
isBlockNode=true,所以不会执行currentBlock.push(vnode),最后返回vnode对象。
回归到createBlock函数中,将vnode.dynamicChildren属性赋值为currentBlock数组(数组中包含文本vnode对象 和 元素vnode对象两个元素,也就是vnode.children)。然后执行closeBlock
export function closeBlock() {
blockStack.pop()
currentBlock = blockStack[blockStack.length - 1] || null
}
该方法内将blockStack数组中的最后一项移除。由本文上述解析可知,blockStack数组中只有一个元素,就是currentBlock数组,然后将currentBlock赋值为null。最后createBlock方法返回vnode对象,type为Symbol('Fragment'),children数组包含两个vnode对象,是type为Symbol('Text')的文本 和 type为'button'的元素。至此render函数的解析已经完全结束了。
总结
本文主要是详细解析render函数的执行过程,首先解析动态数据message时触发get钩子函数,调用track方法进行依赖的收集(activeEffect变量收集到全局的targetMap对象中);然后调用createTextVNode方法构建文本vnode对象;调用createVNode方法构建button元素的vnode对象;最后调用createBlock方法构建根vnode对象。后续将详细解析patch方法利用生成的vnode对象构建出真正的DOM元素