vue编译过程分析(下)

简介: vue编译过程分析

为AST生成render函数


以上我们了解到HTML模板经过解析处理,最终会变成一个AST节点树。随后编译器执行generate函数,为AST生成render函数的代码体。


function generate (
    ast,
    options
  ) {
    var state = new CodegenState(options);
    // 根据AST节点生成代码
    var code = ast ? genElement(ast, state) : '_c("div")';
    return {
      render: ("with(this){return " + code + "}"),
      staticRenderFns: state.staticRenderFns
    }
  }


render函数的主要作用是创建虚拟节点vnode。而创建一个vnode,需要用到三个参数:元素标签,数据对象和子元素列表。genElement作为核心的代码生成方法,会按照顺序去生成这三部分的代码。


/**
 * AST元素的代码生成函数
 * @param {ASTElement} el AST对象
 * @param {CodegenState} state 代码生成状态
 */
function genElement (el: ASTElement, state: CodegenState): string {
  // ... 省略其他代码
  let data
      // 1. 首先生成节点本身的data代码,例如<div ref="myref" id="app"></div> 生成的data数据代码为"{ref:"myref",attrs:{"id":"app"}}"
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el, state)
      }
      // 2. 其次生成子元素创建代码
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      // 3. 拼装成一个元素节点创建方法的字符串 形式如下:_c(tag,data,children)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
  // ... 省略其他代码


data数据对象代码


仍以第一个HTML模板进行举例:


<div id="app">{{msg}}</div>


经解析后的AST对象如下:


{
    attrsList: [{name: "id", value: "app", start: 5, end: 13}],
    attrsMap: {id: "app"},
    children: [{type: 2, expression: "_s(msg)", tokens: [{@binding: "msg"}], text: "{{msg}}"}],
    end: 14,
    parent: undefined,
    plain: false,
    rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}},
    start: 0,
    tag: "div",
    type: 1,
    static: false,
    staticRoot: false
}


下面一段代码是data数据对象代码的生成逻辑:


function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'
  // 先为指令生成代码,因为指令可能会修改元素的其他属性
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','
  // key
  if (el.key) {
    data += `key:${el.key},`
  }
  // ref 给定ref='myref',则生成 'ref: "myref"'
  if (el.ref) {
    data += `ref:${el.ref},`
  }
  // refInFor
  if (el.refInFor) {
    data += `refInFor:true,`
  }
  // pre
  if (el.pre) {
    data += `pre:true,`
  }
  // record original tag name for components using "is" attribute
  if (el.component) {
    data += `tag:"${el.tag}",`
  }
  // 主要是class和style代码的生成 staticClass,staticStyle,classBinding,styleBinding
  for (let i = 0; i < state.dataGenFns.length; i++) {
    data += state.dataGenFns[i](el)
  }
  // attributes元素attribute属性的生成,如给定el.attrs=[{name: 'id', value:'app', dynamic: undefined, start:0,end:5}],则返回'attrs:{"id":"app"}'
  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
}


上述代码中可以看到,vue针对不同的props、attrs、events、directives等分别生成各自的代码,在本例中的AST节点对象只存在attrs属性,因此其处理过程只会执行下面这一个语句:


if (el.attrs) {
    data += `attrs:${genProps(el.attrs)},`
  }


genProps函数如下:


function genProps (props: Array<ASTAttr>): string {
  let staticProps = ``
  let dynamicProps = ``
  for (let i = 0; i < props.length; i++) {
    const prop = props[i]
    const value = __WEEX__
      ? generateValue(prop.value)
      : transformSpecialNewlines(prop.value)
    if (prop.dynamic) {
      dynamicProps += `${prop.name},${value},`
    } else {
      staticProps += `"${prop.name}":${value},`
    }
  }
  staticProps = `{${staticProps.slice(0, -1)}}`
  if (dynamicProps) {
    return `_d(${staticProps},[${dynamicProps.slice(0, -1)}])`
  } else {
    return staticProps
  }
}


可以看到genProps 会将 el.attrs,也就是[{name: "id", value: "app", start: 5, end: 13}] 处理成如下字符串:


"{attrs:{"id":"app"}}"


生成children代码


子元素的创建代码生成函数如下:


function genChildren (
  el: ASTElement,
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  const children = el.children
  // 无子元素不处理
  if (children.length) {
    const el: any = children[0]
    // 如果子元素使用了v-for指令
    if (children.length === 1 &&
      el.for &&
      el.tag !== 'template' &&
      el.tag !== 'slot'
    ) {
      const normalizationType = checkSkip
        ? state.maybeComponent(el) ? `,1` : `,0`
        : ``
      return `${(altGenElement || genElement)(el, state)}${normalizationType}`
    }
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    // 遍历children数组,为每个子元素生成代码
    const gen = altGenNode || genNode
    return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}
/**
 * 生成创建node节点的代码,对各种node类型进行了封装
 * @param {*} node 
 * @param {*} state 
 */
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)
  }
}
/**
 * 生成创建文本节点字符串,返回内容形如:"_v("see me")"
 * @param {*} text 文本类型的AST节点
 */
