浅曦Vue源码-25-挂载阶段-$mount(14)

570 阅读3分钟

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

一、前情回顾 & 背景

上文详细讲解了对 parse 得到的 ast 进行静态标记的过程,这个过程的意义在于被标记成静态的 ast 节点,在数据发生更新是不会被重新渲染;其核心实现主要有在 optimize 方法中:

  1. 调用 genStaticKeysCached 获取 isStaticKeys 方法备用;
  2. 调用 markStatic 方法递归处理 ast 节点及其子节点和条件渲染节点,为每个节点设置 static 属性,值为 isStatic() 方法返回值,isStatic 方法则根据 ast 节点对象上的信息判断是否为静态;
  3. 调用 markStaticRoot() 判断节点是否为静态根,静态根节点在数据更新时会被忽略,也不会被 patch

本篇小作文聚焦于:用前面的 ast 生成渲染函数代码主体的 generate 方法,如下图,code 是一个对象,render 就是经过 generate 方法转换 ast 得来的代码;

image.png

上图中的 render 就是所谓 render 函数主体,接下来的篇幅我们将详细讨论如何得到这个结果的:

// 这个 code 就是上面 generate 方法返回的结果
code = {
    render: "with(this){return _c('div',{attrs:{\"id\":\"app\"}},[_v(\"\\n\\t\"+_s(msg)+\"\\n\\t\"),_c('some-com',{attrs:{\"some-key\":forProp}}),_v(\" \"),_c('div',[_v(\"someComputed = \"+_s(someComputed))]),_v(\" \"),_c('div',{staticClass:\"static-div\"},[_v(\"静态节点\")])],1)}"

    staticRenderFns: [],    
    <prototype>: {…}
}

二、generate

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

方法参数:

  1. ast,预期转成 render 函数的 ast 节点对象;
  2. compilerOptions:编译器选项对象

方法作用:

  1. 通过 CodegenState 类结合传入的 options 生成 state 对象;
  2. 然后调用 genElement(ast, state) 得到 render 函数主体;
  3. 最后返回一个对象,对象包含 renderstaticRenderFns 属性,render 就是经过 with(this) 语句包裹的 render 函数主体;
export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  // 创建 CodeGenState 实例,
  // CodegenState 初始化了如 staticRenderFns 等属性
  const state = new CodegenState(options)

  // 生成字符串格式的代码,比如 '_c(tag, data, children, normalizationType)'
  // fix #11483, Root level <script> tags should not be rendered.
  const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

2.1 CodegenState 类

类的位置:src/compiler/codegen/index.js -> class CodegenState

构造函数参数:options,就是 createCompiler(baseOptions) 方法接收到的 baseOptions

类的作用:

  1. 缓存 baseOptionsthis.options;
  2. options.modeules 中提取 transformCode, genData 方法,这两个方法和我们前面讲 parse 时用到的 preTransformNodepostTransformNode 以及 transformNode 同宗同源;options.modules 包含三个模块:klass/style/model,其中 klass/style 导出了 genData 方法,所以 this.genDataFns = [klass导出 genData 方法, style 导出的 genData 方法];
  3. 实现 Vue 中的基础指令处理方法的复用和 options.directives 处理方法的扩展,最终所有指令将会扩展到一个新的对象,并挂载到 this.directives
    • 3.1 处理基础指令 v-on、v-bind 的方法
    • 3.2 options.directives 处理指令 v-model、v-text、v-html 的方法
  4. 初始化判断 ast 节点是否为组件的方法 this.maybeComponent,其判断原理是 el.component 属性为 true,或者不是平台保留标签;
  5. 初始化 staticRenderFnspre 等属性
export class CodegenState {
  options: CompilerOptions;
  warn: Function;
  transforms: Array<TransformFunction>;
  dataGenFns: Array<DataGenFunction>;
  directives: { [key: string]: DirectiveFunction };
  maybeComponent: (el: ASTElement) => boolean;
  onceId: number;
  staticRenderFns: Array<string>;
  pre: boolean;

  constructor (options: CompilerOptions) {
    this.options = options
    this.warn = options.warn || baseWarn
    // 从  options.modules 提取 transformCode 方法,不过 web 平台下 klass、style、model 没有导出这个方法
    this.transforms = pluckModuleFunction(options.modules, 'transformCode')
    
    // style/klass 导出了 genData 方法
    this.dataGenFns = pluckModuleFunction(options.modules, 'genData') 
    this.directives = extend(extend({}, baseDirectives), options.directives)
    const isReservedTag = options.isReservedTag || no
    this.maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)
    this.onceId = 0
    this.staticRenderFns = []
    this.pre = false
  }
}

2.2 genElement

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

