浅曦Vue源码-14-挂载阶段-$mount(2)

491 阅读6分钟

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

一、前情回顾 & 背景

上一篇小作文介绍讲解了一下 $mount 方法的注册以及模板编译函数—— compileToFunctions

编译模板的函数 compieToFunctions 是个套娃,他是 createCompiler 方法的返回值,而 createCompiler 方法又是 createCompilerCreator 函数的返回值,createCompileToFunction 作为 createCompiler 返回值的一个属性,值是 createCompileToFunction(compile)

本篇小作文的重点在于创建模板编译函数 compileToFunctions 的过程中都做了什么,在这个过程中我们可以看到模板编译的全过程。

这个过程堪称整个 Vue 源码中最复杂的部分,如果一遍搞不定,坚持在来一遍。说句实在话,我也是看了两遍之后才开始动笔写这篇小作文,写的过程中还需要重新梳理这个过程,既是复习也是验证自己的理解是否正确。

image.png

二、createCompileToFunctions

方法位置:src/compiler/to-function.js -> createCompileToFunctionFn

方法参数:compile 方法,这个方法是在 createCompiler 方法中声明,最后在调用 createCompileToFunctions 时传入的

方法作用:

创建 compileToFunctions 并缓存,对,就是前面那个 $mount 方法中用于编译模板的方法。

export function createCompileToFunctionFn (compile: Function): Function {
  const cache = Object.create(null)
  
  return function compileToFunctions (
    template: string, // template 模板,就是 $mount 处理的
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
  }
}

三、compileToFunctions

createCompileToFunctionFn 的返回值

方法位置:src/compiler/to-function.js -> createCompileToFunctionFn -> return function compileToFunctions

方法参数:

  1. template:需要编译的模板字符串
  2. options:传递给 compile 方法的编译选项;这个参数是在 $mount 方法中调用传入的;
// $mount 代码片段
 compileToFunctions(template, {
  outputSourceRange: process.env.NODE_ENV !== 'production', // 在非生产环境下,编译生成 ast 时记录 html 标签属性在模板字符串中开始和结束的位置索引
  shouldDecodeNewlines,
  shouldDecodeNewlinesForHref,
  delimiters: options.delimiters, // 界定符:默认 mustache 语法的 {{}}
  comments: options.comments // 是否保留注释
})
  1. vmVue 实例

方法作用:

  1. 处理 optionsextend 到一个空对象,就是个复制操作;
  2. 开发环境监测 CSP 限制,CSP 是内容安全策略;
  3. 检查缓存,如果本次编译命中缓存就使用缓存;
  4. 执行 createCompileToFunctionFncreateCompiler 方法传入的 compile 函数;
  5. 处理编译报错并输出;
  6. 把编译得到的 renderstaticRenderFns 通过 new Function() 变成函数;
  7. 缓存编译结果;
export function createCompileToFunctionFn (compile: Function): Function {
  const cache = Object.create(null)
  
  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    // 上面参数 options 有说是调用 compileToFunctions 时传入的
    options = extend({}, options) 
    const warn = options.warn || baseWarn
    delete options.warn

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production') {
      // 检测 CSP 限制, CSP 会影响编译器工作,提前检测
      // detect possible CSP restriction
      try {
        new Function('return 1')
      } catch (e) {
        if (e.toString().match(/unsafe-eval|CSP/)) {
          
        }
      }
    }

    // 如果命中缓存,直接从缓存中获取上次编译结果
    // key 是 delimiters + template, +是字符串拼接
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

    // 执行编译方法,编译结果赋值给 compiled
    // 这个 compile 是 createCompilerCreator 调用 createCompileToFunctionsFn 方法时传入的回调,在 src/compiler/create-compiler.js
    const compiled = compile(template, options) 
    
    // 检查编译期间产生的 error 和 tip,分别输出到 console
    if (process.env.NODE_ENV !== 'production') {
    }

  
    // 通过 new Function(code),把编译得到的字符串代码变成函数
    const res = {}
    const fnGenErrors = []
    res.render = createFunction(compiled.render, fnGenErrors)
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })

   
    // 处理上面代码转换过程中的错误
    if (process.env.NODE_ENV !== 'production') {
     
    }

    // 缓存编译结果
    return (cache[key] = res)
  }
}

3.1 createCompiler 中的 compile

说 compile 之前先看下 createCompiler 的代码,看下 compile 的声明位置

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
   
    // 这个就是你朝思暮想的 compile 了
    function compile (template, options) {}

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

方法位置:compilecreateCompileToFunctions 方法在 createCompiler 方法调用时传入的方法,compile 本身也是在 createCompiler 声明;

方法参数:

  1. template: 模板字符串
  2. options: 创建编译器所需参数,指定诸如 delimiters

