浅曦Vue源码-28-挂载阶段-$mount-genChildren&genStatic(17)

621 阅读4分钟

「这是我参与2022首次更文挑战的第29天,活动详情查看:2022首次更文挑战」。

一、前情回顾 & 背景

前两篇小作文在说 parse 生成 ast 后的下一个阶段 generate,这个阶段会将 ast 变成渲染函数代码字符串。这个过程的核心方法是 generate() 方法,而 generate 方法的核心又是 genElment 方法,genElement 方法的第一个步骤就是处理静态根节点即 genStatic 方法,提升静态渲染函数到 staticRenderFns 数组中;

genStatic() 方法执行时会标记 el.satcicProcesstrue,然后递归调用 genElement() 方法执行生成render函数的逻辑,生成 render 函数又需要先得到 ast 上携带的属性信息,这就要调用 genData 方法处理。

genData 是个中间步骤,genData 又会调用 genDirectives 来处理 el.directives,其中又会对 v-model 进行运行时辅助程序即运行时的事件处理函数的绑定。

由于调用栈过深,再进行后面的步骤之前,我们先来回顾一下调用栈:

generate -> genElement -> if(el.staticRoot && !el.saticProcessed) genStatic -> genElement else { data = genData() }

现在讲静态根的处理用的模板例子如下:

<div id="app">
+ <div class="staticR">
+  <article>hahahah</article>
+ </div>
 <span v-for="item in someArr" :key="index">{{item}}</span>
 <input :type="inputType" v-model="inputValue" />
 {{ msg }}
 <some-com :some-key="forProp"></some-com>
 <div>someComputed = {{someComputed}}</div>
 <div class="static-div">静态节点</div>
</div>

本篇我们会接着讲生成 genElementgenData() 之后的步骤:

export function genElement (el: ASTElement, state: CodegenState): string {
  if (...) {}
  else if (...) {
  } else {
    let code
    if (...) {
      // 
    } else {
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        // 上文所说的 genData 
        data = genData(el, state)
      }

      // 这后面就是得到 data 以后的操作了
      const children = el.inlineTemplate ? null : genChildren(el, state, true)

      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`

    return code
  }
}

二、genChildren

方法位置:src/compiler/codegen/index.js -> functions genChildren

方法参数:

  1. el,父元素的 ast 节点
  2. stateCodegenState 对象
  3. checkSkip/altGenElement/altGenNode 暂时忽略

方法作用:接收父元素,处理父元素的子节点数组 el.children,生成 el.children 中所有节点的渲染函数代码,形如:[_c(tag, data, children, normalizationType), _c(tag2, data2, children2...)....]。具体逻辑如下:

  1. 优化 el.children 只有一项且带有 v-for 并且不是 slot/template 标签的情况,不用再走后门的 map 直接走 genElement;
  2. 调用 el.children.map 将每一项都变成 render 函数,并且拼接成一个数组格式的字符串;
export function genChildren (
  el: ASTElement,
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  // 获取 el 的所有子节点
  const children = el.children
  if (children.length) {
   
    const el: any = children[0]

    // optimize single v-for
    if (children.length === 1 &&
      el.for &&
      el.tag !== 'template' &&
      el.tag !== 'slot'
    ) {
      // 优化,只有一个子节点 && 子节点上有 v-for 指令 && 子节点不是 template / slot 标签
      // 直接调用 genElement 生成该节点的渲染函数,不走下面的 map 再 genCode 的方式
      const normalizationType = checkSkip
        ? state.maybeComponent(el) ? `,1` : `,0`
        : ``
      return `${(altGenElement || genElement)(el, state)}${normalizationType}`
    }

    // 获取节点规范化类型,忽略它吧
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0

    // 函数,生成代码的一个函数
    const gen = altGenNode || genNode

    // 返回一个数组,数组的每一项都是一个渲染函数代码字符串
    return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}

2.1 genNode

方法位置:src/compiler/codegen/index.js -> function genNode

方法参数:

  1. nodeast 节点对象
  2. stateCodegenState 实例

方法作用:根据节点类型不同调用不同方法生成渲染函数代码;如果节点类型为 1 说明是元素节点,直接递归 genElement() 方法;

结合上面的 genChildren() 方法可以看出,如果一个子节点是元素,则直接递归重新调用 genElement,如果子节点还有子节点,则一直递归下去生成渲染函数;

function genNode (node: ASTNode, state: CodegenState): string {
  if (node.type === 1) {
    return genElement(node, state)
  } else if (node.type === 3 && node.isComment) {
    return genComment(node)
  } else {
    return genText(node)
  }
}

举个例子来说,div.demo 如下:

  <div class="demo">
    <article>hahahah</article>
    <article>hahahah</article>
  </div>

genChildren 的返回的子元素的渲染函数代码大致如下:

"[
  _c('article', '', _s('hahhahahahha')),
  _c('article', '', _s('hahhahahahha'))
]"

三、 genStatic 返回结果

3.1 梳理调用流程

generate 开始的的调用过程:generate() -> genElment() -> genStatic() -> genElement -> genChildren

现在我们现在要看 genStatic 的返回结果需要返回上一层调用栈—— genElement,调用代码位置如下面示例代码:

  1. genElement 调用 genStatic
export function genElement (): string {
  // genElement 调用  genStatic
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  }
}
  1. genStatic 调用 genElement
function genStatic () {
  // 这里这个 push 语句递归调用 genElement
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
  
  // 这个才是 genStatic 的返回值,这个就是重点了
  return `_m(${
    state.staticRenderFns.length - 1
  }${
    el.staticInFor ? ',true' : ''
  })`
}
  1. genElement 调用 genChildren
export function genElement (el: ASTElement, state: CodegenState): string {
  if (...) {
  } else {
    
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
   
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' 
      }${
        children ? `,${children}` : '' // children
      })`
    }
    
    return code
  }
}