方法参数:

  1. ast,准备生成渲染函数的 ast 节点对象;
  2. stateCodegenState 实例

方法作用:根据不同情况调用不同处理函数,最后得到渲染工具函数调用的字符串实现对应的指令或者组件的功能,例如 v-for 会变成 _l() 方法调用,如题处理如下:

  1. 如果 el 不是根节点,处理 el.pre 属性,判断 el 是否有 v-pre 至指令或者处于有 v-pre 指令的元素包裹;
  2. el.staticRoot 属性为 true 调用 genStatic() 方法处理静态根节点,将静态根节点的渲染函数保存到 staticRenderFns 属性中;
  3. el.once 属性为 true 时处理 v-once 指令,调用 genOnce() 方法处理 v-once 指令;
  4. el.for 属性值存在说明 ast 上存在 v-for 指令,调用 genFor() 方法处理 v-for 指令;
  5. el.if 属性值存在,调用 genIf() 处理 v-if
  6. 如果当前节点标签是 template 且不是 slot 或者 v-pre,则调用 genChildren 处理子节点;
  7. 如果是 slot 插槽,则调用 genSlot() 处理 slot 插槽;
  8. 如果是普通元素、组件、者动态组件 则调用 genComponent() 处理直接得到 code;否则看是不是组件(!el.plain)或者有没有 v-pre 并且是组件,则调用 genData() 处理节点上所有的属性,得到 data 对象;然后在处理其子节点;
  9. 经历前面的操作后得到 code 代码字符串,然后调用 transforms 即前面从 options.modules 提取出来挂载到 CodegenState 实例上的
export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }

  if (el.staticRoot && !el.staticProcessed) {
    // 处理静态根节点,生成静态根节点的渲染函数,结果保存到 staticRenderFns 数组中
    // genStatic 返回一个类似 _m(idx, true) 的字符串 
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    // 处理带有 v-once 指令的节点,结果会有这三种:
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    // 处理节点上的 v-for 指令,
    // 得到 `_l(exp, function (alias, iterator1, iterator2) { return _c(tag, data, children) })`
    // _l 是个工具方法,是渲染一个列表处理
   
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    
    // 处理带有 v-if 指令的节点,得到三元表达式:condition ? render1 : render2
    // condition 就是条件,成立则返回渲染函数1,否则2
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    // 当前节点不是 template 标签 && 不是插槽 && 不带有 v-pre 指令
    // 处理所有子节点的渲染函数,返回一个数组,每个数组就是一个子节点,
    // 格式如:[_c(tag, data, children, normalizationType), ...]
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    // 生成插槽的渲染函数,_t 是处理 slot 的工具方法,结果形如
    // _t(slotName, children, attrs, bind)
   
    return genSlot(el, state)
  } else {
    // 处理动态组件、普通HTML元素(自定义组件,原生标签)
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        // 非普通元素或者带有 v-pre 指令的组件,
        // 处理节点的所有属性,返回一个 JSON 字符串,形如:
        // 比如:'{ key: xx, ref: xx, ....}'
        data = genData(el, state)
      }

      // 处理子节点,得到所有的子节点字符串格式的代码组成的数组,形如:
      // `['_c(tag, data, children)', ....], normalizationType`
     
      const children = el.inlineTemplate ? null : genChildren(el, state, true)

      // 得到最终的字符串格式代码,形如:
      // '_c(tag, data, children, normalizationType)'
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }

    // 调用通过 pluckModuleFunction 从 options.modules 提取的 transformCode 方法,处理 code
    // options.mdoules 中的 klass/style/model 没有导出 transformCode 方法
    // 所以这里这个循环不会执行,code 还是前面的 code
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

提示:上面代码中用到的 _c、_l、_t 都是渲染帮助函数的别名,_c 是创建元素,_l 是渲染列表即 v-for 的实现,_t 则是处理 slot 的。

四、总结

本篇小作文开始讲述挂载阶段的另一个十分重要的主题——生成渲染函数(render函数)代码主体。

前面的 parsehtml 模板转成 astast 包含了包裹指令例如v-if条件渲染v-for 列表渲染等全部信息。接着 generate 就是借用渲染函数的帮助函数实现这些指令的过程。主要分为两个大的步骤:

  1. 实例化 CodegenState 对象,准备一些属性和方法给后面的创建 render 函数主体备用;
  2. 调用 genElement 分情况处理 ast 语法,将 ast 变成对应的帮助函数调用;

说道这里,相信大家大家已经有点感觉了,generate 的作用就是把 Vue 的模板语法变成真正的 HTML 代码的中间步骤,这一步还不是 HTML 而是 js 代码,这些 js 代码执行后就会得到真正的 HTML