1. 程式人生 > 其它 >vue原始碼學習-render函式

vue原始碼學習-render函式

render函式

編譯過程-模板編譯成render函式

通過文章前半段的學習,我們對Vue的掛載流程有了一個初略的認識,接下來將先從模板編譯的過程展開,閱讀原始碼時發現,模板編譯的過程也是相當複雜的,要在短篇幅內將整個編譯過程姜凱是不切實際的,這裡只對實現思路做簡單的介紹。

template的三種寫法

template模板的編寫有三種方式,分別是:

// 1. 熟悉的字串模板
const vm = new Vue({
  el:'#app',
  template: '<div>模板字串</div>'
})
// 2. 選擇符匹配元素的innerHTML模板
<div id="app">
  <div>test1</div>
   <script type="x-template" id="test">
     <p>test</p>
   </script>
</div>
const vm = new Vue({
  el: '#app',
  template: '#test'
})
// 3. dom元素匹配的innerHTML模板
<div id="app">
 <div>test1</div>
 <span id="test"><div class="test2">test2</div></span>
</div>
var vm = new Vue({
 el: '#app',
 template: document.querySelector('#test')
})

三種寫法對應程式碼的三個不同的分支

var template = opitons.template
if(template) {
  // 針對字串模板和選擇符匹配模板
  if(typeof template === 'string') {
    // 選擇符匹配模板
    if(template.charAt(0) === '#') {
      // 獲取匹配元素的innerHTML
      template = idToTemplate(template)
      if(!template) {
         warn(
        ("Template element not found or is empty: " +       (options.template)),
        this
        );
      }
    } 
  // 針對dom元素匹配
  } else if (template.nodeType) {
    // 獲取匹配元素的innerHTML
    template = template.innerHTML
  }else {
    // 其他型別則判定為非法傳入
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
} else if(el) {
  // 如果沒有傳入template模板,則預設el元素所屬的根節點作為基礎模板
  template = getOuterHTML(el)
}

其中X-Template模板方式一般用於模板特別大的demo或極小型的應用,官方不建議在其他情形下使用,因為這會將模板和元件的其他定義分離開。

流程圖解

Vue原始碼中編譯流程比較繞,設計的函式處理邏輯比較多,實現流程中巧妙的運用了偏函式的技巧將配置項處理和編譯核心邏輯抽取出來,為了理解這個設計思路,下圖可幫助理解

邏輯解析

即便有流程圖,編譯邏輯理解起來依然比較晦澀,接下來,結合程式碼法系每個環節的執行過程

var ref = compileToFunctions(template, {
 outputSourceRange: "development" !== 'production',
 shouldDecodeNewlines: shouldDecodeNewlines,
 shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
 delimiters: options.delimiters,
 comments: options.comments
}, this);
 
// 將compileToFunction方法暴露給Vue作為靜態方法存在
Vue.compile = compileToFunctions;

這是編譯的入口,也是Vue對外暴露的編譯方法。compileToFunction需要傳遞三個引數:template模板,編譯配置選項以及Vue例項。我們先大致瞭解一下配置中的幾個預設選項

  1. delimiters:該選項可以改變純文字插入分隔符,當不傳遞值時,Vue預設的分隔符為{{}},使用者可通過該選項修改
  2. comments當設為true時,將會保留且渲染模板中的HTML註釋。預設行為是捨棄它們。
接著一步步尋找compileToFunctions根源
var createCompiler = createCompilerCreator(function baseCompile (template,options) {
 //把模板解析成抽象的語法樹
 var ast = parse(template.trim(), options);
 // 配置中有程式碼優化選項則會對Ast語法樹進行優化
 if (options.optimize !== false) {
 optimize(ast, options);
 }
 var code = generate(ast, options);
 return {
 ast: ast,
 render: code.render,
 staticRenderFns: code.staticRenderFns
 }
});

createCompilerCreator角色定位為建立編譯器的建立者,它傳遞了一個基礎的編譯器baseCompile作為引數,baseCompile是真正執行編譯功能的地方,它傳遞template模板和基礎的配置選項作為引數,實現的功能有兩個

  1. 吧模板解析成抽象的語法樹,簡稱AST,程式碼中對應parse部分
  2. 可選:優化AST語法樹,執行optimize方法
  3. 根據不同平臺將AST語法樹生成需要的程式碼,對應generate函式

具體看看createCompilerCreator的實現方式。

function createCompilerCreator (baseCompile) {
 return function createCompiler (baseOptions) {
 // 內部定義compile方法
 function compile (template, options) {
 ···
 // 將剔除空格後的模板以及合併選項後的配置作為引數傳遞給baseCompile方法,其中finalOptions為baseOptions和使用者options的合併
 var compiled = baseCompile(template.trim(), finalOptions);
 {
  detectErrors(compiled.ast, warn);
 }
 compiled.errors = errors;
 compiled.tips = tips;
 return compiled
 }
 return {
 compile: compile,
 compileToFunctions: createCompileToFunctionFn(compile)
 }
 }
 }

createCompilerCreator函式只有一個作用,利用偏函式將baseCompile基礎編譯方法快取,並返回一個編譯器函式。該函式內部定義了真正執行編譯的compile方法,並最終將compile和compileToFunction作為兩個物件屬性返回。這也是compileToFunction的來源。而內部compile的作用,是為了將基礎的配置baseOpitons和使用者自定義的配置options進行合併,baseOptions是跟外部平臺相關的配置,最終返回併合並配置後的baseCompile編譯方法。

compileToFunctions 來源於 createCompileToFunctionFn 函式的返回值,該函式會將編譯的方法 compile 作為引數傳入。

function createCompileToFunctionFn (compile) {
 var cache = Object.create(null);
 
 return function compileToFunctions (template,options,vm) {
 options = extend({}, options);
 ···
 // 快取的作用:避免重複編譯同個模板造成效能的浪費
 if (cache[key]) {
 return cache[key]
 }
 // 執行編譯方法
 var compiled = compile(template, options);
 ···
 // turn code into functions
 var res = {};
 var fnGenErrors = [];
 // 編譯出的函式體字串作為引數傳遞給createFunction,返回最終的render函式
 res.render = createFunction(compiled.render, fnGenErrors);
 // 渲染優化相關
 res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
 return createFunction(code, fnGenErrors)
 });
 ···
 return (cache[key] = res)
 }
 }

最終我們找到了compileToFunction真正的執行過程var compiled = compile(template, options);,並將編譯後的函式體字串通過 creatFunction 轉化為 render 函式返回。

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

其中函式體字串類似於with(this){return _m(0)},最終的render渲染函式為 function(){with(this){return _m(0)}}