从 vue 源码看问题 —— vue 编译器如何生成渲染函数?(上)

简介: 从 vue 源码看问题 —— vue 编译器如何生成渲染函数?

image.png


前言

前两篇主要了解了 vue 编译器的 解析优化

  • 将组件的 html 模版解析成 AST 对象
  • 基于 AST 语法树 进行静态标记,首先标记每个节点是否为 静态节点,然后进一步标记出静态 根节点,便于在后续更新中跳过静态根节点的更新,从而提高性能

下面就了解一下 vue 编译器是如何从 AST 语法树 生成运行渲染函数.

深入源码

createCompiler() 方法 —— 入口

文件位置:/src/compiler/index.js

其中最主要的就是 generate(ast, options) 方法,它负责从 AST 语法树生成渲染函数.

/*
  在这之前做的所有的事情,只是为了构建平台特有的编译选项(options),比如 web 平台
  1、将 html 模版解析成 ast
  2、对 ast 树进行静态标记
  3、将 ast 生成渲染函数
     - 静态渲染函数放到 code.staticRenderFns 数组中
     - 动态渲染函数 code.render
     - 在将来渲染时执行渲染函数能够得到 vnode
 */
export const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult {
  /* 
   将模版字符串解析为 AST 语法树
   每个节点的 ast 对象上都设置了元素的所有信息,如,标签信息、属性信息、插槽信息、父节点、子节点等
  */
  const ast = parse(template.trim(), options)
  /*
   优化,遍历 AST,为每个节点做静态标记
     - 标记每个节点是否为静态节点,保证在后续更新中跳过这些静态节点
     - 标记出静态根节点,用于生成渲染函数阶段,生成静态根节点的渲染函数
       优化,遍历 AST,为每个节点做静态标记
 */
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  /*
    从 AST 语法树生成渲染函数
    如:code.render = "_c('div',{attrs:{"id":"app"}},_l((arr),function(item){return _c('div',{key:item},[_v(_s(item))])}),0)"
  */
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
复制代码

generate() 方法

文件位置:src\compiler\codegen\index.js

其中在给 code 赋值时,主要的内容是通过 genElement(ast, state) 方法进行生成的.

/*
   从 AST 生成渲染函数:
    - render 为字符串的代码
    - staticRenderFns 为包含多个字符串的代码,形式为 `with(this){return xxx}`
*/
export function generate (
  ast: ASTElement | void, // ast 对象
  options: CompilerOptions // 编译选项
): CodegenResult {
  /*
    实例化 CodegenState 对象,参数是编译选项,最终得到 state ,其中大部分属性和 options 一样
  */
  const state = new CodegenState(options)
  /* 
   生成字符串格式的代码,比如:'_c(tag, data, children, normalizationType)'
    - data 为节点上的属性组成 JSON 字符串,比如 '{ key: xx, ref: xx, ... }'
    - children 为所有子节点的字符串格式的代码组成的字符串数组,格式:
      `['_c(tag, data, children)', ...],normalizationType`,
    - normalization 是 _c 的第四个参数,表示节点的规范化类型(非重点,可跳过)
    注意:code 并不一定就是 _c,也有可能是其它的,比如整个组件都是静态的,则结果就为 _m(0)
  */
  const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}
复制代码

genElement() 方法

文件位置:src\compiler\codegen\index.js

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }
  if (el.staticRoot && !el.staticProcessed) {
    /*
      处理静态根节点,生成节点的渲染函数
        1、将当前静态节点的渲染函数放到 staticRenderFns 数组中
        2、返回一个可执行函数 _m(idx, true or '')
    */
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    /*
      处理带有 v-once 指令的节点,结果会有三种:
        1、当前节点存在 v-if 指令,得到一个三元表达式,`condition ? render1 : render2`
        2、当前节点是一个包含在 v-for 指令内部的静态节点,得到 `_o(_c(tag, data, children), number, key)`
        3、当前节点就是一个单纯的 v-once 节点,得到 `_m(idx, true of '')`
     */
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    /*
      处理节点上的 v-for 指令,得到:
        `_l(exp, function(alias, iterator1, iterator2){return _c(tag, data, children)})`
    */ 
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    /*
      处理带有 v-if 指令的节点,最终得到一个三元表达式:`condition ? render1 : render2`
    */
    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(slotName, children, attrs, bind)` */
    return genSlot(el, state)
  } else {
    /*
      component or element
      处理 动态组件 和 普通元素(自定义组件、原生标签、平台保留标签,如 web 平台中的每个 html 标签)
    */
    let code
    if (el.component) {
      /*
        处理动态组件,生成动态组件的渲染函数,得到 `_c(compName, data, children)`
      */
      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`
        其中的 normalization 表示节点的规范化类型(非重点,可跳过)
      */
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      /*
        得到最终的字符串格式的代码,格式:_c(tag, data, children, normalizationType)
      */ 
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    /*
      如果提供了 transformCode 方法,则最终的 code 会经过各个模块(module)的该方法处理,
      不过框架没提供这个方法,不过即使处理了,最终的格式也是 _c(tag, data, children)
      module transforms
    */
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    // 返回 code
    return code
  }
}
复制代码

