美文网首页
Vue 源码解析 - 模板编译

Vue 源码解析 - 模板编译

作者: Whyn | 来源:发表于2020-04-12 11:10 被阅读0次

    [TOC]

    模板编译

    前文在对 Vue 源码解析 - 主线流程 进行分析时,我们已经知道对于 Runtime + Compiler 的编译版本来说,Vue 在实例化前总共会经历两轮mount过程,分别为:

    • 定义于src\platforms\web\runtime\index.js$mount函数,主要负责组件挂载功能。

    • 定义于src\platforms\web\entry-runtime-with-compiler.js$mount函数,主要负责模板编译 + 组件挂载(其会缓存src\platforms\web\runtime\index.js中定义的$mount函数,最后的组件挂载转交给该函数进行处理)功能。

    以下我们对src\platforms\web\entry-runtime-with-compiler.js$mount函数进行解析,主要分析 模板编译 部分内容:

    // src/platforms/web/entry-runtime-with-compiler.js
    const mount = Vue.prototype.$mount;
    Vue.prototype.$mount = function (
        el?: string | Element,
        hydrating?: boolean,
    ): Component {
        // 获取 el 元素对象,找不到则返回一个 div
        el = el && query(el);
        ...
        const options = this.$options;
        // resolve template/el and convert to render function
        if (!options.render) {
            let template = options.template;
            if (template) {
                // Vue.$options.template 为字符串
                if (typeof template === 'string') {
                    if (template.charAt(0) === '#') {
                        // 由 id 取得对应的 DOM 元素的 innerHTML
                        template = idToTemplate(template);
                        ...
                    }
                } else if (template.nodeType) {
                    template = template.innerHTML;
                } else {
                    if (process.env.NODE_ENV !== 'production') {
                        warn('invalid template option:' + template, this);
                    }
                    return this;
                }
            } else if (el) { // 没有 template
                template = getOuterHTML(el);
            }
            if (template) {
                ...
                // 对模板进行编译
                const {render, staticRenderFns} = compileToFunctions(
                    template,
                    {
                        outputSourceRange: process.env.NODE_ENV !== 'production',
                        shouldDecodeNewlines,
                        shouldDecodeNewlinesForHref,
                        delimiters: options.delimiters,
                        comments: options.comments,
                    },
                    this,
                );
                options.render = render;
                options.staticRenderFns = staticRenderFns;
                ...
            }
        }
        return mount.call(this, el, hydrating);
    };
    
    function getOuterHTML(el: Element): string {
        if (el.outerHTML) {
            return el.outerHTML
        } else {
            const container = document.createElement('div')
            container.appendChild(el.cloneNode(true))
            return container.innerHTML
        }
    }
    
    const idToTemplate = cached(id => {
        const el = query(id)
        return el && el.innerHTML
    })
    
    // src/shared/util.js
    export function cached<F: Function>(fn: F): F {
        const cache = Object.create(null)
        return (function cachedFn(str: string) {
            const hit = cache[str]
            return hit || (cache[str] = fn(str))
        }: any)
    }
    

    从源码中可以看到,只有在Options没有定义render函数时,才会进行模板编译。

    模板编译步骤共分两步:

    1. 获取模板字符串:模板字符串的获取包含以下几种情况:

      • 如果没有定义template,则直接获取el元素的outerHTML,即把el元素作为template

      • 如果template为字符串,并且以#开头,则表明template是以id进行指定,则通过该id获取对应元素的innerHTML

        cached函数参数为一个函数,返回为一个参数为string的函数,在该返回函数内部会调用cached函数的函数参数,并做一个缓存处理。
        对应于我们编译这部分,即会缓存以id进行声明的templateinnerHTML

      • 如果template为字符串,并且不以#开头,则表明template是一个完整的模板字符串,直接返回本身即可。

      • 如果templatenodeType类型,直接返回其innerHTML

      • 如果定义了template,但格式无法识别(即不是字符串,也不是nodeType类型),则给出警告,并退出编译流程。

    2. 将模板字符串编译为render函数:该功能主要由函数compileToFunctions进行实现,其源码如下所示:

    // src/compiler/index.js
    export const createCompiler = createCompilerCreator(function baseCompile (...): CompiledResult {...})
    
    // src/platforms/web/compiler/index.js
    const { compile, compileToFunctions } = createCompiler(baseOptions)
    

    compileToFunctions是由createCompiler(baseOptions)返回的,而createCompilercreateCompilerCreator(function baseCompile (...){...}),这里其实使用了 函数柯里化 的思想,将接收多个参数的函数转化为接收单一参数的函数,这样做的原因是 编译 这个流程和平台或构建方式相关,采用 函数柯里化,将与平台无关的东西固定化,只留出平台相关的内容作为参数,简化调用。比如,这里固定化参数为baseCompile,其主要负责模板的解析,优化并最终生成模板代码的字符串(具体详情见后文),该操作是平台无关操作,而与平台相关的参数为baseOptions,不同的平台该参数不同。

    简而言之,compileToFunctions会经由createCompilerCreator(function baseCompile (...){...}) --> createCompiler(baseOptions)而得到。

    因此,我们先来看下createCompilerCreator(function baseCompile (...){...})的源码实现:

    // src/compiler/index.js
    export const createCompiler = createCompilerCreator(function baseCompile (...){...})
    
    // src\compiler\create-compiler.js
    export function createCompilerCreator (baseCompile: Function): Function {
      return function createCompiler (baseOptions: CompilerOptions) {
    
        function compile (...): CompiledResult {
           ...
          const compiled = baseCompile(template.trim(), finalOptions)
          ...
        }
    
        return {
          compile,
          compileToFunctions: createCompileToFunctionFn(compile)
        }
      }
    }
    

    所以createCompilerCreator就是固定了参数baseCompile,并返回一个函数createCompiler,该函数内部又会返回一个包含两个函数的实例,这其中就有一个我们需要分析的函数compileToFunctions(这个就是$mount函数内部使用的createCompileToFunctionFn),其指向为函数createCompileToFunctionFn(compile)的执行结果,我们先对函数createCompileToFunctionFn源码进行查看:

    // src/compiler/to-function.js
    export function createCompileToFunctionFn(compile: Function): Function {
        ...
        return function compileToFunctions(...): CompiledFunctionResult {...}
    }
    

    可以看到又是一个 函数柯里化 的操作,固定了平台无关参数compile,并返回了我们最终需要的compileToFunctions函数。

    compileToFunctions函数获取这部分的代码由于采用了多个 函数柯里化 操作,导致代码逻辑比较混乱,下面是该部分代码的整个调用链:

    // src/platforms/web/entry-runtime-with-compiler.js
    const {render, staticRenderFns} = compileToFunctions(template, {...}, this)
    
    // src/platforms/web/compiler/index.js
    const {compile, compileToFunctions} = createCompiler(baseOptions)
    
    // src/compiler/index.js
    export const createCompiler = createCompilerCreator(function baseCompile(...) {...})
    
    // src/compiler/create-compiler.js
    export function createCompilerCreator(baseCompile: Function): Function {
        return function createCompiler(baseOptions: CompilerOptions) {
    
            function compile(...): CompiledResult {...}
    
            return {
                compile,
                compileToFunctions: createCompileToFunctionFn(compile)
            }
        }
    }
    
    // src/compiler/to-function.js
    export function createCompileToFunctionFn(compile: Function): Function {
        return function compileToFunctions(...): CompiledFunctionResult {
              ...
            const compiled = compile(template, options)
            ...
        }
    }
    

    可以看到,compileToFunctions的获取调用链为:createCompilerCreator --> createCompiler --> createCompileToFunctionFn --> compileToFunctions

    到这里我们才理清了compileToFunctions函数的定义出处,现在回到主线流程,看下compileToFunctions是怎样具体编译出render函数:

    // src/compiler/to-function.js
    export function createCompileToFunctionFn(compile: Function): Function {
        const cache = Object.create(null)
    
        return function compileToFunctions(
            template: string,
            options?: CompilerOptions,
            vm?: Component
        ): CompiledFunctionResult {
            ...
            // check cache
            const key = options.delimiters
                ? String(options.delimiters) + template
                : template
            if (cache[key]) {
                return cache[key]
            }
    
            // compile
            const compiled = compile(template, options)
            ...
            // turn code into functions
            const res = {}
            const fnGenErrors = []
            res.render = createFunction(compiled.render, fnGenErrors) // 生成渲染函数
            res.staticRenderFns = compiled.staticRenderFns.map(code => {
                return createFunction(code, fnGenErrors)
            })
            ...
            return (cache[key] = res)
        }
    }
    
    function createFunction(code, errors) {
        try {
            return new Function(code) // 将字符串 code 渲染成函数
        } catch (err) {
            errors.push({err, code})
            return noop
        }
    }
    

    所以当我们调用compileToFunctions时,其会做如下三件事:

    • 模板编译:通过函数compile进行编译。

    • 生成渲染函数:通过函数createFunction将编译完成的模板生成相应的渲染函数(其实就是使用Function构造函数将编译完成的模板代码字符串转换成函数)。

    • 缓存渲染函数:依据模板字符串内容作为键值,缓存其编译结果。

    这里面最核心的就是 模板编译 步骤,目的就是编译出模板对应的渲染函数字符串。
    我们着重对这步进行分析,对compile函数进行源码查看:

    // src/compiler/create-compiler.js
    export function createCompilerCreator(baseCompile: Function): Function {
        return function createCompiler(baseOptions: CompilerOptions) {
            function compile(
                template: string,
                options?: CompilerOptions
            ): CompiledResult {
                const finalOptions = Object.create(baseOptions)
                ...
                const compiled = baseCompile(template.trim(), finalOptions)
                ...
                return compiled
            }
            ...
        }
    }
    

    compile内部会将编译过程交由参数baseCompile进行实际处理,而根据我们前面的分析,baseCompile就是函数createCompilerCreator采用 函数柯里化 固定的平台无关的参数,其源码如下所示:

    // src/compiler/index.js
    function baseCompile(
        template: string,
        options: CompilerOptions
    ): CompiledResult {
        const ast = parse(template.trim(), options)
        if (options.optimize !== false) {
            optimize(ast, options)
        }
        const code = generate(ast, options)
        return {
            ast,
            render: code.render,
            staticRenderFns: code.staticRenderFns
        }
    }
    

    因此,$mount函数内部的compileToFunctions函数最终调用的就是baseCompile函数进行模板编译流程。

    从源码中可以看到,baseCompile函数内部主要做了三件事:

    • 模板编译:由函数parse进行模板编译,并生成抽象语法树 AST

    • 优化 AST:由函数optimize负责。

    • 生成代码:由函数generate负责根据 AST 和编译选项生成相应代码(字符串形式)。

    我们下面针对这三个过程继续进行分析:

    • 模板编译:编译过程的第一步就是解析模板字符串,生成抽象语法树 AST。
      我们进入parse函数,查看其源码:
    // src/compiler/parser/index.js
    /**
     * Convert HTML string to AST.
     */
    export function parse(
        template: string,
        options: CompilerOptions
    ): ASTElement | void {
        ...
        let root
        ...
        parseHTML(template, {
            ...
            start(tag, attrs, unary, start, end) {
                ...
                let element: ASTElement = createASTElement(tag, attrs, currentParent)
                ...
                if (!root) {
                    root = element
                    ...
                }
                ...
            },
    
            end(tag, start, end) {
                const element = stack[stack.length - 1]
                // pop stack
                stack.length -= 1
                currentParent = stack[stack.length - 1]
                ...
                closeElement(element)
            },
    
            chars(text: string, start: number, end: number) {
                ...
                parseText(text, delimiters)
                ...
                children.push(child)
                ...
            },
            comment(text: string, start, end) {
                if (currentParent) {
                    ...
                    currentParent.children.push(child)
                }
            }
        })
        return root
    }
    

    parse函数最终通过调用函数parseHTML对模板进行解析,查看parseHTML源码:

    // src/compiler/parser/html-parser.js
    // Regular Expressions for parsing tags and attributes
    ...
    const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
    const doctype = /^<!DOCTYPE [^>]+>/i
    const comment = /^<!\--/
    const conditionalComment = /^<!\[/
    ...
    export function parseHTML(html, options) {
        ...
        let index = 0
        let last, lastTag
        while (html) {
            last = html
            // Make sure we're not in a plaintext content element like script/style
            if (!lastTag || !isPlainTextElement(lastTag)) {
                let textEnd = html.indexOf('<')
                if (textEnd === 0) {
                    // Comment:
                    if (comment.test(html)) {
                        ...
                        advance(commentEnd + 3)
                        continue
                        }
                    }
    
                    // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
                    if (conditionalComment.test(html)) {
                        ...
                        advance(conditionalEnd + 2)
                        continue
                        }
                    }
    
                    // Doctype:
                    const doctypeMatch = html.match(doctype)
                    if (doctypeMatch) {
                        advance(doctypeMatch[0].length)
                        continue
                    }
    
                    // End tag:
                    const endTagMatch = html.match(endTag)
                    if (endTagMatch) {
                        const curIndex = index
                        advance(endTagMatch[0].length)
                        parseEndTag(endTagMatch[1], curIndex, index)
                        continue
                    }
    
                    // Start tag:
                    const startTagMatch = parseStartTag()
                    if (startTagMatch) {
                            ...
                            advance(1)
                        }
                        continue
                    }
                }
                ...
                if (textEnd >= 0) {
                    rest = html.slice(textEnd)
                    while (
                        !endTag.test(rest) &&
                        !startTagOpen.test(rest) &&
                        !comment.test(rest) &&
                        !conditionalComment.test(rest)
                    ) {
                        // < in plain text, be forgiving and treat it as text
                        ...
                    text = html.substring(0, textEnd)
                }
                ...
                advance(text.length)
                ...
            } else {
                ...
                parseEndTag(stackedTag, index - endTagLength, index)
            }
            ...
        }
    
        // Clean up any remaining tags
        parseEndTag()
    
        function advance(n) {
            index += n
            html = html.substring(n)
        }
        ...
    }
    

    简单来说,parseHTML函数采用正则表达式来解析模板template,其解析步骤大概如下所示:

    • 首先获取模板字符<的索引位置,如果索引为0,则表明当前模板以<开头,则使用正则依次判断template是否匹配CommentconditionalCommentDoctypeEnd Tag还是Start Tag,匹配完成后,依据不同的匹配标签进行各自的解析,比如,对于Comment标签,则会进行如下解析:

      // Comment:
      if (comment.test(html)) {
          const commentEnd = html.indexOf('-->')
      
          if (commentEnd >= 0) {
              ...
              options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
              ...
              advance(commentEnd + 3)
              continue
          }
      }
      

      即如果template匹配Comment标签,则找到-->的索引位置,即找到注释标签的末尾位置,然后取出注释内容html.substring(4,commentEnd),交由options.comment函数进行处理(options.comment其实是parse函数内调用parseHTML传递进行的options.comment,因此,这里其实起一个回调作用,parse函数内就可以通过回调获取当前模板解析得到的注释节点的内容,从而可以进行处理或保存),解析完成后会通过advance函数将当前template的字符串进行截取,只保留还未进行解析的内容。

      其他节点解析处理与上述操作逻辑类似,均是根据节点特点进行解析,然后通过advance函数去除已解析的内容,只保留未解析的模板字符串,继续新一轮的解析。

      举个栗子:比如对于如下模板:

      template: `<h2 style="color:red">Hi, {{message}}</h2>`
      

      其解析流程如下图所示:

    parseHTML

    parseHTML每次解析完成一个节点时,就会将结果回调给parse函数,parse函数就可以根据这些结果进行抽象语法树(AST)的构建,其实质就是构建一个javascript对象,比如,上述模板构建得到的 AST 如下所示:

    {
    "type": 1,
    "tag": "h2",
    "attrsList": [],
    "attrsMap": {
      "style": "color:red"
    },
    "rawAttrsMap": {
      "style": {
        "name": "style",
        "value": "color:red",
        "start": 4,
        "end": 21
      }
    },
    "children": [
      {
        "type": 2,
        "expression": "\"Hi, \"+_s(message)",
        "tokens": [
          "Hi, ",
          {
            "@binding": "message"
          }
        ],
        "text": "Hi, {{message}}",
        "start": 22,
        "end": 37
      }
    ],
    "start": 0,
    "end": 42,
    "plain": false,
    "staticStyle": "{\"color\":\"red\"}"
    }
    

    到这里,我们就大概了解了模板字符串template解析成抽象语法树(AST)的整个过程。

    • 优化 AST:当完成 AST 的构建后,就可以对 AST 进行一些优化。

      Vue 之所以有 优化 AST 这个过程,主要是因为 Vue 的特性之一是 数据驱动,并且数据具备响应式功能,因此,当更改数据的时候,模板会重新进行渲染,显示最新的数据。但是,模板中并不是所有的节点都需要进行重新渲染,对于不包含响应式数据的节点,从始至终只需一次渲染即可。

      我们进入optimize函数,查看其源码:

      // src/compiler/optimizer.js
      /**
      * Goal of the optimizer: walk the generated template AST tree
      * and detect sub-trees that are purely static, i.e. parts of
      * the DOM that never needs to change.
      *
      * Once we detect these sub-trees, we can:
      *
      * 1. Hoist them into constants, so that we no longer need to
      *    create fresh nodes for them on each re-render;
      * 2. Completely skip them in the patching process.
      */
      export function optimize(root: ?ASTElement, options: CompilerOptions) {
          ...
          // first pass: mark all non-static nodes.
          markStatic(root)
          // second pass: mark static roots.
          markStaticRoots(root, false)
      }
      

      optimize主要就是做了两件事:

      • markStatic:标记静态节点。其源码如下:
      // src/compiler/optimizer.js
      function markStatic(node: ASTNode) {
          node.static = isStatic(node)
          if (node.type === 1) {
              ...
              for (let i = 0, l = node.children.length; i < l; i++) {
                  const child = node.children[i]
                  markStatic(child)
                  if (!child.static) {
                      node.static = false
                  }
              }
              if (node.ifConditions) {
                  for (let i = 1, l = node.ifConditions.length; i < l; i++) {
                      const block = node.ifConditions[i].block
                      markStatic(block)
                      if (!block.static) {
                          node.static = false
                      }
                  }
              }
          }
      }
      

      parse函数解析生成的抽象语法树 AST 中,其元素节点总有三种类型,如下所示:

      type 类型
      1 普通元素
      2 表达式(expression)
      3 文本(text)

      从源码中可以看到,markStatic函数会对当前节点,以及当前节点的子节点和v-if的子节点进行静态节点标记,静态节点的判定标准由函数isStatic判定:

      // src/compiler/optimizer.js
      function isStatic(node: ASTNode): boolean {
          if (node.type === 2) { // expression
              return false
          }
          if (node.type === 3) { // text
              return true
          }
          return !!(node.pre || (
              !node.hasBindings && // no dynamic bindings
              !node.if && !node.for && // not v-if or v-for or v-else
              !isBuiltInTag(node.tag) && // not a built-in
              isPlatformReservedTag(node.tag) && // not a component
              !isDirectChildOfTemplateFor(node) &&
              Object.keys(node).every(isStaticKey)
          ))
      }
      

      可以看到,静态节点的判定标准为:纯文本类型 或者 没有动态绑定且没有v-if且没有v-for指令且不是内置slot/component且是平台保留标签且不是带有v-for指令的template标签的直接子节点且节点的所有属性都是静态key
      当满足静态节点的判定时,就会为该节点打上static=true属性,作为标记。

      • markStaticRoots:标记静态根节点。其源码如下:
      // src/compiler/optimizer.js
      function markStaticRoots(node: ASTNode, isInFor: boolean) {
          if (node.type === 1) {
              ...
              // For a node to qualify as a static root, it should have children that
              // are not just static text. Otherwise the cost of hoisting out will
              // outweigh the benefits and it's better off to just always render it fresh.
              if (node.static && node.children.length && !(
                  node.children.length === 1 &&
                  node.children[0].type === 3
              )) {
                  node.staticRoot = true
                  return
              } else {
                  node.staticRoot = false
              }
              if (node.children) {
                  for (let i = 0, l = node.children.length; i < l; i++) {
                      markStaticRoots(node.children[i], isInFor || !!node.for)
                  }
              }
              if (node.ifConditions) {
                  for (let i = 1, l = node.ifConditions.length; i < l; i++) {
                      markStaticRoots(node.ifConditions[i].block, isInFor)
                  }
              }
          }
      }
      

      根节点即为普通元素节点,从源码中可以看到,markStaticRoots函数会对当前节点,以及当前节点的子节点和带有v-if的子节点进行静态根节点标记。
      静态根节点的判定标准为:节点为静态节点,且其有子节点,并且子节点不能只是一个文本节点。
      当满足静态根节点的判定时,就会为该节点打上staticRoot=true属性,作为标记。

      因此,Vue 对模板解析生成的 AST 的优化就是对 AST 元素节点进行静态节点和静态根节点的标记,以避免重新渲染静态节点元素。

    • 生成代码:当对 AST 进行优化后,编译的最后一步就是将优化过后的 AST 树转换成可执行代码。

      我们进入generate函数,查看其源码:

      // src/compiler/codegen/index.js
      export function generate(
          ast: ASTElement | void,
          options: CompilerOptions
      ): CodegenResult {
          const state = new CodegenState(options)
          const code = ast ? genElement(ast, state) : '_c("div")'
          return {
              render: `with(this){return ${code}}`,
              staticRenderFns: state.staticRenderFns
          }
      }
      

      generate函数内部主要就是调用了函数genElement对 AST 树进行解析并生成对应可执行代码,最后返回一个对象,该对象含有两个函数renderstate.staticRenderFns,其中,render函数会封装一下genElement函数生成的代码。

      我们下面对genElement函数进行分析,看下其代码生成逻辑:

      // src/compiler/codegen/index.js
      export function genElement(el: ASTElement, state: CodegenState): string {
          ...
          if (el.staticRoot && !el.staticProcessed) {
              return genStatic(el, state)
          } else if (el.once && !el.onceProcessed) {
              return genOnce(el, state)
          } else if (el.for && !el.forProcessed) {
              return genFor(el, state)
          } else if (el.if && !el.ifProcessed) {
              return genIf(el, state)
          } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
              return genChildren(el, state) || 'void 0'
          } else if (el.tag === 'slot') {
              return genSlot(el, state)
          } else {
              // component or element
              let code
              if (el.component) {
                  code = genComponent(el.component, el, state)
              } else {
                  let data
                  if (!el.plain || (el.pre && state.maybeComponent(el))) {
                      data = genData(el, state)
                  }
      
                  const children = el.inlineTemplate ? null : genChildren(el, state, true)
                  ...
              }
              ...
              return code
          }
      }
      

      可以看到,genElement函数内部会依据 AST 树的节点类别分别调用不同的函数生成对应的代码,比如:

      • 对于静态根节点,使用genStatic函数进行代码生成。
      • 对于带有v-once指令的节点,使用genOnce函数进行代码生成。
      • 对于带有v-for指令的节点,使用genFor函数进行代码生成。
      • 对于带有v-if指令的节点,使用genIf函数进行代码生成。
      • 对于不带slot指令的template标签,会使用genChildren函数遍历其子节点并进行代码生成。
      • 对于slot标签,使用genSlot函数进行代码生成。
      • 对于组件或元素,使用genComponent等函数进行代码生成。
        ...

      我们这里就随便找一个函数简单分析下代码生成的具体逻辑,比如:genStatic,其源码如下所示:

      // src/compiler/codegen/index.js
      function genStatic(el: ASTElement, state: CodegenState): string {
          ...
          state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
          ...
          return `_m(${
              state.staticRenderFns.length - 1
              }${
              el.staticInFor ? ',true' : ''
              })`
      }
      

      其实最终就是使用字符串拼接将对应元素的内容转换为字符串代码。

      举个栗子:比如我们构造一个静态根节点的模板,如下所示:

      template: `
          <div>
              <h2 style="color:red">Hi</h2>
          </div>
      `
      

      最后,经过genStatic后,最终生成的代码为:_m(0)
      所以上述模板最终经过generate函数后,生成的代码如下:

      {
          "render": "with(this){return _m(0)}",
          "staticRenderFns": [
                  "with(this){return _c('div',[_c('h2',{staticStyle:{\"color\":\"red\"}},[_v(\"Hi\")])])}"
              ]
      }
      

      _m函数其实是renderStatic函数,Vue 中还设置了_o_l_v等函数,如下所示:

      // src/core/instance/render-helpers/index.js
      export function installRenderHelpers (target: any) {
        target._o = markOnce
        target._n = toNumber
        target._s = toString
        target._l = renderList
        target._t = renderSlot
        target._q = looseEqual
        target._i = looseIndexOf
        target._m = renderStatic
        target._f = resolveFilter
        target._k = checkKeyCodes
        target._b = bindObjectProps
        target._v = createTextVNode
        target._e = createEmptyVNode
        target._u = resolveScopedSlots
        target._g = bindObjectListeners
        target._d = bindDynamicKeys
        target._p = prependModifier
      }
      

      其余的代码生成函数就不进行分析了,大概的原理就是根据不同节点特征,在 AST 树中获取需要的数据,拼接成可执行代码的字符串代码。

    到这里,模板编译的一个大概完整过程便完成了。

    参考

    相关文章

      网友评论

          本文标题:Vue 源码解析 - 模板编译

          本文链接:https://www.haomeiwen.com/subject/ybcvmhtx.html