Vue 2.0 源碼分析(三) 基礎篇 模板渲染 el、emplate、render屬性詳解

用法css


 Vue有三個屬性和模板有關,官網上是這樣解釋的:html

el     ;提供一個在頁面上已存在的 DOM 元素做爲 Vue 實例的掛載目標vue

template  ;一個字符串模板做爲 Vue 實例的標識使用。模板將會 替換 掛載的元素。掛載元素的內容都將被忽略,除非模板的內容有分發插槽。node

render    ;字符串模板的代替方案,容許你發揮 JavaScript 最大的編程能力。該渲染函數接收一個 createElement 方法做爲第一個參數用來建立 VNode編程

 

簡單說一下,就是:數組

  Vue內部會判斷若是沒有render屬性則把template屬性的值做爲模板,若是template不存在則把el對應的DOM節點的outerHTML屬性做爲模板,通過一系列正則解析和流程生成一個render函數,最後經過with(this){}來執行。瀏覽器

  也就是說template的優先級大於el。app

  render的參數是Vue內部的$createElement函數(位於4486行),它的可擴展性更強一些,在一些項目的需求中,能夠用很簡單的代碼獲得一個模板。例如Vue實戰9.3裏介紹的例子,有興趣能夠看看函數

render能夠帶3個參數,分別以下:源碼分析

    tag          ;元素的標籤名,也能夠是組件名
    data        ;該VNode的屬性,是個對象
    children        ;子節點,是個數組
其中參數2能夠省略的,在4335行作了修正,最後執行_createElement()函數,以下:

function createElement (  //第4335行
  context,
  tag,
  data,
  children,
  normalizationType,
  alwaysNormalize
) {
  if (Array.isArray(data) || isPrimitive(data)) {     //若是data是個數組或者是基本類型
    normalizationType = children;
    children = data;                                      //修正data爲children
    data = undefined;                                     //修正data爲undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE;
  }
  return _createElement(context, tag, data, children, normalizationType)     //最後執行_createElement建立一個虛擬VNode
}

 

例以下面三個Vue實例,分別用el、template和rentder指定模板,它們的輸出是同樣的

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <script src="https://cdn.bootcss.com/vue/2.5.16/vue.js"></script>
    <title>Document</title>
</head>
<body>
    <div id="app1">{{message}}</div>
    <div id="app2"></div>
    <div id="app3"></div>
    <script>
        var data={message:'you are so annoying'}
        new Vue({el:'#app1',data})                                                          //用el作模板
        new Vue({el:'#app2',data,template:"<div>{{message}}</div>"})                        //用template作模板
        new Vue({el:'#app3',data,render:function(h){return h('div',this.message)}})         //直接用render函數指定模板
    </script>        
</body>
</html>

 

、瀏覽器顯示結果:

能夠看到輸出是一摸同樣的

 

  源碼分析

Vue實例後會先執行_init()進行初始化,快結束時會判斷是否有el屬性,若是存在則調用$mount進行掛載,$mount函數以下:

