一、获取HTML(template)
先了解一下,vue有两个运行环境的编译,一个是npm运行时的runtime版本,一个是浏览器引入vuejs的runtime-compile版本。
runtime-only:
用vue-loader将.vue文件编译成js,然后使用render函数渲染, 打包的时候就编译成了render函数需要的格式,不需要在客户端编译:
所以我们用webpack开发要使用render函数,如果没有render函数会报错:
代码语言:javascript复制new Vue({
render: h => h(App),
}).$mount('#app')
//error
You are using the runtime-only build of Vue where the template compiler is not available.
Either pre-compile the templates into render functions, or use the compiler-included build.
runtime-compile
将模板字符串编译成js进行渲染,运行时直接在客户端编译,所以初始化vue的时候一般传入el,也可以使用template或者mount。三个的优先级:render > template > el >
vue.js引入使用render:
代码语言:javascript复制 new Vue({
el: '#app',
data(){
return {
msg: 'msg'
}
},
render(h){
return h('div', {
attrs:{"id":"app"},
staticClass:"test",
staticStyle:{"background":"red"}
}, 'is render')
}
})
//生成
<div id="app" class="test" style="background: red;">is render</div>
vue.js里面可以找到getOuterHTML函数:
代码语言:javascript复制template = getOuterHTML(el);
function getOuterHTML (el) {
if (el.outerHTML) {
return el.outerHTML
} else {
var container = document.createElement('div');
container.appendChild(el.cloneNode(true));
return container.innerHTML
}
}
这就是获取传入的el元素的HTML,可以自己使用这个函数看看结果,其实就是整个标签的字符串。
二、转化成ast
ast叫抽象语法树,所有语言都可以转化成ast。当获取了HTML的内容,第一步要先把HTML转成ast, 用ast可以进行各种编译扩展,vue只是拿来生成render函数。
vue.js里面搜索: var ast = parse(template.trim(), options); 然后打印结果:
可以都展开看看内容,vue2主要是用正则一个一个匹配出来,可以搜索startTagOpen看看那些主要正则。
vue3好像换了,这个目录下:packages > compiler-core > src > parse.ts 可以看见里面好像是一个一个字符去解析,生成的结果字段也改了一些,但是最终效果都是差不多的。
tips
{{}}是大胡子语法mustache,挺多库使用的,可以自行了解一下。
parseHTML函数
解析的主要函数,通过正侧和栈数据结构把开始标签、结束标签、文本、注释等等分别进行不同的处理, 给不同元素类型加上不同的type,用来标记不同的节点类型。
optimize函数
生成之后会调用这个函数:
代码语言:javascript复制if (options.optimize !== false) {
optimize(ast, options);
}
添加标记是否是静态节点(static)和静态根节点(staticRoot),试了一下,只要不是纯静态的,staticRoot都是false。主要是优化patch性能,静态的可以跳过比对直接复制使用。
三、生成render函数
有了ast之后,通过generate函数生成render函数:
代码语言:javascript复制 var createCompiler = createCompilerCreator(function baseCompile (
template,
options
) {
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
}
});
render函数其实就是一个带有with语法的字符串:
代码语言:javascript复制with(this){
return _c('div',{attrs:{"id":"app"}},
[_c('p',{staticClass:"test",staticStyle:{"background":"red"}},
[_v("是" _s(msg) _s(msg))]),_v(" "),_c('div',[_v("ss")])])
}
tips:with语法用来处理_s(msg)等数据变量
代码语言:javascript复制let obj = {name: 'wade'}
with(obj){
console.log(name) //wade
}
vue只要传入vue实例this,就可以在生成虚拟dom的时候把数据直接赋值进去。
createFunction
有了字符串,就可以通过new Function执行,vue里面:
代码语言:javascript复制 function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err: err, code: code });
return noop
}
}
_c _s _v
render函数不止使用了这三个,简单介绍这三个:
- _c createElement 创建元素
- _v createTextVNode 创建文本
- _s toString 把数据JSON.stringify或者String
另外的函数可以搜索installRenderHelpers,里面有各种处理函数。
genElement
这个里面进行了一系列判断,然后生成render函数,里面也能看见v-for优先级大于v-if。 里面对template、slot、component、element都进行了判断然后进行不同的处理。
四、生成虚拟dom
有了render函数,调用就可以生成虚拟dom(vnode),生成的时间就是在mount。Vue.prototype.mount里面调用了mountComponent。
mountComponent函数
里面能看见runtime-only的报错,也可以看见beforeMount、mounted挂载,还有Watcher等, 但是核心是vm._update(vm._render(), hydrating);
Vue.prototype._render
_render其实就是调用render,然后生成虚拟dom:
代码语言:javascript复制vnode = render.call(vm._renderProxy, vm.$createElement);
可以直接打印这个vnode看看虚拟dom的结构:
会发现跟那些分析虚拟dom diff算法的有一些不同,比如props,vue其实是data, 不过这只是字段使用的不同,原理是都一样。
仔细看也会发现虚拟dom跟ast非常像,不过会多出一些属性。要明确ast是语法层面的属性,描述的是语言本身, 可以描述dom、js、css、Java等语言,不能随意添加不存在的属性。虚拟dom是自己定义用来描述的dom的数据结构,可以自己随意添加需要的属性。
五、生成真实dom
生成真实dom是使用Vue.prototype._update,里面有几行代码:
代码语言:javascript复制 var prevVnode = vm._vnode;
var restoreActiveInstance = setActiveInstance(vm);
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
- prevVnode其实就是oldVnode
- vnode是最新的虚拟dom
- vm._vnode = vnode;把新的缓存,下次更新就是oldVnode
- !prevVnode如果没有旧的,相当于是第一次渲染,直接更新,不用比对(initial render)
- __patch__其实就是patch函数Vue.prototype.patch = inBrowser ? patch : noop;
调用_update会生成真实的dom,至于怎么生成的,这次不去了解,其实核心就是patch函数,也是dom diff的核心。
上面就是vue模板编译的大概流程,总结一下:
- 获取HTML(template)
- 转化成ast
- 生成render函数
- 生成虚拟dom
- 生成真实dom
模板编译大致的步骤就这样,最好是可以对照着几个核心的函数,然后自己到vue.js源码里面看看。最好当然是把vue npm包拉下来看,划分的会更细。
理清楚了vue模板编译的流程,再去看依赖收集,看什么时机触发更新,然后再去学dom diff,会比较容易一点。
最后,过程是清楚了,但最核心的实现步骤自己并没有完全懂,也就是没办法手写,毕竟看代码比敲代码容易多了。