用了多年vue 今天對自己了解的render 做一個梳理
一、使用template模板
先從vue 初始化開始:
眾所周知項目的main.js中定義了
var app = new Vue({})
這vue初始化操作
其實他會執行到
這個方法中的_init函數,在這個方法執行一些列的初始化后,判斷$options
是否定義el,如果定義調用
vm.$mount(vm.$options.el)
函數,這個函數其實是在entry-runtime-with-compiler.js中定義的,if (!options.render) {}
判斷是否定義了render函數,如果未定義再判斷是否定義template,如果定義直接調用‘compileToFunctions’函數將template編譯成為一個匿名函數,這里先借一個簡單案例,打個斷點測試下執行流程
import Vue from 'vue'
/** * 使用 template 方式 */
var app = new Vue({
el:'#admin',
template:'<ul :class="bindCls" class="list" v-if="isShow">'+
'<li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li></ul>',
data(){
return {
bindCls:'a',
isShow:true,
data:['A','B','C','D']
}
},
methods:{
clickItem(index){
console.log(index);
}
}
})
最后render匿名函數內容是這樣的
(function anonymous(
) {
with(this){return (isShow)?_c('ul',{staticClass:"list",class:bindCls},_l((data),function(item,index){return _c('li',{on:{"click":function($event){return clickItem(index)}}},[_v(_s(item)+":"+_s(index))])}),0):_e()}
})
看似一段簡短的render,其實生成這個render是一個相當復雜的過程
1、將template 轉換為render 字符串
首先在compiler 文件中的index.js中
定義了createCompiler常量該值為create-compiler.js里的createCompilerCreator函數,同時將baseCompile函數傳入
而createCompilerCreator返回的是一個createCompiler函數而該createCompiler函數最后的返回值是一個
{ compile, compileToFunctions: createCompileToFunctionFn(compile) }
對象,
該對像中createCompileToFunctionFn其實是to-functiuon.js中定義的,目的就是把compile作為參數傳入,進行一些列校驗合並等
再回過頭來調用compile實現編譯操作,取得最后compile編譯的結果,其實准確說是baseCompile編譯后的結果,因compile中其實是調用baseCompile進行模板編譯的(其中的通過parse 生成ast和generate生成code就不細說了,其實這個code中就包含了render),
而createCompileToFunctionFn
取得了編譯后的render並使用createFunction將其轉為一個匿名函數,就是上面看見的樣子了
而createCompilerCreator
值返回到entry-runtime-with-compiler.js中的Vue.prototype.$mount
函數中,同時將render掛載到了vu.$options
上,再次調用runtime下index.js中的Vue.prototype.$mount
函數
此時函數中又調用到了core/instance/lifecycle中的mountComponent
函數;mountComponent
函數在new Watcher
中執行get(), 調用了updateComponent函數; 開始了執行vm._render()
的操作,這個函數其實就定義在instance/render.js中;
這一連串的調用最終將template編譯為render,又將其使用createElement
編譯為vdom,再使用__patch__
初始化事件將其生成真實dom插入到頁面的body中
看文字很不直觀,這里截幾個源碼圖來看看
2、render執行調用
好了現在就來看看render究竟是怎么執行的,其實他的執行也挺簡單直接使用call進行調用,如
這個里面的_renderProxy其實是在init.js中綁定的
其實就是在開發階段做一些校驗,再將vm 使用proxy代理一個handlers將其返回,而生產環境就是vm實例
而vm.$createElement
是上面一個initRender中初始化了,其實這個主要是用於我們手寫render用的
然render.call(vm._renderProxy, vm.$createElement)
,中render就是上面說過的匿名函數
(function anonymous(
) {
with(this){return (isShow)?_c('ul',{staticClass:"list",class:bindCls},_l((data),function(item,index){return _c('li',{on:{"click":function($event){return clickItem(index)}}},[_v(_s(item)+":"+_s(index))])}),0):_e()}
})
他里面的with作用更改作用域鏈,執行時會首先從局部去查找里面的函數,如_c(),如果局部沒有就會到其綁定的作用域this中去找,在render.call時我們知道其實是將vm傳入了,而這個_c()就是上面initRender中定義的
,
而其他的如_e(),_l()這些就是在render-helper中了
而_c()又對應一個createElement函數這個就是真正的創建vdom的地方了,到此render 編譯template就完成了
二、手寫render
import Vue from 'vue'
var app = new Vue({
el:'#admin',
render(createElement){
return createElement('div',{
attrs:{
id:'admin'
}
},this.message)
},
data(){
return {
message:'Hello vue'
}
}
})
而手寫render 其實是一樣的,只是在entry-runtime-with-compiler.js不會進行compileToFunctions編譯,而直接調用mount,而上面代碼中render里的createElement是在render.call(vm._renderProxy, vm.$createElement)
直接將createElement函數作為參數傳入的,最后將該函數返回值返回給 Vue.prototype._update
,然后將其使用vm.__patch__
轉為真實dom數插入到頁面中
三、自定義函數進行模擬
手動寫render
var vm = function(){}
function createEl(a,b){
console.log(a,b);
}
vm.$createEl = (a,b) => createEl(a,b);
render.call(vm,vm.$createEl); // div1 div2
function render(createEl){
return createEl('div1','div2');
}
2、編譯 template為render
// vm構造函數
var vm= function(){};
// 創建vdom函數
function createEl(a,b){
console.log(a,b);
};
// 模擬獲得編譯后的render 字符串
var compiledRender = "with(this){return _c('div1','div2')}";
// 將其轉換為函數
var render= new Function(compiledRender);
// 為vm定義_c函數
vm._c = (a,b) => createEl(a,b);
// 為vm定義$createEl 函數
vm.$createEl = (a,b) => createEl(a,b);
// 執行調用
render.call(vm, vm.$createEl ); // div1 div2
到此把整個render 流程理了一遍,感謝閱讀有不正確處歡迎指正和探討,學習永不止步,探索從未停止。