vue源码解析之编译过程-含2种模式(及vue-loader作用)

2种模式指的是

  1. .html文件模式。.html文件内使用vue,没有vue-loader
    • 执行.html文件 是vue的最基本的执行,不用加入vue-loader。先了解这个过程,后续更好理解vue-loader做了什么
  1. .vue文件模式。使用webpack工程,用vue-loader解析.vue 文件

编译过程

(2种模式,大部分过程是相同的,就获取 匿名渲染树函数 上有所不同)

  1. 先初始化各种属性和方法
  2. .html文件模式:
    1. 拿到#app对应的dom代码字符串template
    2. 编译template生成ast树 和 匿名渲染树函数
  1. .vue文件模式:
    1. vue-loader编译.vue文件,得到 匿名渲染树函数
  1. 执行 vm._update(vm._render(), hydrating); // (关键函数) 渲染/更新 函数
    1. 先执行vm._render(),通过 执行 匿名渲染树函数,得到 虚拟dom树vnode
    2. 在执行vm._update() ,层层递归 虚拟dom树vnode,得到真正的dom节点,然后update到真正的dom树上去,然后浏览器渲染出最新的dom树

.html文件模式的编译过程

执行.html文件 是vue的最基本的执行,不用加入vue-loader。先了解这个过程,后续更好理解vue-loader做了什么

测试文件:.html文件

  • CDN引入vue的未压缩版,在script标签内,直接使用vue
    1. <!DOCTYPE html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <title>Title</title>
    6. <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
    7. </head>
    8. <body>
    9. <div id="app">
    10. {{aa}} --- 1
    11. <div @click="qqq">click me</div>
    12. {{C_aa}}
    13. </div>
    14. <script type="module">
    15. debugger
    16. new Vue({
    17. el: '#app',
    18. data: {
    19. aa: 123
    20. },
    21. watch: {
    22. aa (nval, oval) {
    23. console.log(nval, oval)
    24. }
    25. },
    26. computed: {
    27. C_aa () {
    28. return this.aa + 100
    29. }
    30. },
    31. methods: {
    32. async qqq () {
    33. this.aa = this.aa + 1
    34. }
    35. }
    36. })
    37. </script>
    38. </body>
    39. </html>

以下按源代码执行顺序,从上到下

  1. 先初始化各种属性和方法

    1. initLifecycle(vm); // 初始化生命周期
    2. initEvents(vm); // 初始化事件
    3. initRender(vm); // 初始化,处理渲染模板的函数
    4. callHook(vm, 'beforeCreate'); // 执行 beforeCreate
    5. initInjections(vm); // 初始化inject before data/props
    6. initState(vm); // 初始化 props,methods,data,computed,watch
    7. initProvide(vm); // 初始化provide after data/props
    8. callHook(vm, 'created'); // 执行created
  2. 拿到#app对应的dom代码字符串
    template = getOuterHTML(el);
    打印template就是:"<div id="app"> {{aa}} --- 1 <div @click="qqq">click me</div> {{C_aa}} </div>"

  3. 编译template生成ast树 和 匿名渲染树函数
    var compiled = compile(template, options);

    • 得到ast树
      ast.png
      树结构,子元素都在children内
      ast-children.png

      1. compiled: {
      2. ast: 太大了 如图,
      3. }
    • 得到 匿名渲染树函数
      通过 createFunction(compiled.render, fnGenErrors); 得到 匿名渲染树函数
      还会把这个 匿名渲染树函数 的结果缓存起来,key是上面的template,value是匿名渲染树函数

      1. compiled: {
      2. 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")])}
      3. }
      1. (function anonymous(
      2. ) {
      3. 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")])}
      4. })
  1. callHook(vm, ‘beforeMount’); // 执行beforeMount
  2. 执行 vm._update(vm._render(), hydrating); // (关键函数) 渲染/更新 函数
    1. 先执行vm._render(),通过 执行 匿名渲染树函数,得到 虚拟dom树vnode
      • 虚拟dom树vnode是用js对象去表示dom树
      • 操作js要比操作真实的dom性能高很多,特别是做 新旧vnode之间的 diff算法 的时候
  1. // 执行 匿名渲染树函数,得到vnode 虚拟dom树
  2. vnode = render.call(vm._renderProxy, vm.$createElement);
  3. vnode结构如下图

vnode.png

  1. 在执行vm._update() ,层层递归 虚拟dom树vnode,得到真正的dom节点,然后update到真正的dom树上去,然后浏览器渲染出最新的dom树 diff算法的细节,可以看我另一篇:https://juejin.cn/post/6850037279268798472
    1. if (!prevVnode) { // 第一次渲染
    2. /* 第一次渲染,里面通过递归,一层一层的 createElement 生成真正的dom,
    3. 在appendChild到页面上,在removeChild #app的dom */
    4. vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
    5. } else {
    6. /* updates 通过diff算法,一层一层的对比新旧vonde(时间复杂是n),
    7. 高效的得到新旧vnode的差异,然后根据vnode生成真实的dom,并update到真实的dom树中 */
    8. vm.$el = vm.__patch__(prevVnode, vnode); // 旧vnode 和 新vnode
    9. }
  1. callHook(vm, ‘mounted’); // 执行mounted,此时,真实的dom树已经好了,可以获取到dom元素了
  2. 调用结束

.vue文件模式的编译过程

测试文件:.vue文件(内容同上)

  • 使用webpack工程,用vue-loader解析.vue 文件
    app.vue
    main.js

    1. <template>
    2. <div id="app">
    3. {{aa}} --- 1
    4. <div @click="qqq">click me</div>
    5. {{C_aa}}
    6. </div>
    7. </template>
    8. <script>
    9. export default {
    10. name: 'App',
    11. data () {
    12. return {
    13. aa: 123
    14. }
    15. },
    16. watch: {
    17. aa (nval, oval) {
    18. console.log(nval, oval)
    19. }
    20. },
    21. computed: {
    22. C_aa () {
    23. return this.aa + 100
    24. }
    25. },
    26. methods: {
    27. async qqq () {
    28. this.aa = this.aa + 1
    29. }
    30. }
    31. }
    32. </script>
    1. import Vue from 'vue'
    2. import App from './App.vue'
    3. console.log(App)
    4. debugger
    5. new Vue({
    6. render: h => {
    7. debugger
    8. console.log(h(App))
    9. return h(App)
    10. }
    11. }).$mount('#app')

以下按源代码执行顺序,从上到下

  1. 先初始化各种属性和方法(同.html文件模式)
  2. 通过vue-loader编译.vue文件,得到 匿名渲染树函数
    1. 先看看vue-loader编译后的.vue文件 长什么样子:(上面main.js 第3行的打印)
      vue-loader.png
    2. 点击 App.vue?6fd5:1 后,可以得到 如下 匿名渲染树函数
  1. var render = function() {
  2. var _vm = this
  3. var _h = _vm.$createElement
  4. var _c = _vm._self._c || _h
  5. return _c("div", { attrs: { id: "app" } }, [
  6. _vm._v(" " + _vm._s(_vm.aa) + " --- 1 "),
  7. _c("div", { on: { click: _vm.qqq } }, [_vm._v("click me")]),
  8. _vm._v(" " + _vm._s(_vm.C_aa) + " ")
  9. ])
  10. }
  11. var staticRenderFns = []
  12. render._withStripped = true
  13. export { render, staticRenderFns }
  1. 总结:vue-loader的作用:编译.vue文件,得到 匿名渲染树函数
  1. 往下的其他步骤 同.html文件模式一样

码字不易,点赞鼓励!