genChildren() 方法

文件位置:src\compiler\codegen\index.js

/*
  生成所有子节点的渲染函数,返回一个数组,格式如:
   `[_c(tag, data, children, normalizationType), ...]`
 */
export 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]
    // optimize single v-for
    if (children.length === 1 &&
      el.for &&
      el.tag !== 'template' &&
      el.tag !== 'slot'
    ) {
      /* 
       优化处理:
         - 条件:只有一个子节点 && 子节点的上有 v-for 指令 && 子节点的标签不为 template 或者 slot
         - 方式:直接调用 genElement 生成该节点的渲染函数,不需要走下面的循环然后调用 genCode 最后得到渲染函数
      */
      const normalizationType = checkSkip
        ? state.maybeComponent(el) ? `,1` : `,0`
        : ``
      return `${(altGenElement || genElement)(el, state)}${normalizationType}`
    }
    // 获取节点规范化类型,返回一个 number: 0、1、2(非重点,可跳过)
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    // 是一个函数,负责生成代码的一个函数
    const gen = altGenNode || genNode
    /*
      返回一个数组,其中每个元素都是一个子节点的渲染函数
      格式:['_c(tag, data, children, normalizationType)', ...]
    */ 
    return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}
复制代码

genNode() 方法

文件位置:src\compiler\codegen\index.js

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)
  }
}
复制代码

genComment() 方法

文件位置:src\compiler\codegen\index.js

// 得到返回值,格式为:`_e(xxxx)`
export function genComment (comment: ASTText): string {
  return `_e(${JSON.stringify(comment.text)})`
}
复制代码

genText() 方法

文件位置:src\compiler\codegen\index.js

// 得到返回值,格式为:`_v(xxxxx)`
export 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))
  })`
}



目录
相关文章
|
2天前
|
JavaScript
vue打印v-model 的值
vue打印v-model 的值
|
2天前
|
JavaScript
Vue实战-组件通信
Vue实战-组件通信
5 0
|
2天前
|
JavaScript
Vue实战-将通用组件注册为全局组件
Vue实战-将通用组件注册为全局组件
5 0
|
3天前
|
JavaScript 前端开发
vue的论坛管理模块-文章评论02
vue的论坛管理模块-文章评论02
|
3天前
|
JavaScript
VUE里的find与filter使用与区别
VUE里的find与filter使用与区别
12 0
|
3天前
|
JavaScript
vue页面加载时同时请求两个接口
vue页面加载时同时请求两个接口
|
3天前
|
JavaScript
vue里样式不起作用的方法,可以通过deep穿透的方式
vue里样式不起作用的方法,可以通过deep穿透的方式
12 0
|
3天前
|
移动开发 JavaScript 应用服务中间件
vue打包部署问题
Vue项目`vue.config.js`中,`publicPath`设定为&quot;/h5/party/pc/&quot;,在线环境基于打包后的`dist`目录,而非Linux的`/root`。Nginx代理配置位于`/usr/local/nginx/nginx-1.13.7/conf`,包含两个相关配置图。
vue打包部署问题
|
3天前
|
JavaScript 前端开发
iconfont 图标在vue里的使用
iconfont 图标在vue里的使用
13 0
|
4天前
|
存储 JavaScript
Vue当前时间与接口返回时间的判断
Vue当前时间与接口返回时间的判断
8 0