vue2 模版编译原理

渲染整个流程

  1. 模板解析(parse):生成 AST Vue 需要将用户编写的 template(或 el 挂载的 DOM 模板)转换为可执行的 渲染函数(render function),这一步是 "模板 → 逻辑" 的转换。

代码生成(generate):AST → 渲染函数

  1. 渲染函数的核心是调用 h 函数(即 createElement),h 函数会生成虚拟 DOM(VNode)
  2. 虚拟 DOM → 真实 DOM(patch 首次渲染)

将模板编译成渲染函数

将模板编译成渲染函数可以分两个步骤,先将模板解析成AST(Abstract Syntax Tree,抽象语法树)​,然后再使用AST生成渲染函数。 在大体逻辑模版编译 分为三部分内容:

  1. 将模版解析成AST
  2. 遍历AST标记静态节点
  3. 使用AST生成渲染函数

模版编译的整体流程

渲染函数的作用是创建vnode

模板编译在整个渲染过程的位置 什么是模版编译 如何将模版编译成渲染函数

先将模版解析抽象语法树(AST)然后遍历AST标记静态节点 最后使用AST生成代码字符串。这部分内容分别对应内容分别对应三个模块: 解析器 优化器 代码生成器 。

解析器

AST数据结构

markdown 复制代码
/**
 * @internal
 * 抽象语法树(AST)元素节点类型定义,用于表示模板中的HTML元素
 */