function genText (text: ASTText | ASTExpression): string {
  return `_v(${text.type === 2
    ? text.expression // no need for () because already wrapped in _s()
    : transformSpecialNewlines(JSON.stringify(text.text))
  })`
}


对于本例而言,div的子元素是一个文本节点,执行genText会生成如下代码:


"[_v(_s(message))]"


render函数的完整代码


最后在genElement函数中,将各部分代码拼接到一起,组成一段完整的代码:


'with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(message))])}'


以上是render函数基本的创建步骤,接下来我们再看下针对于v-if、v-model和v-for(v-on在v-model的处理中也有涉及,不单独举例了)几个常见的指令,生成的代码有什么不同。


v-if的处理


再来看一下genElement函数中关于v-if的处理逻辑:


function genElement (el: ASTElement, state: CodegenState): string {
  //... 省略其他代码
  // 节点存在v-if指令执行genIf
  if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } 
}


可以发现如果模板中有HTML标签使用了v-if,编译器会调用genIf,其主要代码如下:


function genIf (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  el.ifProcessed = true 
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
function genIfConditions (
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  if (!conditions.length) {
    return altEmpty || '_e()'
  }
  // 将ifConditions数组中的条件语句转成三元运算表达式
  const condition = conditions.shift()
  // 生成三元表达式形如a?1:2
  if (condition.exp) {
    return `(${condition.exp})?${
      genTernaryExp(condition.block)
    }:${
      genIfConditions(conditions, state, altGen, altEmpty)
    }`
  } else {
    return `${genTernaryExp(condition.block)}`
  }
  // v-if 使用了 v-once  (a)?_m(0):_m(1)
  function genTernaryExp (el) {
    return altGen
      ? altGen(el, state)
      : el.once
        ? genOnce(el, state)
        : genElement(el, state)
  }
}


执行完genIf,会创建一个三元运算表达式。如果以如下模板为例:


div id="app"><p v-if="seen">you can see me</p><p v-else>you can not see me</p></div>
复制代码

那么使用v-if和v-else的p标签所生成的对应代码如下:

"(seen)?_c('p',[_v("you can see me")]):_c('p',[_v("you can not see me")])"


从生成的代码中,我们就能理解v-show和v-if的区别了。当使用v-if来控制dom元素的隐藏和显示的时候,每次都需要移除和重新创建的。


v-model的处理


如果我们在如下模板使用了v-model:


<div id="app"><input v-model="message" placeholder="edit me"></div>


input标签所对应的AST节点核心信息如下:


{
    attrs: [{name: "placeholder", value: ""edit me"", dynamic: undefined, start: 39, end: 60}]
    attrsList: [{name: "v-model", value: "message", start: 21, end: 38}, {name: "placeholder", value: "edit me", start: 39, end: 60}],
    attrsMap: {v-model: "message",placeholder: "edit me"},
    children: [],
    directives:[{name: "model", rawName: "v-model", value: "message", arg: null, isDynamicArg: false,modifiers: undefined,start:21,end:38}],
    events:{input: {value: "if($event.target.composing)return;message=$event.target.value", dynamic: undefined}},
    props:[{name: "value", value: "(message)", dynamic: undefined}],
    parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
    rawAttrsMap: {placeholder: {name: "placeholder", value: "edit me", start: 39, end: 60},v-model: {name: "v-model", value: "message", start: 21, end: 38}},
    hasBindings: true,
    tag: "input",
    type: 1
}


input没有子元素,因此只需要生成data数据对象。生成的代码如下:


"{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"placeholder":"edit me"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}}"


从代码中可以看到,data数据对象中,新增了在domProps和on部分代码,其中指定了value和input事件处理函数。因此也就理解了v-model在本质上是把v-bind与v-on:input封装之后的语法糖。


经拼接后,完整的input生成的代码如下:


"_c('input',{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"placeholder":"edit me"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}})"


v-for的处理


如果如下模板中使用了v-for:


<div id="app"><p v-for="(item, index) in items" :key="index">{{item}}</p></div>


p标签对应的AST核心信息如下:


{
    alias: "item",
    for: "items",
    iterator1: "index",
    forProcessed: true,
    key: "index",
    attrsList: [],
    attrsMap: {v-for: "(item, index) in items", :key: "index"},
    children: [{type: 2, expression: "_s(item)", tokens: Array(1), text: "{{item}}", start: 61, …}],
    parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …},
    rawAttrsMap: {:key: {name: ":key", value: "index", start: 48, end: 60},v-for: {name: "v-for", value: "(item, index) in items", start: 17, end: 47}},
    tag: "p",
    type: 1
}


调用genFor函数:


