vue源码学习之模板编译

36 篇文章 2 订阅

模板编译

模板编译的主要目的是将模板(template)转换为渲染函数(render)
在这里插入图片描述

vue中使用模板编译的必要性

Vue 2.0需要用到VNode描述视图以及各种交互,手写显然不切实际,因此用户只需编写类似HTML代码的Vue模板,通过编译器将模板转换为可返回VNode的render函数。

模板编译

带编译器的版本中,可以使用template或el的方式声明模板。

<div id="app"> 
	<h1>Vue模板编译</h1>
	<p>{{name}}</p> 
	<Com1></Com1> 
</div> 
<script>
// 1.声明Com1组件构造函数VueComponent
// 2.全局配置选项加上了一个components:{Com1} 
	Vue.component('Com1', { template: '<div>component</div>' })
	const app = new Vue({ el: '#app', data: {name:'lau'} }); // 创建实例 
	console.log(app.$options.render); // 输出render函数 
</script>
// 输出
ƒ anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[
	_c('h1',[_v("Vue模板编译")]),_v(" "),
	_c('p',[_v(_s(name))]),_v(" "),
	_c('com1')],1)}
}

_c返回vnode,就是createElement
_v 创建文本节点
_s 格式化函数
其他的helpers:src/core/instance/render-helper/index.js

模板编译流程

compileToFunctions()
src/platforms/web/entry-runtime-with-compiler.js
在$mount方法中会判断是否存在template或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
}

compileToFunctions()createCompiler(baseOptions)这个工厂函数的返回结果

// src/platforms/web/compiler/index.js
const { compile, compileToFunctions } = createCompiler(baseOptions)
编译过程

src/compiler/index.js

export const createCompiler = createCompilerCreator(function baseCompile ( // 创建一个编译器
  template: string,
  options: CompilerOptions
): CompiledResult {
  // parse作用是将字符串模板编译成AST(抽象语法树)
  // AST 就是js对象,类似VNODE
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options) // 添加static,staticRoot,用于判断是否是静态(不含data或指令)的,即要不要更新
    // 优化 减少更新耗时
    // 虚拟DOM中的patch,可以跳过静态子树
  }
  // 代码生成,AST转换成代码字符串'function(){}'
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

从上面的代码可以了解到模板编译的过程共有三个阶段:解析、优化和生成。

parse()- 解析

解析器将模板解析为抽象语法树AST,只有将模板解析成AST后,才能基于它做优化或者生成代码字符串。
在这里插入图片描述
解析器内部分了HTML解析器、文本解析器和过滤器解析器,最主要是HTML解析器,核心算法说明:
src/compiler/parser/index.js

// 解析html是parse的关键,在这个过程中会用到parseText和parseFilter
parseHTML(tempalte, { 
	start(tag, attrs, unary){
		...
		// 结构性指令的处理 v-if,v-for等
		processFor(element)
        processIf(element)
        processOnce(element)
		...
	}, // 遇到开始标签的处理 
	end(){},// 遇到结束标签的处理 
	chars(text){},// 遇到文本标签的处理 
	comment(text){}// 遇到注释标签的处理 
})
optimize()- 优化

优化器的作用是在AST中找出静态子树并打上标记。静态子树是在AST中永远不变的节点,如纯文本节点。

标记静态子树的好处:

  • 每次重新渲染,不需要为静态子树创建新节点
  • 虚拟DOM中patch时,可以跳过静态子树

src/compiler/optimizer.js

export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // first pass: mark all non-static nodes.
  // 标记静态属性static
  markStatic(root)
  // second pass: mark static roots.
  // 标记根节点静态属性staticRoot
  markStaticRoots(root, false)
}
generate() - 代码生成

将AST转换成渲染函数中的内容(代码字符串)
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
  }
}

此时生成的render函数是字符串形式,需改成function,执行createCompileToFunctionFn()
src/compiler/to-function.js

export function createCompileToFunctionFn (compile: Function): Function {
  ...
  res.render = createFunction(compiled.render, fnGenErrors)
  ...
}

createFunction()就是直接new了一个Function

function createFunction (code, errors) {
  try {
    return new Function(code)
  } catch (err) {
    errors.push({ err, code })
    return noop
  }
}
v-if指令的实现
<div id="app"> 
    <h1>Vue模板编译</h1>
    <p v-if="name">{{name}}</p> 
    <Com1></Com1> 
  </div> 

在这里插入图片描述
生成的AST中就会存在上图两个属性。
parse()阶段,processIf(element)中为ast新增这两个属性。

function processIf (el) {
  const exp = getAndRemoveAttr(el, 'v-if')
  if (exp) {
    el.if = exp // 添加了if属性
    addIfCondition(el, { // 添加了ifCondition属性
      exp: exp,
      block: el
    })
  } else {
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}

把断点点在src/compiler/index.js中的

const code = generate(ast, options)

可以看到生成的代码字符串为:

"with(this){return _c('div',{attrs:{"id":"app"}},[_c('h1',[_v("Vue模板编译")]),_v(" "),
	(name)?_c('p',[_v(_s(name))]):_e(),_v(" "), // 可以看到v-if对render函数的影响只是多了个三元表达式的判断(if-else就是三元表达式的嵌套),如果表达式name存在,就创建这个节点,否则创建空节点
	_c('com1')],1)}"

这个v-if代码是在genIf()这个函数中生成
genIf()
src/compiler/codegen/index.js

export function genIf (){
  el.ifProcessed = true // avoid recursion
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
function genIfConditions (
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  if (!conditions.length) {
    return altEmpty || '_e()'
  }
  const condition = conditions.shift()
  if (condition.exp) { // 如果存在表达式,返回一个字符串
    return `(${condition.exp})?${
      genTernaryExp(condition.block) // 嵌套
    }:${
      genIfConditions(conditions, state, altGen, altEmpty)
    }`
  } else {
    return `${genTernaryExp(condition.block)}`
  }

  // v-if with v-once should generate code like (a)?_m(0):_m(1)
  function genTernaryExp (el) {
    return altGen
      ? altGen(el, state)
      : el.once
        ? genOnce(el, state)
        : genElement(el, state)
  }
}

研究指令,看ast最终怎么受影响,其次就是对代码生成的影响(在src/compiler/codegen/index.js中的generate()方法中查看)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值