3.2 逐层返回结果

  • 在我们的例子中 div.staticR 是个静态根,我们以此为例来看返回值:
<div class="staticR">
 <article>hahahah</article>
</div>

上一步 3.1 我们得知了调用栈,现在倒着退回去就可以拿到 genStatic 的返回值:

  1. hahahahrender 函数为:
"[_v(\"hahahah\")]"
  1. <article>hahahah</article>render 函数:
"[_c('article',[_v(\"hahahah\")])]"
  1. <div class="staticR"><article>hahahah</article></div>render 函数体:
 "_c('div', {staticClass:\"staticR\"}, [_c('article',[_v(\"hahahah\")])])"

这些 render 函数还不是真正的函数,因为尚在编译阶段这些还只是字符串,这些代码都是要在运行时调用的。但是可以看的出,_c(...) 是一个 _c 方法的调用,_c 也是一个运行时的渲染帮助函数,这些方法最终都会挂载到 Vue 实例上,通过 with(this) 让浏览器能够从 this(Vue) 实例上找到 _c 方法;

这里为啥没有 this ?好问题,如果你有这疑问,说明你已经读懂了;这是因为上面的返回值在 pushstaticRenderFns 之前被拼接的:

function genStatic () {
  // 这里这个 push 的 `with(this)` 就是了
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
  1. genStatic() 返回结果

genStatic 的返回值形如: "_m(index, true/'')"_m() 也是一个渲染帮助函数,同理上面的 _c 也是 Vue 提前准备好挂载到 Vue 实例 this 上的方法,说完这部分渲染我们会用专门的篇幅来讲解这些渲染帮助函数的作用;

_m 的第一个参数为啥是 index?因为在返回值之前,我们才 pushstaticRenderFns 中,所以数组最后一项就是当前元素对应的索引了。

function genStatic () {

  // 返回值形式: _m(index, true/'') 
  return `_m(${
    state.staticRenderFns.length - 1
  }${
    el.staticInFor ? ',true' : ''
  })`
}

四、总结

本篇小作文讨论了 genElement 的第一个情况——genStatic 获取渲染结果的过程,genStatic 主要做了以下工作:

  1. 标记当前 el.staticProcessedtrue,防止被重复处理;
  2. 递归调用 genElement,产生各个节点的渲染函数;
  3. genElement 最后的 else 就是处理普通元素的渲染的。首先调用 genData 获取元素上的 data, 然后调用 genChildren() 将子元素都处理成由 render 函数代码组成的数组项,每一项都渲染一个子元素;
  4. genStatic 获取 genElement 返回的结果,包裹 with(this) 语句,然后 pushstaticRenderFns
  5. genStatic 的最后用将当前静态根节点处理成 _m 方法的运行时调用_m(当前元素在 staticRenderFns 的索引, true 或 '')