vue源码解析之编译过程-含2种模式(及vue-loader作用)
2种模式指的是
- .html文件模式。.html文件内使用vue,没有vue-loader
- 执行.html文件 是vue的最基本的执行,不用加入vue-loader。先了解这个过程,后续更好理解vue-loader做了什么
- .vue文件模式。使用webpack工程,用vue-loader解析.vue 文件
编译过程
(2种模式,大部分过程是相同的,就获取 匿名渲染树函数 上有所不同)
- 先初始化各种属性和方法
- .html文件模式:
- 拿到#app对应的dom代码字符串template
- 编译template生成ast树 和 匿名渲染树函数
- .vue文件模式:
- vue-loader编译.vue文件,得到 匿名渲染树函数
- 执行 vm._update(vm._render(), hydrating); // (关键函数) 渲染/更新 函数
- 先执行vm._render(),通过 执行 匿名渲染树函数,得到 虚拟dom树vnode
- 在执行vm._update() ,层层递归 虚拟dom树vnode,得到真正的dom节点,然后update到真正的dom树上去,然后浏览器渲染出最新的dom树
.html文件模式的编译过程
执行.html文件 是vue的最基本的执行,不用加入vue-loader。先了解这个过程,后续更好理解vue-loader做了什么
测试文件:.html文件
- CDN引入vue的未压缩版,在script标签内,直接使用vue
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Title</title><script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script></head><body><div id="app">{{aa}} --- 1<div @click="qqq">click me</div>{{C_aa}}</div><script type="module">debuggernew Vue({el: '#app',data: {aa: 123},watch: {aa (nval, oval) {console.log(nval, oval)}},computed: {C_aa () {return this.aa + 100}},methods: {async qqq () {this.aa = this.aa + 1}}})</script></body></html>
以下按源代码执行顺序,从上到下
先初始化各种属性和方法
initLifecycle(vm); // 初始化生命周期initEvents(vm); // 初始化事件initRender(vm); // 初始化,处理渲染模板的函数callHook(vm, 'beforeCreate'); // 执行 beforeCreateinitInjections(vm); // 初始化inject before data/propsinitState(vm); // 初始化 props,methods,data,computed,watchinitProvide(vm); // 初始化provide after data/propscallHook(vm, 'created'); // 执行created
拿到#app对应的dom代码字符串
template = getOuterHTML(el);
打印template就是:"<div id="app"> {{aa}} --- 1 <div @click="qqq">click me</div> {{C_aa}} </div>"编译template生成ast树 和 匿名渲染树函数
var compiled = compile(template, options);得到ast树

树结构,子元素都在children内
compiled: {ast: 太大了 如图,}
得到 匿名渲染树函数
通过 createFunction(compiled.render, fnGenErrors); 得到 匿名渲染树函数
还会把这个 匿名渲染树函数 的结果缓存起来,key是上面的template,value是匿名渲染树函数compiled: {render: with(this){return _c('div',{attrs:{"id":"app"}},[_v("\n "+_s(aa)+" --- 1\n "),_c('div',{on:{"click":qqq}},[_v("click me")]),_v("\n "+_s(C_aa)+"\n")])}}
(function anonymous() {with(this){return _c('div',{attrs:{"id":"app"}},[_v("\n "+_s(aa)+" --- 1\n "),_c('div',{on:{"click":qqq}},[_v("click me")]),_v("\n "+_s(C_aa)+"\n")])}})
- callHook(vm, ‘beforeMount’); // 执行beforeMount
- 执行 vm._update(vm._render(), hydrating); // (关键函数) 渲染/更新 函数
- 先执行vm._render(),通过 执行 匿名渲染树函数,得到 虚拟dom树vnode
- 虚拟dom树vnode是用js对象去表示dom树
- 操作js要比操作真实的dom性能高很多,特别是做 新旧vnode之间的 diff算法 的时候
- 先执行vm._render(),通过 执行 匿名渲染树函数,得到 虚拟dom树vnode
// 执行 匿名渲染树函数,得到vnode 虚拟dom树vnode = render.call(vm._renderProxy, vm.$createElement);vnode结构如下图

- 在执行vm._update() ,层层递归 虚拟dom树vnode,得到真正的dom节点,然后update到真正的dom树上去,然后浏览器渲染出最新的dom树 diff算法的细节,可以看我另一篇:https://juejin.cn/post/6850037279268798472
if (!prevVnode) { // 第一次渲染/* 第一次渲染,里面通过递归,一层一层的 createElement 生成真正的dom,在appendChild到页面上,在removeChild #app的dom */vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);} else {/* updates 通过diff算法,一层一层的对比新旧vonde(时间复杂是n),高效的得到新旧vnode的差异,然后根据vnode生成真实的dom,并update到真实的dom树中 */vm.$el = vm.__patch__(prevVnode, vnode); // 旧vnode 和 新vnode}
- callHook(vm, ‘mounted’); // 执行mounted,此时,真实的dom树已经好了,可以获取到dom元素了
- 调用结束
.vue文件模式的编译过程
测试文件:.vue文件(内容同上)
使用webpack工程,用vue-loader解析.vue 文件
app.vue
main.js<template><div id="app">{{aa}} --- 1<div @click="qqq">click me</div>{{C_aa}}</div></template><script>export default {name: 'App',data () {return {aa: 123}},watch: {aa (nval, oval) {console.log(nval, oval)}},computed: {C_aa () {return this.aa + 100}},methods: {async qqq () {this.aa = this.aa + 1}}}</script>
import Vue from 'vue'import App from './App.vue'console.log(App)debuggernew Vue({render: h => {debuggerconsole.log(h(App))return h(App)}}).$mount('#app')
以下按源代码执行顺序,从上到下
- 先初始化各种属性和方法(同.html文件模式)
- 通过vue-loader编译.vue文件,得到 匿名渲染树函数
- 先看看vue-loader编译后的.vue文件 长什么样子:(上面main.js 第3行的打印)

- 点击 App.vue?6fd5:1 后,可以得到 如下 匿名渲染树函数
- 先看看vue-loader编译后的.vue文件 长什么样子:(上面main.js 第3行的打印)
var render = function() {var _vm = thisvar _h = _vm.$createElementvar _c = _vm._self._c || _hreturn _c("div", { attrs: { id: "app" } }, [_vm._v(" " + _vm._s(_vm.aa) + " --- 1 "),_c("div", { on: { click: _vm.qqq } }, [_vm._v("click me")]),_vm._v(" " + _vm._s(_vm.C_aa) + " ")])}var staticRenderFns = []render._withStripped = trueexport { render, staticRenderFns }
- 总结:vue-loader的作用:编译.vue文件,得到 匿名渲染树函数
- 往下的其他步骤 同.html文件模式一样
码字不易,点赞鼓励!
