Vue有三個屬性和模板有關,官網上是這樣解釋的:
el ;提供一個在頁面上已存在的 DOM 元素作為 Vue 實例的掛載目標
template ;一個字符串模板作為 Vue 實例的標識使用。模板將會 替換 掛載的元素。掛載元素的內容都將被忽略,除非模板的內容有分發插槽。
render ;字符串模板的代替方案,允許你發揮 JavaScript 最大的編程能力。該渲染函數接收一個 createElement
方法作為第一個參數用來創建 VNode
。
簡單說一下,就是:
Vue內部會判斷如果沒有render屬性則把template屬性的值作為模板,如果template不存在則把el對應的DOM節點的outerHTML屬性作為模板,經過一系列正則解析和流程生成一個render函數,最后通過with(this){}來執行。
也就是說template的優先級大於el。
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函數如下:
writer by:大沙漠 QQ:22969969
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節點。