vue 中render执行流程梳理
用了多年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 流程理了一遍,感谢阅读有不正确处欢迎指正和探讨,学习永不止步,探索从未停止。
下一篇: 项目代码重构