浅曦Vue源码-18-挂载阶段-$mount(7)

325 阅读6分钟

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

一、前情回顾 & 背景

本篇小作文的主题是讨论 parseHTML 方法执行过程中解析到开始标签后调用 parseHTML 方法接收到的参数options.start 回调方法处理开始标签,其主要工作如下:

  1. 创建 AST 节点 element
  2. 调用 options.modules 中的 preTransformNode 方法处理 elementoptions.modules 来之 createCompiler 时传入的 baseOptions
  3. 处理 v-pre 指令以及在 pre 标签内的情景,接着处理 v-forv-forv-ifv-once
  4. 维护 root 节点,root 只在第一次处理时会被赋值,后面处理的所有节点都是 root 的子节点;
  5. 维护 currrentParent 变量,这个意义在于:调用 parseHTML 的时候是以一种一维的方式解析树形的模板字符串,但是建立 AST 时却需要还原模板描述的节点间的父子关系,也就是说 AST 是有深度的。
  6. 非自闭和元素时维护 element 入栈 stack,当解析到闭合标签时出栈,如果是自闭合标签执行 closeElement 自动闭合当前元素,因为它没有闭合标签了,闭合标签的逻辑就要在这儿调用

前面一篇说了很多 options.start 方法,但是也只是梗概,从本篇小作文开始将致力于 options.start 方法中的各个能力实现的细节方法,本篇的重点在于 createASTElementpreTransforms

二、createASTElement

方法位置:src/compiler/parser/index.js -> function createASTElement

方法参数:

  1. tag:标签名
  2. attrs:标签上的行内属性数组,是经过 handleStartTag 方法处理过的 attrs 形如:[{name: attrName, value: attrValue, start, end }]
  3. parent:当前元素的 parent,用于组织新建 AST 元素对象之间的关系

方法作用:

创建 type1AST 对象,包含 attrsList,attrsMapchildren 属性;这里要说的是 attrListattrMap 的区别,attrList 就是 attrs 参数,上面的参数中有示例,而 attrsMap 是以 { attrKeyName: attrVlaue } 的形式存储;

export function createASTElement (
  tag: string,
  attrs: Array<ASTAttr>,
  parent: ASTElement | void
): ASTElement {
  return {
    type: 1, // ast 节点类型
    tag, // 标签名
    attrsList: attrs, // 当前标签上的属性数组形如:[{name: attrName, value: attrValue, start, end }]
    attrsMap: makeAttrsMap(attrs), // 标签的属性对象 { attrName: attrVal, .... }
    rawAttrsMap: {}, // 原始属性对象
    parent, // 父节点
    children: [] // 以后该 ast 节点的孩子节点都要保存在这个数组中
  }
}

三、preTransforms

preTransform 不是个方法,它是个数组,是在 parse 方法中通过 pluckModuleFunction(options.modules, 'preTransformNode')options.modules 上摘取出来的方法数组;

export function parse (template, options) {
  // 从 options 中摘取方法
  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
  
  parseHTML(template, {
     start () {
       // for 循环调用 preTranforms 中的方法处理 element 这个新创建的 AST 节点
       for (let i = 0; i < preTransforms.length; i++) {
         element = preTransforms[i](element, options) || element
       }
    }
  })
}

3.1 options.modules

pluckModuleFunciton 接收的 options 参数是 parse 方法接收到的参数,而 parse 方法也是从 createCompiler 接收到 baseOptions:

const { compile, compileToFunctions } = createCompiler(baseOptions)
  • src/platform/web/compiler/options.js 导出的 baseOptions 如下:
import modules from './modules/index' // modules

export const baseOptions: CompilerOptions = {
  expectHTML: true,
  modules, // 处理 class、style、v-module
  directives, // 处理指令
  isPreTag, // 是否是 pre 标签
  isUnaryTag, // 是否自闭和标签
  mustUseProp, 
  canBeLeftOpenTag, 
  isReservedTag,
  getTagNamespace,
  staticKeys: genStaticKeys(modules)
}
  • ./modules/index 导出的 module 如下:
import klass from './class'
import style from './style'
import model from './model'

export default [
  klass,
  style,
  model
]
  • klass、style、model 三个模块导出情况如下,只有 model 导出了 preTransformNode 方法: src/platforms/web/compiler/modules/style.js
