Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

渲染之compiler - 11 #11

Open
any-u opened this issue Mar 8, 2021 · 0 comments
Open

渲染之compiler - 11 #11

any-u opened this issue Mar 8, 2021 · 0 comments

Comments

@any-u
Copy link
Owner

any-u commented Mar 8, 2021

渲染之compiler - 11

前言

上文我们探讨 render 的过程,render 函数来源于 Vue.prototype.$mount

const { render, staticRenderFns } = compileToFunctions(template, {
  outputSourceRange: process.env.NODE_ENV !== 'production',
  shouldDecodeNewlines,
  shouldDecodeNewlinesForHref,
  delimiters: options.delimiters,
  comments: options.comments
}, this)

这里调用了compileToFunctions函数,接下来我们来分析compileToFunctions函数的代码

compileToFunctions

const { compile, compileToFunctions } = createCompiler(baseOptions)

调用了createCompiler,传入了 baseOptions,返回一个对象,内含 compilecompileToFunctions,接下来我们先看看 baseOptions

export const baseOptions: CompilerOptions = {
  expectHTML: true,
  modules,
  directives,
  isPreTag,
  isUnaryTag,
  mustUseProp,
  canBeLeftOpenTag,
  isReservedTag,
  getTagNamespace,
  staticKeys: genStaticKeys(modules)
}

baseOptions 是一个对象,内含很多解析方法,具体方法我们后续分析。这里传入baseOptions来构造编译函数,通过传入编译函数来构建编译函数,可以轻松实现跨平台性。倘若我们需要支持某个新平台,只需要用去实现 baseOptions 里的方法即可。

而对于createCompiler, 它的代码来自src/compiler/index.js

export const createCompiler = createCompilerCreator(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
  }
})

这里调用了函数createCompilerCreator,然后传入了 baseCompile 这个函数,结果赋给了 createCompiler。看起来可能有些混乱,那么先这么看, 把 baseCompile 看成一个函数变量,那这里就变成了

export const createCompiler = createCompilerCreator(baseCompile),那接下来我们先从 createCompilerCreator 入手

createCompilerCreator

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
			// 省略...
    }

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

调用 createCompilerCreator 函数,直接返回了一个新的函数 createCompiler。然后 createCompiler内部新建了一个 compile 函数,然后返回了一个对象,这个对象就包含了前文所需要的 compilecompileToFunction

compile

compile 函数的源码如下:

function compile (
	template: string,
 	options?: CompilerOptions
): CompiledResult {
  const finalOptions = Object.create(baseOptions)
  const errors = []
  const tips = []

  let warn = (msg, range, tip) => {
    (tip ? tips : errors).push(msg)
  }

  if (options) {
    if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
      // $flow-disable-line
      const leadingSpaceLength = template.match(/^\s*/)[0].length

      warn = (msg, range, tip) => {
        const data: WarningMessage = { msg }
        if (range) {
          if (range.start != null) {
            data.start = range.start + leadingSpaceLength
          }
          if (range.end != null) {
            data.end = range.end + leadingSpaceLength
          }
        }
        (tip ? tips : errors).push(data)
      }
    }
    // merge custom modules
    if (options.modules) {
      finalOptions.modules =
        (baseOptions.modules || []).concat(options.modules)
    }
    // merge custom directives
    if (options.directives) {
      finalOptions.directives = extend(
        Object.create(baseOptions.directives || null),
        options.directives
      )
    }
    // copy other options
    for (const key in options) {
      if (key !== 'modules' && key !== 'directives') {
        finalOptions[key] = options[key]
      }
    }
  }

  finalOptions.warn = warn

  const compiled = baseCompile(template.trim(), finalOptions)
  if (process.env.NODE_ENV !== 'production') {
    detectErrors(compiled.ast, warn)
  }
  compiled.errors = errors
  compiled.tips = tips
  return compiled
}

首先这个函数包含两个参数,templateoptionstemplate 指的是模板字符串,而 options 指的编译时的选项。

接下来定义了3 个常量 finalOptions, errors, tips,分别做初始化操作

const finalOptions = Object.create(baseOptions)
const errors = []
const tips = []

然后定一个 warn 函数

let warn = (msg, range, tip) => {
  (tip ? tips : errors).push(msg)
}

接下来是 if 语句块,判断 options 是否存在,存在则执行下面代码。

if (options) {
  if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
    // $flow-disable-line
    const leadingSpaceLength = template.match(/^\s*/)[0].length

    warn = (msg, range, tip) => {
      const data: WarningMessage = { msg }
      if (range) {
        if (range.start != null) {
          data.start = range.start + leadingSpaceLength
        }
        if (range.end != null) {
          data.end = range.end + leadingSpaceLength
        }
      }
      (tip ? tips : errors).push(data)
    }
  }
  // merge custom modules
  if (options.modules) {
    finalOptions.modules =
      (baseOptions.modules || []).concat(options.modules)
  }
  // merge custom directives
  if (options.directives) {
    finalOptions.directives = extend(
      Object.create(baseOptions.directives || null),
      options.directives
    )
  }
  // copy other options
  for (const key in options) {
    if (key !== 'modules' && key !== 'directives') {
      finalOptions[key] = options[key]
    }
  }
}