export type ASTElement = {
  /**
   * 节点类型标记,1表示元素节点
   */
  type: 1
  
  /**
   * 元素标签名
   */
  tag: string
  
  /**
   * 元素属性列表,按原始顺序存储
   */
  attrsList: Array<ASTAttr>
  
  /**
   * 元素属性映射,键为属性名,值为属性值
   */
  attrsMap: { [key: string]: any }
  
  /**
   * 原始属性映射,键为属性名,值为完整的ASTAttr对象
   */
  rawAttrsMap: { [key: string]: ASTAttr }
  
  /**
   * 父元素节点引用
   */
  parent: ASTElement | void
  
  /**
   * 子节点数组,包含元素、文本或表达式节点
   */
  children: Array<ASTNode>

  /**
   * 元素在原始模板中的起始位置索引
   */
  start?: number
  
  /**
   * 元素在原始模板中的结束位置索引
   */
  end?: number

  /**
   * 是否已处理标记
   */
  processed?: true

  /**
   * 是否为静态节点(内容不会随数据变化)
   */
  static?: boolean
  
  /**
   * 是否为静态根节点(可被提升的静态节点)
   */
  staticRoot?: boolean
  
  /**
   * 是否在v-for循环中的静态节点
   */
  staticInFor?: boolean
  
  /**
   * 静态节点是否已处理
   */
  staticProcessed?: boolean
  
  /**
   * 是否包含动态绑定
   */
  hasBindings?: boolean

  /**
   * 元素的文本内容
   */
  text?: string
  
  /**
   * 处理后的属性列表
   */
  attrs?: Array<ASTAttr>
  
  /**
   * 动态属性列表
   */
  dynamicAttrs?: Array<ASTAttr>
  
  /**
   * 组件属性(props)列表
   */
  props?: Array<ASTAttr>
  
  /**
   * 是否为普通元素(没有特殊指令或绑定)
   */
  plain?: boolean
  
  /**
   * 是否为<pre>标签,需要保留空白
   */
  pre?: true
  
  /**
   * 元素的XML命名空间
   */
  ns?: string

  /**
   * 组件名称
   */
  component?: string
  
  /**
   * 是否使用内联模板
   */
  inlineTemplate?: true
  
  /**
   * 过渡模式(in-out/out-in)
   */
  transitionMode?: string | null
  
  /**
   * 插槽名称
   */
  slotName?: string | null
  
  /**
   * 插槽目标
   */
  slotTarget?: string | null
  
  /**
   * 插槽目标是否为动态的
   */
  slotTargetDynamic?: boolean
  
  /**
   * 插槽作用域标识符
   */
  slotScope?: string | null
  
  /**
   * 作用域插槽映射
   */
  scopedSlots?: { [name: string]: ASTElement }

  /**
   * 引用标识符
   */
  ref?: string
  
  /**
   * 引用是否在v-for循环中
   */
  refInFor?: boolean

  /**
   * v-if条件表达式
   */
  if?: string
  
  /**
   * v-if是否已处理
   */
  ifProcessed?: boolean
  
  /**
   * v-else-if条件表达式
   */
  elseif?: string
  
  /**
   * 是否为v-else分支
   */
  else?: true
  
  /**
   * if条件分支列表
   */
  ifConditions?: ASTIfConditions

  /**
   * v-for表达式
   */
  for?: string
  
  /**
   * v-for是否已处理
   */
  forProcessed?: boolean
  
  /**
   * 循环key属性
   */
  key?: string
  
  /**
   * 循环变量别名
   */
  alias?: string
  
  /**
   * 循环索引变量名
   */
  iterator1?: string
  
  /**
   * 对象循环的值变量名
   */
  iterator2?: string

  /**
   * 静态CSS类名
   */
  staticClass?: string
  
  /**
   * 动态类绑定表达式
   */
  classBinding?: string
  
  /**
   * 静态样式字符串
   */
  staticStyle?: string
  
  /**
   * 动态样式绑定表达式
   */
  styleBinding?: string
  
  /**
   * 事件处理器映射
   */
  events?: ASTElementHandlers
  
  /**
   * 原生事件处理器映射
   */
  nativeEvents?: ASTElementHandlers

  /**
   * 过渡动画名称或标记
   */
  transition?: string | true
  
  /**
   * 初始渲染时是否应用过渡动画
   */
  transitionOnAppear?: boolean

  /**
   * v-model指令配置
   */
  model?: {
    value: string      // v-model绑定的值
    callback: string   // 更新值的回调函数
    expression: string // 原始表达式
  }

  /**
   * 应用在元素上的指令列表
   */
  directives?: Array<ASTDirective>

  /**
   * 是否为禁止使用的元素
   */
  forbidden?: true
  
  /**
   * 是否使用v-once指令
   */
  once?: true
  
  /**
   * v-once是否已处理
   */
  onceProcessed?: boolean
  
  /**
   * 数据包装函数,用于自定义代码生成
   */
  wrapData?: (code: string) => string
  
  /**
   * 监听器包装函数,用于自定义代码生成
   */
  wrapListeners?: (code: string) => string

  /**
   * 2.4版本SSR优化标记,指示节点的可优化程度
   */
  ssrOptimizability?: number
}

解析器内部运行原理

解析器内部也分了好几个子解析器,比如HTML解析器、文本解析器以及过滤器解析器

html解析器

解析过程

ini 复制代码
// 定义 AST 元素类
class ASTElement {
  constructor(tag, attrs) {
    this.type = 1; // 类型为元素
    this.tag = tag; // 标签名
    this.attrsList = attrs || []; // 属性列表
    this.parent = null; // 父节点
    this.children = []; // 子节点列表
    this.text = ''; // 文本内容
  }
}

// 定义 AST 文本类
class ASTText {
  constructor(text) {
    this.type = 3; // 类型为文本
    this.text = text; // 文本内容
  }
}

