浅曦Vue源码-26-挂载阶段-$mount(15)

890 阅读5分钟

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

一、前情回顾 & 背景

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

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

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

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

本篇小作文将详述 genElment 方法处理具体情况时使用的具体方法,比如 genFor、genStatic 等,在这个过程中我们会一步一步看到渲染函数主体是如何产生的。

二、genStatic

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

方法参数:

  1. el: ast 节点对象
  2. state: CodegenState 对象,这里用到了 state.staticRenderFns 用于提升静态子树

方法作用:

  1. 标记当前 ast 节点对象 el.staticProcessed = true,防止被重复处理,前面 genElement 调用 genStatic 前判断了 el.staticProcessed不为 rue才调用;
  2. 将当前 l这个静态根生成的渲染函数保存到 tate.staticRenderFns注意这里还会再次调用 {genElement(el, state)}
  3. 最后返回_m 方法的调用的字符串形式的代码,形如: _m(静态根render函数索引, true 或者 '') ,第一个参数是面生成的静态根的渲染函数代码在 state.staticRenderFns 的索引位置,第二个参数 el.staticInFor 标识结果;
function genStatic (el: ASTElement, state: CodegenState): string {
  // 标记当前静态节点已经被处理过了
  el.staticProcessed = true
  
  const originalPreState = state.pre
  if (el.pre) {
    state.pre = el.pre
  }

  // 将静态根节点的渲染函数 push 到 staticRendersFns 数组中,比如:
  // [`with(this){return _c('div',{staticClass:\"staticR\"},[_c('article',[_v(\"hahahah\")])]) }]
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
  state.pre = originalPreState

  // 返回一个可执行函数 _m(idx, true or '')
  // idx = 当前静态节点的渲染函数在 staticRenderFns 数组中的下标
  return `_m(${
    state.staticRenderFns.length - 1
  }${
    el.staticInFor ? ',true' : ''
  })`
}

我们在 test.html 中加入一段静态 HTML 模板内容如下:

<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>

显而易见的 div.staticR 就是个静态根,我们以此为例看下最后得到的 staticRenderFns 的真容。

image.png

在 generate() 方法中第一次调用 genStatic 的目的其实只是提升而已,把静态根节点 push 到staticRenderFns 中,在 genStatic 中再次调用 genElement 才是真正的生成静态的 render 函数的步骤,此时 el.staticProcessed 已经为 true,所以 genElement() 中的 if (el.staticRoot && !el.staticProcessed) { genStatic() } ,就不会再执行了,而是向下执行,

最终上图中的 code 就是 genStatic 调用 genElement 得到的 code:

"_c('div',{staticClass:\"staticR\"},[_c('article',[_v(\"hahahah\")])])"

所以,state.staticRenderFns.push("with(this){return ${genElement(el, state)}}") 最终得到的结果是:

state.staticRenderFns = [`with(this){return _c('div',{staticClass:\"staticR\"},[_c('article',[_v(\"hahahah\")])]) }`]

三、genData

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

方法参数:

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

方法作用:处理 ast 节点上的属性,最终拼接生成一个 JSON 字符串,这些信息描述了渲染生成的 DOM 的具体行为;这些属性在 HTML 模板中是写在标签行内的属性,这些属性的信息被解析处理后放在了 ast 节点上,例如写 HTML 标签上的 v-for,被处理后就是 el.for = 可迭代对象el.alias = 可迭代对象条目名称。具体工作如下:

  1. 优先处理指令,因为指令运行时有可能会更改 el 上的某些属性;
  2. 收集 el 上属性: key,ref,refInfor,precomponent 放到 data 中;
  3. 调用 state.genDataFnsgenDataFnsbaseOptions.modulesklass/style/model 三个模块中导出的方法;
  4. 处理 el.attrs,props,events,slot,scopedSlot
  5. 处理 v-model,将 v-model 拆分成多个属性:modle,callback,expression
  6. 处理 inlineTemplate 内联模板,dynamicAttrs、wrapData、warapListeners