Vue.prototype.$mount = function (    //定義在10861行
  el,
  hydrating
) {
  el = el && query(el);

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    "development" !== 'production' && warn(
      "Do not mount Vue to <html> or <body> - mount to normal elements instead."
    );
    return this
  }

  var options = this.$options;
  // resolve template/el and convert to render function
  if (!options.render) {                                            //若是render屬性不存在
    var template = options.template;                                //則嘗試獲取template屬性並將其編譯成render   
    if (template) {
      if (typeof template === 'string') {                              
        if (template.charAt(0) === '#') {
          template = idToTemplate(template);
          /* istanbul ignore if */
          if ("development" !== 'production' && !template) {
            warn(
              ("Template element not found or is empty: " + (options.template)),
              this
            );
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML;
      } else {
        {
          warn('invalid template option:' + template, this);
        }
        return this
      }
    } else if (el) {                                                //若是templtate不存在可是el存在,則獲取調用getOuterHTML()函數獲取el的outerHTML屬性,getOuterHTML()定義在10933行,也就是末尾,用戶獲取DOM的outerHTML
      template = getOuterHTML(el);
    }
    if (template) {
      /* istanbul ignore if */
      if ("development" !== 'production' && config.performance && mark) {
        mark('compile');
      }

      var ref = compileToFunctions(template, {
        shouldDecodeNewlines: shouldDecodeNewlines,
        shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this);                                                     //這裏調用compileToFunctions()將template解析成一個render函數,並返回
      var render = ref.render;
      var staticRenderFns = ref.staticRenderFns;
      options.render = render;
      options.staticRenderFns = staticRenderFns;

      /* istanbul ignore if */
      if ("development" !== 'production' && config.performance && mark) {
        mark('compile end');
        measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
      }
    }
  }
  return mount.call(this, el, hydrating)
};
compileToFunctions函數是由createCompiler()返回的(這裏有點繞,研究代碼的時候在裏面繞了好幾天),我把大體主體貼出來,以下:
var baseOptions ={}                                                     //編譯的配置項 第9802行
function createCompileToFunctionFn(compile){
  var cache = Object.create(null);  
  return return function compileToFunctions(template, options, vm) {    //編譯時先執行這裏
    /**/
    compile(template,options)       
    /**/
  }
}
function createCompilerCreator(baseCompile){
  return function(baseOptions){
    function compile(template, options) {/**/}
              return {
                  compile: compile,
                  compileToFunctions: createCompileToFunctionFn(compile)    //難點:匿名函數返回的值中又調用了createCompileToFunctionFn函數
              }
  }
}
var createCompiler = createCompilerCreator(function(){                //傳入一個匿名函數
        var ast = parse(template.trim(), options);                          //編譯時,第二步:再執行這裏
        if (options.optimize !== false) {          
              optimize(ast, options);
        }
        var code = generate(ast, options);         
        return {ast: ast,render: code.render,staticRenderFns: code.staticRenderFns}   //最後返回一個對象
})
var ref$1 = createCompiler(baseOptions);
var compileToFunctions = ref$1.compileToFunctions;                  //編譯的入口文件

是否是有點暈呢,我舉一個例子就能看明白了,以下:

    function show(show){      //shou函數也直接返回一個匿名函數,帶一個參數
      return function(info){
        show(info)              //show經過做用域鏈就能夠訪問到參數的show函數了
      }
    }

    var info=show(function(info){
      console.log(info)
    })                          //這裏執行show函數,傳入一個匿名函數
    info({name:'gsz'})     //控制檯輸出:{name: "gsz"}

Vue內部看得晦澀是由於傳參的時候都註明了一個函數名,其實這個函數名是能夠忽略的,這樣看起來會更清晰一點    注:這樣設計是爲了跨平臺一些代碼的複用和存放吧,代碼結構在node下更好理解一點

compileToFunctions函數內部會調用parse()將模板通過一系列的正則解析,用一個AST對象保存,而後調用generate()作靜態節點標記,最後調用generate生成一個render函數

以上面的第一個Vue實例來講,parse()解析後的AST對象以下:

、再經過generate()後生成以下一個對象,其中render就是最終要執行的render函數了

compileToFunctions函數返回值是一個對象,以上面的第一個vue實例爲例,返回後的信息以下:{  render:"(function anonymous() {with(this){return _c('div',{attrs:{"id":"app1"}},[_v(_s(message))])}})",     //最終渲染出來的render函數  staticRenderFns:Function[]                                                //若是是靜態節點,則保存到這裏}之後分析到每一個API時這裏會單獨分析的最後在mountcomponent()函數內會以當前Vue實例爲上下文,執行該render函數(在2739行),此時就會完成渲染watch的收集,並生成虛擬VNode,最後調用_update()方法生成真實DOM節點。
相關文章
相關標籤/搜索