方法作用: 合并 baseOptions,调用 baseCompile 编译模板,返回编译结果;

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
  
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      // 创建一个以 baseOptions 为原型的对象,相当于继承 baseOptions 
      const finalOptions = Object.create(baseOptions)
      const errors = []
      const tips = []

      // 日志,负责记录将 error 和 tip
      let warn = (msg, range, tip) => {
        (tip ? tips : errors).push(msg)
      }

      // 传了 options 就合并 options 和 baseOptions
      if (options) {
        if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
         
          const leadingSpaceLength = template.match(/^\s*/)[0].length
          
          // 抛出劲警告的方法
          warn = (msg, range, tip) => {}
        }
        
        // 合并自 module
        if (options.modules) {
          finalOptions.modules =
            (baseOptions.modules || []).concat(options.modules)
        }

        // 合并指令
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives || null),
            options.directives
          )
        }

        // 复制其他选项到 finalOptions
        for (const key in options) {
          if (key !== 'modules' && key !== 'directives') {
            finalOptions[key] = options[key]
          }
        }
      }

      finalOptions.warn = warn

      // 调用 baseCompile 
      const compiled = baseCompile(template.trim(), finalOptions)
      if (process.env.NODE_ENV !== 'production') {
        detectErrors(compiled.ast, warn)
      }

      // 将编译期间产生的错误和提示挂载到编译结果上
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

在这里,我们暂时不管 options.modules/directives,在我们的 demo 中,初次渲染 options 中的 modulesdirectives 都是 undefined

紧接着这里又调用了 baseCompile,那么 baseCompile 又是哪里来的呢?

3.2 createCompileCreator 接收到的 baseCompile

方法位置:baseCompile 是作为参数传递给 createCompilerCreator 方法的:

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 这个函数就是 baseCompile
})

前面我们说过 createCompilerCreator 返回的函数就是 createCompiler 函数,createCompiler 调用就会返回 { compile, compileToFunctions }

方法参数:

  1. template,需要编译的模板字符串
  2. 编译过程中所需要的配置项目

接下来我们看看 baseCompile 都做了什么工作:

  1. 调用 parse 方法把编译模板编译成 AST 节点。每个 AST 节点包含了一个模板标记的所有信息,比如标签名字、属性、插槽、父节点、子节点
  2. options.optimize 不为 false 则调用 parse 方法进行静态节点的优化工作;
  3. 调用 generate 方法将 AST 变成 render/staticRenderFns 的字符串形式代码,这就是 render/staticRenderFns 函数的前身了;
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 将模板解析为 ast,ast 对象上都设置节点上的所有信息
  // 比如,标签信息,属性信息,插槽信息,父节点,子节点等
  
  const ast = parse(template.trim(), options)
  // 遍历 ast,标记每个节点是否为静态节点,以便进一步标记出静态根节点
  // 这样在后续更新中就不更新这些静态节点了,算是提升性能的一种重要方式
  if (options.optimize !== false) {
    optimize(ast, options)
  }

  // generate 代码生成,将 ast 转换成可执行的 render 函数的字符串形式:

  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

3.2.1 ast 对象

ast 对象就是对我们模板的一种抽象描述,用一个对象描述节点上的所有信息,比如标签、标签的行内属性、子节点、父节点;比如我们 test.html 中的模板文件长这样:

<div id="app">
 {{ msg }}
 <some-com :some-key="forProp"></some-com>
 <div>someComputed = {{someComputed}}</div>
 <div class="static-div">静态节点</div>
</div>

编译出来的 ast 长这样: image.png

3.2.2 把 ast 转换成 code

baseCompileparsegenerate 后得到的 code 是一种字符串形式的 js 函数体:

image.png

为了让大家感受的更直观一些,下面这个是经过手动格式化的 code.render

  code.render = with (this) {
    return _c( // <div id=app>
      'div',
      { attrs: { 'id': 'app' } }, // div 上的属性 id = app
      [ // 这个数组就是 div#app 的子元素了
        _v('\n\t' + _s(msg) + '\n\t'), // 第一个子元素 
        _c( // 第二个子元素 <some-com />
          'some-com',
          { attrs: { 'some-key': forProp } } // some-com 的 属性
         ),
        _v(' '), // 第三个子元素
        _c( // 第四个子元素
          'div',
          [_v('someComputed = ' + _s(someComputed))]
        ),
        _v(' '), // 第五个
        _c( // 第六个
          'div',
          { staticClass: 'static-div' },
          [_v('静态节点')]
        )
      ],
      1
    )}
})

参照上面的 html 中的模板,可以看到先到 ast 再将 ast 变成一个 render 函数的代码体。无论是 ast 还是 render 函数都是在描述 html 模板。

四、总结

本文细致的梳理 compileToFunctions 方法的获取过程,另外简单的交代了 test.html 模板生成的 astgenerate ast 后得到的 code.renderrender 函数体,注意我这里说的是函数体并不是 render 函数 这是因为现在它还是一坨字符串,经过后面的 new Function 才是函数哦;

很多人写 Vue 源码的时候都没有写这个套娃部分,我试着写这部分是为了理解到这么设计的好处,但是很显然没有悟道,这个坑先不填了,等我再看看,懂了在补上吧;所以如果你觉得这部分套娃复杂就不用管了,先看看 compilebaseCompile 做了什么就可以了。

当然,如果你发现这个妙处,也请不吝赐教评论区告诉我,万分感谢~

五、新年快乐