vue源碼學習之模板編譯

模板編譯

模板編譯的主要目的是將模板(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()方法中查看)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章