export function genData (el: ASTElement, state: CodegenState): string {
  // 节点属性组成的 JSON 字符串
  let data = '{'

  // 首先处理指令,指令可能在生成其他属性之前改变这些属性
  // 执行处理指令的方法,比如 web 平台的 v-text、v-html、v-model 等,
  // v-html 会向 el 上增加 el.innerHTML = _s(value, dir)
  // 如果指令的完整能力有一部分是依赖运行时的,比如 v-model 指令,
  // 则返回 directives: [{ name, rawName, value, arg, modifiers }, ....]
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','

  // key data= { key: xx }
  if (el.key) {
    data += `key:${el.key},`
  }
  //
  // ref, data= { ref: xx }
  if (el.ref) {
    data += `ref:${el.ref},`
  }

  // 带有 ref 属性的节点在带有 v-for 指令的节点内部,data = { refInFor: true }
  if (el.refInFor) {
    data += `refInFor:true,`
  }

  // pre, v-pre 指令,data = { pre: true }
  // pre
  if (el.pre) {
    data += `pre:true,`
  }

  // 动态组件,data= { tag: 'component' }
  // record original tag name for components using "is" attribute
  if (el.component) {
    data += `tag:"${el.tag}",`
  }

  // 为节点执行模块(class, 
  for (let i = 0; i < state.dataGenFns.length; i++) {
    data += state.dataGenFns[i](el)
  }
  // attributes
  if (el.attrs) {
    data += `attrs:${genProps(el.attrs)},`
  }
  // DOM props
  if (el.props) {
    data += `domProps:${genProps(el.props)},`
  }
  // event handlers
  if (el.events) {
    data += `${genHandlers(el.events, false)},`
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true)},`
  }
  // slot target
  // only for non-scoped slots
  if (el.slotTarget && !el.slotScope) {
    data += `slot:${el.slotTarget},`
  }
  // scoped slots
  if (el.scopedSlots) {
    data += `${genScopedSlots(el, el.scopedSlots, state)},`
  }
  // component v-model
  if (el.model) {
    data += `model:{value:${
      el.model.value
    },callback:${
      el.model.callback
    },expression:${
      el.model.expression
    }},`
  }
  // inline-template
  if (el.inlineTemplate) {
    const inlineTemplate = genInlineTemplate(el, state)
    if (inlineTemplate) {
      data += `${inlineTemplate},`
    }
  }
  data = data.replace(/,$/, '') + '}'

  if (el.dynamicAttrs) {
    data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
  }
  // v-bind data wrap
  if (el.wrapData) {
    data = el.wrapData(data)
  }
  // v-on data wrap
  if (el.wrapListeners) {
    data = el.wrapListeners(data)
  }
  return data
}

在我们的例子中只有一个静态的类名 staticR,这个静态类名会通过调用 state.dataGenFns 数组的 genData 方法处理,即class.js 模块中导出的处理类名的 genData 方法处理返回 "staticClass: 'staticR'",所以在我们的这个静态根 div.staticRdata 只有一个属性:{ staticClass: \"staticR\"}

3.1 class.js 中 的 genData

方法位置:src/platforms/web/compiler/modules/class.js -> function genData

方法参数:elast 节点对象

方法作用:提取 ast 节点对象上的动态和静态类名,这个方法会就是前面的 state.dataGenFns 数组中的一项。所谓静态类名就是实实在在的确定的类名,而动态类名则是通过 v-bind:class/:class 语法绑定的 Vue 实例上的变量,比如 props/data/computed 中的属性;

function genData (el: ASTElement): string {
  let data = ''
  if (el.staticClass) {
    // 静态类名 <div class="staticR" ></div> 中 staticR 就是静态类名
    data += `staticClass:${el.staticClass},`
  }
  if (el.classBinding) {
    // 动态类名 <div :class="someDynamicClass"></div> 这里的 someDynamicClass 就是动态类名
    data += `class:${el.classBinding},`
  }
  return data
}

export default {
  staticKeys: ['staticClass'],
  transformNode,
  genData // state.dataGenFns 中的方法来自这里
}

image.png

3.2 genDirectives

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

方法参数

  1. elast 节点对象;
  2. stateCodegenState 实例对象

方法作用:调用 genDirectives() 方法进行指令的编译,这部分内容也很经典,这里详细介绍了 Vue 是如何处理各式各样的双向数据绑定的,我们在下一篇用专门的篇幅讨论它。

四、总结

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

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

genData 是个中间步骤,因为调用栈太深了,这里先简单回顾一下调用栈:

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

下一篇我们会进一步讨论 genDirectives 的细节问题。