首先判断是否为非生产环境,且 outputSourceRange 被设为 true, 如果是的话,重新定义下 warn 函数。这里的作用在于如果解析出现了问题,可以告知错误的位置,详见Issue

然后判断 options.modules 是否存在,存在则合并 modules

// merge custom modules
if (options.modules) {
  finalOptions.modules =
    (baseOptions.modules || []).concat(options.modules)
}

接着判断 options.directives 是否存在,存在则继承 directives

if (options.directives) {
  finalOptions.directives = extend(
    Object.create(baseOptions.directives || null),
    options.directives
  )
}

最后拷贝 options 上的其他属性

for (const key in options) {
  if (key !== 'modules' && key !== 'directives') {
    finalOptions[key] = options[key]
  }
}

处理完 if 语句块后,接下来把 warn 函数赋给最终的选项 finalOptions.

finalOptions.warn = warn

所以总的来说,这一段就是对 options 的处理操作。

处理完 options 后,接下来是这样一段代码

const compiled = baseCompile(template.trim(), finalOptions)
if (process.env.NODE_ENV !== 'production') {
  detectErrors(compiled.ast, warn)
}
compiled.errors = errors
compiled.tips = tips
return compiled

首先调用 baseCompile 来生成编译后的结果,接着判断是否为非正式环境,如果不是,就检测错误并打印相应的警告。接着把 errorstips 赋给 compiled, 并返回它。

到此,compile 函数的逻辑就说完了,而 **compileToFunctions **是 createCompileToFunctionFn(compile) 的结果,故接下来我们来看看 createCompileToFunctionFn

createCompileToFunctionFn

export function createCompileToFunctionFn (compile: Function): Function {
  const cache = Object.create(null)

  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    // 省略...
  }
}

首先定义了一个常量 cache, 然后把 Object.create(null) 的值赋给它。接着返回 compileToFunctions 函数。对于 compileToFunctions, 首先我们来看它的参数,它的调用来自src/platforms/web/entry-runtime-with-compiler.js中的Vue.prototype.$mount

const { render, staticRenderFns } = compileToFunctions(template, {
  outputSourceRange: process.env.NODE_ENV !== 'production',
  shouldDecodeNewlines,
  shouldDecodeNewlinesForHref,
  delimiters: options.delimiters,
  comments: options.comments
}, this)

故这里的 template 就是处理过的模板字符串,options 也就是这里的 options 对象, 而 vm 也就是这里的 this, 即Vue实例。说完参数,接下来就来看看具体的内部实现。

options = extend({}, options)
const warn = options.warn || baseWarn
delete options.warn

首先通过 extend 将选项参数混合到一个新的对象中,然后将值赋给 options. 接着定义一个常量 warn, 其值为 options.warnbaseWarn,如果 options.warn 不存在,则使用 baseWarn 。然后删除 options 上的 warn

接下来是检查缓存相关代码:

const key = options.delimiters
	? String(options.delimiters) + template
	: template
if (cache[key]) {
  return cache[key]
}

首先判断 options 中的 delimiters,如果存在,则使用 String 方法将其转成字符串并与template拼接来作为 key 的值,否则直接将 template 作为 key 的值。然后判断 cache[key] 是否存在,如果存在直接返回 cache[key]。这么做的目的是缓存字符串模板的编译结果,防止重复编译,提升性能。

delimiters作为纯文本插入分隔符,如['${', '}'], 详见delimiters

接下来是这里的重心代码:

// compile
const compiled = compile(template, options)

调用上面的 compile 函数生成编译后的结果,具体过程后续分析。

再往下是这样一段代码:

const res = {}
const fnGenErrors = []
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
  return createFunction(code, fnGenErrors)
})

首先定义两个常量 resfnGenErrorsres 用于作为 compileToFunctions 的结果,fnGenErrors 用于保存生成渲染函数过程中的错误。

接下来是调用 createFunction 函数,代码如下:

function createFunction (code, errors) {
  try {
    return new Function(code)
  } catch (err) {
    errors.push({ err, code })
    return noop
  }
}

这里将调用 new Function ,将 compiled.render 形成一个新函数,然后用 fnGenErrors 保存渲染过程中的错误。最后将这个新函数赋给 res.render。同理处理下 res.staticRenderFns

接着判断是否为非正式环境,如果是非正式环境且存在错误,则在控制台打印相应的错误信息。

最后执行这样一段代码。

return (cache[key] = res)

这里把 res 赋值给cache[key], 用来缓存编译后的结果,下次如果重复编译,则直接返回来提高性能。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant