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">
debugger
new 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'); // 执行 beforeCreate
initInjections(vm); // 初始化inject before data/props
initState(vm); // 初始化 props,methods,data,computed,watch
initProvide(vm); // 初始化provide after data/props
callHook(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)
debugger
new Vue({
render: h => {
debugger
console.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 = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _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 = true
export { render, staticRenderFns }
- 总结:vue-loader的作用:编译.vue文件,得到 匿名渲染树函数
- 往下的其他步骤 同.html文件模式一样
码字不易,点赞鼓励!