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

1,873 阅读3分钟

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

自从上一次内部分享会分享了 vue的diff算法 后,小伙伴们一致对vue的源码感兴趣,那就整吧

注:以下内容只讲了几个关键步骤,个人觉得,先把关键步骤了解明白,知道关键步骤的 输入和输出 后,自己也可以尝试手写实现

2种模式指的是

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

编译过程

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

  1. 先初始化各种属性和方法
  2. .html文件模式:
    1. 拿到#app对应的dom代码字符串template
    2. 编译template生成ast树 和 匿名渲染函数
  3. .vue文件模式:
    1. vue-loader编译.vue文件,得到 匿名渲染函数
  4. 执行 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
    <!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>
    

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

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

    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
    
  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树(好处是 方便解析一些vue在标签内的一些特定语法,最终有助于生成匿名渲染函数)

      compiled: {
          ast: 太大了 如图,
      }
      

      ast.png

      树结构,子元素都在children内

      ast-children.png

    • 得到 匿名渲染函数

      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")])}   
      }
      

      通过 createFunction(compiled.render, fnGenErrors); 得到 匿名渲染函数

      (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")])}
      })
      

      还会把这个 匿名渲染函数 的结果缓存起来,key是上面的template,value是匿名渲染函数

  4. callHook(vm, 'beforeMount'); // 执行beforeMount

  5. 执行 vm._update(vm._render(), hydrating); // (关键函数) 渲染/更新 函数

    1. 先执行vm._render(),通过 执行 匿名渲染函数,得到 虚拟dom树vnode
      • 虚拟dom树vnode是用js对象去表示dom树
      • 操作js要比操作真实的dom性能高很多,特别是做 新旧vnode之间的 diff算法 的时候
      // 执行 匿名渲染函数,得到vnode 虚拟dom树
      vnode = render.call(vm._renderProxy, vm.$createElement); 
      vnode结构如下图
      
      vnode.png
    2. 在执行vm._update() ,层层递归 虚拟dom树vnode,得到真正的dom节点,然后update到真正的dom树上去,然后浏览器渲染出最新的dom树
      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  
      }
      
      diff算法的细节,可以看我另一篇:juejin.cn/post/685003…
  6. callHook(vm, 'mounted'); // 执行mounted,此时,真实的dom树已经好了,可以获取到dom元素了

  7. 调用结束

.vue文件模式的编译过程

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

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

    app.vue

    <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>
    
    

    main.js

    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')
    

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

  1. 先初始化各种属性和方法(同.html文件模式)

  2. 通过vue-loader编译.vue文件,得到 匿名渲染函数

    1. 先看看vue-loader编译后的.vue文件 长什么样子:(上面main.js 第3行的打印) vue-loader.png
    2. 点击 App.vue?6fd5:1 后,可以得到 如下 匿名渲染函数
    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 }
    
    1. 总结:vue-loader的作用:编译.vue文件,得到 匿名渲染函数
  3. 往下的其他步骤 同.html文件模式一样

下一篇

vue源码解析之调度原理(响应式原理)


码字不易,点赞鼓励!