Vue3追本溯源(七)执行render生成VNode对象

上文详细解析了模版编译generate函数的内部实现,是如何拼接render字符串的。
本文将回归到模版编译的上层方法中,解析如何执行render函数生成VNode对象,利用patch函数将VNode对象渲染成DOM节点

执行renderString生成render函数

上文解析完generate函数之后,回归到baseCompile方法中,实际此方法的返回即是generate函数的返回(包含astcode等属性的对象)。回归到调用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中,keytemplate模版字符串,valuerender函数,并且返回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._rctrue(在上面方法的结尾处赋值_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
    }
  }
)
复制代码

此对象中主要是设置了gethas的陷阱钩子函数(后续使用到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函数,参数仍然是fnoptions

// 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=truedeps=[])。然后将这个函数返回了。回归到上层的effect函数中,effect变量等于createReactiveEffect函数的返回值,就是reactiveEffect方法,然后判断options.lazy(为falseoptions中并无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对象进行结构,首先会触发_ctxhas钩子函数(因为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值不是以'_'开头的,并且不是特定的一些字符串,类似ObjectBoolean等(具体可以去看下isGloballyWhitelisted方法的内部实现),此时key值为message,所以hastrue

之后获取messgae的值,_ctx.message会触发get钩子函数,先判断属性名是否等于Symbol.unscopables,此时key值为message,所以执行PublicInstanceProxyHandlersget方法。一起看下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对象,本文前面提到了_ctxinstance.ctxProxy代理对象(关于instance.ctx对象上的_属性的值,在文章二”双向数据绑定”中提到了instance对象的创建,并新增_属性值为instance)。回归到get钩子函数中,判断属性名key(message)不是以'$'开头的,并且不存在于instanceaccessCache缓存对象中,再判断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)。因为messageRefImpl的实例对象,所以获取属性时会触发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=targetvalue=new Map()(空的Map对象),然后获取depsMap(Map对象)中key=message的属性值,因为depsMap是新建的空Map对象,所以也不存在message属性,固执行depsMap.set(key, (dep = new Set())),设置key=messagevalue=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赋值,因为typeSymbol('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 }patchFlag8

调用normalizeChildren处理children属性,此元素节点的children也是字符串,所以vnode.children='修改数据'vnode.shapeFlag = 1 | 8 = 9,最后将vnode对象存入currentBlock数组中并返回vnode对象

createBlock创建根vnode对象

8、回归到render函数中,当中括号的两个方法(createTextVNodecreateVNode)执行完成后,最后执行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对象]64true

createVNode方法中首先根据typeSymbol类型,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对象,typeSymbol('Fragment')children数组包含两个vnode对象,是typeSymbol('Text')的文本 和 type'button'的元素。至此render函数的解析已经完全结束了。

总结

本文主要是详细解析render函数的执行过程,首先解析动态数据message时触发get钩子函数,调用track方法进行依赖的收集(activeEffect变量收集到全局的targetMap对象中);然后调用createTextVNode方法构建文本vnode对象;调用createVNode方法构建button元素的vnode对象;最后调用createBlock方法构建根vnode对象。后续将详细解析patch方法利用生成的vnode对象构建出真正的DOM元素

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享