export default {
  staticKeys: ['staticStyle'],
  transformNode,
  genData
}

src/platforms/web/compiler/modules/class.js


export default {
  staticKeys: ['staticClass'],
  transformNode,
  genData
}

src/platforms/web/compiler/modules/model.js

export default {
  preTransformNode // 这个就是我们要说的 preTransformNode 方法
}

3.2 preTransformNode

前面我们分析 options.modules,发现只有 model.js 导出了一个 preTransformNode 方法;

方法位置:src/platforms/web/compiler/modules/model.js

方法参数:

  1. ASTAST 节点
  2. optionscompilerOptions 就是 baseOptions

方法作用:

以这个模板为例:

<input :type="inputType" 
       v-model="someInputValueInType" 
       v-if="someIfCondition === 10"
 />

处理存在 v-modelinput 标签,这个过程不处理 v-model 的双向数据绑定,而是处理 inputtypecheckboxradio和其他情况的。具体步骤如下:

  1. 判断 el.taginput 才处理,同时 el.attrsMap 如果没有 v-model,就直接退出;
  2. 获取动态绑定的 type 对应的绑定变量,赋值给变量 typeBinding,比如上面模板上的 inputType
  3. 如果动态绑定 type 不为空,则进一步处理 input 上的 v-if、v-else-if、v-else 指令动态绑定的表达式;
  4. v-if 表达式变为解析所得的 ifConditon,变为 ifExtraCondition&& ifCondition,这个有啥用呢?后面将 typecheckboxradiov-if 表达式变为 type === 'checkbox' + ifExtraConditiontype === 'checkbox' && ifCondition
  5. 获取 elelast 对象)上的 v-else-if 绑定的表达式,赋值到 elseIfCondition,解析 el 上有是否有 v-else 赋值到到 hasElse 变量
  6. 克隆 el 得到 branch0,处理克隆出来的 ast 的信息并为 branch0 添加 v-if 对应的条件,接着克隆 el 得到 branch1branch1if 条件为 type === checkbox && branch0.if,同理克隆 branch2,为branch2 增加 if 条件为 type === radio && branch.if;最后处理 type 不为 checkboxradio 的其他情况作为 branch3branch3if 就是 branch0.if
  7. 返回克隆的 branch0,而非 preTransfromNode 接收到的 el 代表的原始 ast