function genFor (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altHelper?: string
): string {
  const exp = el.for
  const alias = el.alias
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
  // 非生产环境下,如果未指定:key,控制台会提示用户
  if (process.env.NODE_ENV !== 'production' &&
    state.maybeComponent(el) &&
    el.tag !== 'slot' &&
    el.tag !== 'template' &&
    !el.key
  ) {
    state.warn(
      `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
      `v-for should have explicit keys. ` +
      `See https://vuejs.org/guide/list.html#key for more info.`,
      el.rawAttrsMap['v-for'],
      true /* tip */
    )
  }
  el.forProcessed = true 
  // 拼接完整代码
  return `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
      `return ${(altGen || genElement)(el, state)}` +
    '})'
}


可以生成如下代码:


"_l((items),function(item,index){return _c('p',{key:index},[_v(_s(item))])}),0"


下面的是render函数中所用到的帮助方法的说明:


  • Vue.prototype._s 转换为字符类型


  • Vue.prototype._l 渲染列表


  • Vue.prototype._v 创建文本类型的vnode


  • Vue.prototype._c 创建vnode


总结


以上就是vue模板编译的总体过程:通过核心正则表达式,逐个将HTML标签解析成AST节点,最后根据AST节点,生成render函数的函数体。render函数负责生成vnode。



目录
打赏
0
0
0
0
1
分享
相关文章
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
204 0
Vue 自定义进度条组件封装及使用方法详解
这是一篇关于自定义进度条组件的使用指南和开发文档。文章详细介绍了如何在Vue项目中引入、注册并使用该组件,包括基础与高级示例。组件支持分段配置(如颜色、文本)、动画效果及超出进度提示等功能。同时提供了完整的代码实现,支持全局注册,并提出了优化建议,如主题支持、响应式设计等,帮助开发者更灵活地集成和定制进度条组件。资源链接已提供,适合前端开发者参考学习。
171 17
Vue 表情包输入组件实现代码及详细开发流程解析
这是一篇关于 Vue 表情包输入组件的使用方法与封装指南的文章。通过安装依赖、全局注册和局部使用,可以快速集成表情包功能到 Vue 项目中。文章还详细介绍了组件的封装实现、高级配置(如自定义表情列表、主题定制、动画效果和懒加载)以及完整集成示例。开发者可根据需求扩展功能,例如 GIF 搜索或自定义表情上传,提升用户体验。资源链接提供进一步学习材料。
89 1
Vue框架中常见指令的应用概述。
通过以上的详细解析,你应该已经初窥Vue.js的指令的威力了。它们是Vue声明式编程模型的核心之一,无论是构建简单的静态网站还是复杂的单页面应用,你都会经常用到。记住,尽管Vue提供了大量预定义的指令,你还可以创建自定义指令以满足特定的需求。为你的Vue应用程序加上这些功能增强器,让编码变得更轻松、更愉快吧!
39 1
如何高效实现 vue 文件批量下载及相关操作技巧
在Vue项目中,实现文件批量下载是常见需求。例如文档管理系统或图片库应用中,用户可能需要一次性下载多个文件。本文介绍了三种技术方案:1) 使用`file-saver`和`jszip`插件在前端打包文件为ZIP并下载;2) 借助后端接口完成文件压缩与传输;3) 使用`StreamSaver`解决大文件下载问题。同时,通过在线教育平台的实例详细说明了前后端的具体实现步骤,帮助开发者根据项目需求选择合适方案。
106 0
Vue 项目中如何自定义实用的进度条组件
本文介绍了如何使用Vue.js创建一个灵活多样的自定义进度条组件。该组件可接受进度段数据数组作为输入,动态渲染进度段,支持动画效果和内容展示。当进度超出总长时,超出部分将以红色填充。文章详细描述了组件的设计目标、实现步骤(包括props定义、宽度计算、模板渲染、动画处理及超出部分的显示),并提供了使用示例。通过此组件,开发者可根据项目需求灵活展示进度情况,优化用户体验。资源地址:[https://pan.quark.cn/s/35324205c62b](https://pan.quark.cn/s/35324205c62b)。
49 0
|
3月前
|
vue实现任务周期cron表达式选择组件
vue实现任务周期cron表达式选择组件
378 4
基于 Vue 与 D3 的可拖拽拓扑图技术方案及应用案例解析
本文介绍了基于Vue和D3实现可拖拽拓扑图的技术方案与应用实例。通过Vue构建用户界面和交互逻辑,结合D3强大的数据可视化能力,实现了力导向布局、节点拖拽、交互事件等功能。文章详细讲解了数据模型设计、拖拽功能实现、组件封装及高级扩展(如节点类型定制、连接样式优化等),并提供了性能优化方案以应对大数据量场景。最终,展示了基础网络拓扑、实时更新拓扑等应用实例,为开发者提供了一套完整的实现思路和实践经验。
244 77
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问