// 简化的 parseHTML 函数
function parseHTML(html) {
  let index = 0; // 当前解析位置索引
  let root; // AST 根节点
  let currentParent; // 当前父节点

  const stack = []; // 节点栈,用于处理嵌套关系

  // 循环直到 html 字符串为空
  while (html) {
    // 查找下一个标签开始的位置
    let textEnd = html.indexOf('<');

    if (textEnd === 0) {
      // 处理注释、条件注释、Doctype等特殊标签

      // 匹配注释
      const commentMatch = html.match(/^<!--([\s\S]*?)-->/);
      if (commentMatch) {
        advance(commentMatch[0].length); // 移动指针跳过注释
        continue;
      }

      // 匹配 Doctype
      const doctypeMatch = html.match(/^<!DOCTYPE [^>]+>/i);
      if (doctypeMatch) {
        advance(doctypeMatch[0].length); // 移动指针跳过 Doctype
        continue;
      }

      // 处理结束标签
      const endTagMatch = html.match(/^<\/([a-zA-Z_][^\t\r\n\f />]*)/);
      if (endTagMatch) {
        const tagName = endTagMatch[1];
        advance(endTagMatch[0].length); // 移动指针跳过结束标签
        closeElement(tagName.toLowerCase()); // 关闭当前元素
        continue;
      }

      // 处理开始标签
      const startTagMatch = html.match(/^[<]([a-zA-Z_][^\t\r\n\f />]+)/);
      if (startTagMatch) {
        const tagName = startTagMatch[1];
        const attrMatch = [];
        let end, attr;

        // 匹配属性
        while (
          !(end = html.match(/^\/?>/)) && // 查找结束符号 > 或 />
          (attr = html.match(
            /^([^=><"'` ]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+))?/
          ))
        ) {
          attrMatch.push(attr); // 收集属性
          advance(attr[0].length); // 移动指针跳过属性
        }
        advance(end[0].length); // 移动指针跳过结束符号

        const element = new ASTElement(tagName.toLowerCase(), attrMatch); // 创建 AST 元素
        processAttrs(element); // 处理属性

        if (!root) {
          root = element; // 设置根节点
        }

        if (currentParent) {
          currentParent.children.push(element); // 将元素添加到当前父节点的子节点列表
          element.parent = currentParent; // 设置父节点
        }

        if (!end[0].includes('/')) {
          stack.push(element); // 将元素压入栈中
          currentParent = element; // 更新当前父节点
        }

        continue;
      }
    }

    // 处理文本节点
    if (textEnd >= 0) {
      const text = html.substring(0, textEnd); // 获取文本内容
      if (text) {
        currentParent.children.push(new ASTText(text)); // 将文本节点添加到当前父节点的子节点列表
      }
      advance(textEnd); // 移动指针跳过文本
    }

    // 如果还有剩余的 HTML 字符串,则将其作为文本节点处理
    if (html) {
      const remainingText = html.trim();
      if (remainingText) {
        currentParent.children.push(new ASTText(remainingText)); // 将剩余文本作为文本节点添加
        advance(html.length); // 移动指针跳过剩余字符串
      }
    }
  }

  return root; // 返回 AST 根节点

  // 辅助函数:移动指针
  function advance(n) {
    index += n; // 更新索引
    html = html.substring(n); // 截取剩余字符串
  }

  // 辅助函数:关闭元素
  function closeElement(tagName) {
    let element = stack.pop(); // 弹出栈顶元素
    if (element && element.tag !== tagName) {
      console.warn(`Mismatched tags: ${tagName}`); // 报告不匹配的标签
    }
    currentParent = stack[stack.length - 1]; // 更新当前父节点为新的栈顶元素
  }

  // 辅助函数:处理属性
  function processAttrs(element) {
    element.attrsList.forEach(attr => {
      const name = attr[1]; // 属性名
      const value = attr[3] || attr[4] || attr[5] || true; // 属性值
      element.attrs[name] = value; // 将属性添加到元素的 attrs 对象中
    });
  }
}

// 示例用法
const htmlString = '<div id="app"><span class="greeting">Hello, Vue!</span></div>';
const astRoot = parseHTML(htmlString);
console.log(JSON.stringify(astRoot, null, 2)); // 打印生成的 AST 树

流程图

ini 复制代码
// 定义 AST 元素类
class ASTElement {
  constructor(tag, attrs) {
    this.type = 1; // 类型为元素
    this.tag = tag; // 标签名
    this.attrsList = attrs || []; // 属性列表
    this.parent = null; // 父节点
    this.children = []; // 子节点列表
    this.text = ''; // 文本内容
  }
}

// 定义 AST 文本类
class ASTText {
  constructor(text) {
    this.type = 3; // 类型为文本
    this.text = text; // 文本内容
  }
}

// 简化的 parseHTML 函数
function parseHTML(html) {
  let index = 0; // 当前解析位置索引
  let root; // AST 根节点
  let currentParent; // 当前父节点

  const stack = []; // 节点栈,用于处理嵌套关系

  // 循环直到 html 字符串为空
  while (html) {
    // 查找下一个标签开始的位置
    let textEnd = html.indexOf('<');

    if (textEnd === 0) {
      // 处理注释、条件注释、Doctype等特殊标签

      // 匹配注释
      const commentMatch = html.match(/^<!--([\s\S]*?)-->/);
      if (commentMatch) {
        advance(commentMatch[0].length); // 移动指针跳过注释
        continue;
      }

      // 匹配 Doctype
      const doctypeMatch = html.match(/^<!DOCTYPE [^>]+>/i);
      if (doctypeMatch) {
        advance(doctypeMatch[0].length); // 移动指针跳过 Doctype
        continue;
      }

      // 处理结束标签
      const endTagMatch = html.match(/^<\/([a-zA-Z_][^\t\r\n\f />]*)/);
      if (endTagMatch) {
        const tagName = endTagMatch[1];
        advance(endTagMatch[0].length); // 移动指针跳过结束标签
        closeElement(tagName.toLowerCase()); // 关闭当前元素
        continue;
      }

      // 处理开始标签
      const startTagMatch = html.match(/^[<]([a-zA-Z_][^\t\r\n\f />]+)/);
      if (startTagMatch) {
        const tagName = startTagMatch[1];
        const attrMatch = [];
        let end, attr;

        // 匹配属性
        while (
          !(end = html.match(/^\/?>/)) && // 查找结束符号 > 或 />
          (attr = html.match(
            /^([^=><"'` ]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+))?/
          ))
        ) {
          attrMatch.push(attr); // 收集属性
          advance(attr[0].length); // 移动指针跳过属性
        }
        advance(end[0].length); // 移动指针跳过结束符号

        const element = new ASTElement(tagName.toLowerCase(), attrMatch); // 创建 AST 元素
        processAttrs(element); // 处理属性

        if (!root) {
          root = element; // 设置根节点
        }

        if (currentParent) {
          currentParent.children.push(element); // 将元素添加到当前父节点的子节点列表
          element.parent = currentParent; // 设置父节点
        }

        if (!end[0].includes('/')) {
          stack.push(element); // 将元素压入栈中
          currentParent = element; // 更新当前父节点
        }

        continue;
      }
    }

    // 处理文本节点
    if (textEnd >= 0) {
      const text = html.substring(0, textEnd); // 获取文本内容
      if (text) {
        currentParent.children.push(new ASTText(text)); // 将文本节点添加到当前父节点的子节点列表
      }
      advance(textEnd); // 移动指针跳过文本
    }

    // 如果还有剩余的 HTML 字符串,则将其作为文本节点处理
    if (html) {
      const remainingText = html.trim();
      if (remainingText) {
        currentParent.children.push(new ASTText(remainingText)); // 将剩余文本作为文本节点添加
        advance(html.length); // 移动指针跳过剩余字符串
      }
    }
  }

  return root; // 返回 AST 根节点

  // 辅助函数:移动指针
  function advance(n) {
    index += n; // 更新索引
    html = html.substring(n); // 截取剩余字符串
  }

  // 辅助函数:关闭元素
  function closeElement(tagName) {
    let element = stack.pop(); // 弹出栈顶元素
    if (element && element.tag !== tagName) {
      console.warn(`Mismatched tags: ${tagName}`); // 报告不匹配的标签
    }
    currentParent = stack[stack.length - 1]; // 更新当前父节点为新的栈顶元素
  }

  // 辅助函数:处理属性
  function processAttrs(element) {
    element.attrsList.forEach(attr => {
      const name = attr[1]; // 属性名
      const value = attr[3] || attr[4] || attr[5] || true; // 属性值
      element.attrs[name] = value; // 将属性添加到元素的 attrs 对象中
    });
  }
}

// 示例用法
const htmlString = '<div id="app"><span class="greeting">Hello, Vue!</span></div>';
const astRoot = parseHTML(htmlString);
console.log(JSON.stringify(astRoot, null, 2)); // 打印生成的 AST 树

当上面这个模版被HTML解析器解析时 ,所触发的钩子函数依次是 start start chars end 和 end

优化器

代码生成器

代码生成器是模板编译的最后一步,它的作用是将AST转换成渲染函数中的内容,这个内容可以称为代码字符串。代码字符串可以被包装在函数中执行,这个函数就是我们通常所说的渲染函数。渲染函数被执行之后,可以生成一份VNode,而虚拟DOM可以通过这个VNode来渲染视图。关于虚拟DOM如何使用VNode渲染视图,我们在第二篇中已经介绍过。

核心代码

scss 复制代码
// 创建虚拟DOM元素的内部实现函数
export function _createElement(
  // 组件上下文实例
  context: Component,
  // 标签名(字符串)、组件选项对象、构造函数或异步组件工厂函数
  tag?: string | Component | Function | Object,
  // 虚拟节点的数据对象,包含props、attrs、事件等
  data?: VNodeData,
  // 子节点,可以是字符串、数组或函数
  children?: any,
  // 子节点规范化类型:1-简单规范化,2-总是规范化
  normalizationType?: number
): VNode | Array<VNode> {
  // 检查data是否为响应式对象(有__ob__属性表示被Vue观测过)
  if (isDef(data) && isDef((data as any).__ob__)) {
    // 开发环境下警告:避免使用响应式对象作为vnode数据
    __DEV__ &&
      warn(
        `Avoid using observed data object as vnode data: ${JSON.stringify(
          data
        )}\n` + 'Always create fresh vnode data objects in each render!',
        context
      )
    // 返回空虚拟节点,避免使用响应式数据
    return createEmptyVNode()
  }
  // 处理动态组件语法(:is绑定)
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    // 将data.is的值赋给tag,实现动态组件
    tag = data.is
  }
  // 如果tag不存在(可能是:is绑定到了假值)
  if (!tag) {
    // in case of component :is set to falsy value
    // 返回空虚拟节点
    return createEmptyVNode()
  }
  // 开发环境下检查key是否为原始值(字符串或数字)
  // warn against non-primitive key
  if (__DEV__ && isDef(data) && isDef(data.key) && !isPrimitive(data.key)) {
    // 警告:避免使用非原始值作为key,应该使用字符串或数字
    warn(
      'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
      context
    )
  }
  // 支持将单个函数子节点作为默认作用域插槽
  // support single function children as default scoped slot
  if (isArray(children) && isFunction(children[0])) {
    // 确保data对象存在
    data = data || {}
    // 将第一个函数子节点设置为默认作用域插槽
    data.scopedSlots = { default: children[0] }
    // 清空children数组,避免重复处理
    children.length = 0
  }
  // 根据规范化类型处理子节点
  if (normalizationType === ALWAYS_NORMALIZE) {
    // 总是规范化:处理嵌套数组和函数子节点
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // 简单规范化:只处理基本的一维数组
    children = simpleNormalizeChildren(children)
  }
  // 声明虚拟节点和命名空间变量
  let vnode, ns
  // 判断tag类型:字符串标签 vs 组件对象
  if (typeof tag === 'string') {
    // 组件构造函数变量
    let Ctor
    // 获取标签的命名空间(SVG、MathML等特殊命名空间)
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 检查是否为平台保留标签(HTML标签或SVG标签)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      // 开发环境下检查.native修饰符的使用是否正确
      if (
        __DEV__ &&
        isDef(data) &&
        isDef(data.nativeOn) &&
        data.tag !== 'component'
      ) {
        // 警告:.native修饰符只能用于组件,不能用于普通元素
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      // 创建平台内置元素的虚拟节点
      vnode = new VNode(
        // 解析平台特定的标签名(处理如<transition>等抽象组件)
        config.parsePlatformTagName(tag),
        data,
        children,
        undefined,
        undefined,
        context
      )
    } else if (
      // 检查是否为已注册的组件
      (!data || !data.pre) &&
      isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
    ) {
      // component
      // 创建组件虚拟节点
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // 处理未知或未列出的命名空间元素
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      // 创建通用虚拟节点,可能在父节点规范化时分配命名空间
      vnode = new VNode(tag, data, children, undefined, undefined, context)
    }
  } else {
    // 直接处理组件选项对象或构造函数
    // direct component options / constructor
    // 创建组件虚拟节点
    vnode = createComponent(tag as any, data, context, children)
  }
  // 处理返回结果:可能是数组或单个虚拟节点
  if (isArray(vnode)) {
    // 如果是数组,直接返回(处理函数式组件的情况)
    return vnode
  } else if (isDef(vnode)) {
    // 如果虚拟节点存在
    // 应用命名空间
    if (isDef(ns)) applyNS(vnode, ns)
    // 注册深度绑定(处理样式和类的响应式依赖)
    if (isDef(data)) registerDeepBindings(data)
    // 返回虚拟节点
    return vnode
  } else {
    // 如果虚拟节点不存在,返回空虚拟节点
    return createEmptyVNode()
  }
}

代码字符串中的 _c其实是createElement的别名。createElement是虚拟DOM中所提供的方法,它的作用是创建虚拟节点,有三个参数,分别是:

  • 标签名
  • 一个包含模板相关属性的数据对象
  • 子节点列表

我们介绍了代码生成器的作用及其内部原理,了解了代码生成器其实就是字符串拼接的过程。通过递归AST来生成字符串,最先生成根节点,然后在子节点字符串生成后,将其拼接在根节点的参数中,子节点的子节点拼接在子节点的参数中,这样一层一层地拼接,直到最后拼接成完整的字符串。同时还介绍了三种类型的节点,分别是元素节点、文本节点与注释节点。而不同类型的节点生成字符串的方式是不同的。最后,我们介绍了当字符串拼接好后,会将字符串拼在with中返回给调用者。

相关推荐
一只小阿乐4 分钟前
react 状态管理mobx中的行为模式
前端·javascript·react.js·mobx·vue开发·react开发
l***O5207 分钟前
前端路由历史监听,React与Vue实现
前端·vue.js·react.js
超级战斗鸡8 分钟前
React 性能优化教程:useMemo 和 useCallback 的正确使用方式
前端·react.js·性能优化
bemyrunningdog8 分钟前
创建 React 项目指南:Vite 与 Create React App 详
前端·react.js·前端框架
大雷神18 分钟前
DevUI 实战教程:从零构建电商后台管理系统(完整版)
前端·javascript·华为·angular.js
come1123420 分钟前
现代前端技术栈关系详解 (PHP 开发者特供版)
开发语言·前端·php
合作小小程序员小小店24 分钟前
web网页开发,在线%图书管理%系统,基于Idea,html,css,jQuery,java,ssm,mysql。
java·前端·后端·mysql·jdk·intellij-idea
猪八戒1.01 小时前
onenet接口
开发语言·前端·javascript·嵌入式硬件
程序猿小蒜1 小时前
基于Spring Boot的宠物领养系统的设计与实现
java·前端·spring boot·后端·spring·宠物
合作小小程序员小小店1 小时前
web网页开发,在线%食堂管理%系统,基于Idea,html,css,jQuery,java,ssm,mysql。
java·前端·mysql·html·intellij-idea·jquery