function preTransformNode (el: ASTElement, options: CompilerOptions) {
  if (el.tag === 'input') {
    const map = el.attrsMap
    // 不存在 v-model 属性则直接 return
    if (!map['v-model']) {
      return
    }


    // 获取 :type 的值
    let typeBinding
    if (map[':type'] || map['v-bind:type']) {
      typeBinding = getBindingAttr(el, 'type')
    }
    if (!map.type && !typeBinding && map['v-bind']) {
      typeBinding = `(${map['v-bind']}).type`
    }

    // 如果存在动态绑定的 type 属性,如上面的 :type="inputType" ,typeBingding 就是 inputType
    // inputType 是一个变量,代指一个具体的 input 的 type 值,比如 checkbox、radio、color、text
    if (typeBinding) {
      // 获取 v-if 的值,比如 <input :type="inputType" v-model="someInputValueInType" v-if="someIfCondition === 10" />
      // ifCondition 为 someIfCondition === 10
      const ifCondition = getAndRemoveAttr(el, 'v-if', true)

      // && someIfCondition === 10
      const ifConditionExtra = ifCondition ? `&&(${ifCondition})` : ``

      // 是否存在 v-else 属性,<input v-else />
      const hasElse = getAndRemoveAttr(el, 'v-else', true) != null

      // 获取 v-else-if 属性的值 <input v-else-if="inputElseIfValue" />
      const elseIfCondition = getAndRemoveAttr(el, 'v-else-if', true)

      // 克隆一个新的 el 对象,分别处理 input type 为 checkbox、radio 情形,剩下都是其他类型了
      // 具体是哪种情况,通过 el.ifConditions 条件来判断
      // 1. checkbox
      const branch0 = cloneASTElement(el)
      
      // 处理 input 上带 v-for 的情况 <input v-for="item in arr" :key="index" />
      // 处理 v-for 表达式,得到 branch0.for = 被迭代对象如上面的 arr,
      // 得到 branch0.alias=迭代条目的名字,如上的 item
      processFor(branch0)

      // 在 branch0.attrsMap 和 attrList 对象中添加属性 type,值为 checkbox
      addRawAttr(branch0, 'type', 'checkbox')

      // 分别处理元素节点的 key、ref、插槽、自闭合的 slot 标签、动态组件、class、style、v-bind、v-on、其他指令和一些原生属性
      processElement(branch0, options)

      // 标记当前对象已经被处理过了,防止被重复处理
      branch0.processed = true 
      
      // branch0 这个克隆出来的 ast 的 v-if 条件表达式变为:
      // type 绑定变量 === 'checkbox' && el 的 v-if表达式
      // inputType === 'checkbox' && someIfCondition === 10
      branch0.if = `(${typeBinding})==='checkbox'` + ifConditionExtra

      // 在 branch0.ifConditions 中放入 { exp, block } 对象
      addIfCondition(branch0, {
        exp: branch0.if,
        block: branch0 // 这个 block 就是将来 exp 代表的条件成立时渲染出来的元素
      })

      // 再克隆一个新的 ast 对象
      // 2. 给新克隆所得的 branch1 新的 ast 增加 type 为 radio 的 else-if 条件
      const branch1 = cloneASTElement(el)

      // 获取新克隆的 branch1 上的 v-for 指令对应的值
      getAndRemoveAttr(branch1, 'v-for', true)

      // 在 branch1.attrsMap 和 branch1.attrList 对象中添加 type 属性,值为 radio
      addRawAttr(branch1, 'type', 'radio')

      // 分别处理 key、ref、插槽、自闭合 slot 标签、动态组件、class、style、v-bind、v-on、其他指令、原生属性
      processElement(branch1, options)

      // 在 branch1.ifConditions 中放入 { exp, block } 对象
      // 你会发现 addIfCondition 是个谁添加?是 branch0,而 branch0 就是克隆的 el
      addIfCondition(branch0, {
        exp: `(${typeBinding})==='radio'` + ifConditionExtra, // 判断是否为 radio
        block: branch1 // 当满足 exp 所代表的条件成立时渲染 block 对应的 branch1 这个 ast
      })

      // 3. other,input type 为除 checkbox或radio 外的其他值,如 text
      const branch2 = cloneASTElement(el)
      getAndRemoveAttr(branch2, 'v-for', true)
      addRawAttr(branch2, ':type', typeBinding)
      processElement(branch2, options)
      
      // 这里同样是给 branch0 进行 addIfCondition 操作
      addIfCondition(branch0, {
        exp: ifCondition,
        block: branch2
      })


      // 弄了半天,branch1/2 有个啥用???
      // 其目的在于给 branch0 通过 addIfConditon 设置条件,使满足不同条件时渲染对应 type 的 input 标签
      if (hasElse) {
        branch0.else = true
      } else if (elseIfCondition) {
        branch0.elseif = elseIfCondition
      }

      // 最后返回的不是el,而是克隆出来的 branch0
      return branch0
    }
  }
}

3.3 preTransfromsNode 为了啥?

起初我也并没看明白,直到第二次看的时候我才完全看懂。它这么做是为了解决一个问题,就是 input 动态绑定 type 时的渲染问题。

例如咱们的例子 <input :type="inputType" v-if="someCondition" /> ,此时尚在编译,并不能准确获知 inputType 所表示的真实类型,inputType 有可能是 checkbox/radio/button/color/calendar... 中的任一个,为了解决这个问题,Vue 就把这一个模板变成下面的一系列模板:

 <input v-if="someCondition" />
 <input v-else-if="inputType === 'checkbox' && someCondition" />
 <input v-else-if="inputType === 'radio' && someCondition" />
 <input v-else />

这样无论你的 type 绑定的是个什么值,我相当于预判了你的所有预判,简直了。。。

四、总结

本文详细讨论了创建 AST 的方法 createASTElement 方法,以及来自 options.modulespreTransforms 变量所代表的 preTransfromNode 方法;

  1. createASTElement 方法创建 type1,即元素的 AST 节点对象,包含 parentchildren 等用于组织节点间关系的属性;
  2. preTransfromsNode 方法就是预处理带有 v-model 且动态绑定了 type 属性的 input 标签,目的是解决无论 type 绑定何种值,最后都能渲染除一个符合预期的 input 元素。