金三银四,来一波图解Vue干货?

阅读本文能够帮助你什么?

  • 在学习vue源码的时候发现组件化过程很绕?
  • 在响应式过程中ObserverDepWatcher三大对象傻傻分不清?
  • 搞不清楚对象、数组依赖收集、派发更新的流程?depwatcher互调造成混乱?
  • 想直接上代码学源码十分枯燥、乏味,而且缺乏大体流程概念?
  • 像我一样,有段时间没看vue源码好像有点遗忘?但是想快速回顾却无从下手?

本文主要讲解组件化响应式的核心流程。或许上述的这些问题,在这里你都能找到答案

一、组件化流程

1. 整个new Vue阶段做了什么?

  1. 执行init操作。包括且不限制initLifecycleinitState
  2. 执行mount。进行元素挂载
  3. compiler步骤在runtime-only版本中没有。
    • compiler步骤对template属性进行编译,生成render函数。
    • 一般在项目中是在.vue文件开发,通过vue-loader处理生成render函数。
  4. 执行render。生成vnode
    • render例子,如下
    <div id="app">{{ message }}</div>
    复制代码
    • 对应手写的render函数
    render (h) {
      return h('div', {
         attrs: {
            id: 'app'
          },
      }, this.message)
    }
    复制代码
  5. patch。新旧vnode经过diff后,渲染到真实dom上

new vue.png


2. 普通dom元素如何渲染到页面?

  1. 执行$mount
    • 实际执行mountComponent
    • 这里会实例化一个Watcher
    • Watcher中会执行get方法,触发updateComponent
  2. 执行updateComponent。执行vm._update(vm._render(), hydrating)
  3. 执行vm.render()
    • render其实调用createElment(h函数)
    • 根据tag的不同,生成组件、原生VNode并返回
  4. 执行vm.update()createElm()createChildren() 递归调用
  5. 将VNode转化为真实的dom,并且最终渲染到页面

patch.png


3. 组件如何渲染到页面?

  • 这里以如下代码案例讲解更加清晰~没错,就是这么熟悉!就是一个初始化的Vue项目

    // mian.js
    import Vue from 'vue'
    import App from './App.vue'
    
    new Vue({
      render: h => h(App),
    }).$mount('#app')
    复制代码
    // App.vue
    <div id="app">
      <p>{{ msg }}</p>
    </div>
    <script>
        export default {
            name: 'App',
            data () {
                return {
                    msg: 'hello world'
                }
            }
        }
    </script>
    复制代码
  • 主要讲解组件普通元素的不同之处,主要有2点:

    1. 如何生成VNode——创建组件VNodecreateComponent
      • (注意:这一步对应上图render流程的紫色块的展开!!!)
      • 区分普通元素VNode
        • 普通VNode:tag是html的保留标签,如tag: 'div'
        • 组件VNode:tag是以vue-component开头,如tag: 'vue-component-1-App'

    组件VNode.png

    1. 如何patch——组件new Vue到patch流程createComponent

      • (注意:这一步对应上图patch流程的紫色块的展开!!!)
      1. $vnode:占位符vnode。最终渲染vnode挂载的地方。所有的组件通过递归调用createComponent直至不再存在组件VNode,最终都会转化成普通的dom。
      {
          tag: 'vue-component-1-App',
          componentInstance: {组件实例},
          componentOptions: {Ctor, ..., }
      }
      复制代码
      1. _vnode:渲染vnode。
      {
          tag: 'div',
          {
              "attrs": {
                  "id": "app"
              }
          },
          // 对应占位符vnode: $vnode
          parent: {
              tag: 'vue-component-1-App',
              componentInstance: {组件实例},
              componentOptions: {Ctor, ..., }
          },
          children: [
              // 对应p标签
              { 
                  tag: 'p',
                  // 对应p标签内的文本节点{{ msg }}
                  children: [{ text: 'hello world' }]
              }, {
                // 如果还有组件VNode其实也是一样的
                tag: 'vue-component-2-xxx'
              }              
          ]
      }
      复制代码

patch组件.png


4. Vue组件化简化流程

  • 相信你看完细粒度的Vue组件化过程可能已经晕头转向了,这里会用一个简化版的流程图进行回顾,加深理解

组件化简化.png


二、响应式流程

  • 案例代码
// 案例
export default {
    name: 'App',
    data () {
        return {
            msg: 'hello world',
            arr = [1, 2, 3]
        }
    }
}
复制代码

1. 依赖收集

  • 这里会从Observer、Dep、Watcher三个对象进行讲解,分object、array两种依赖收集方式

    1. 三个核心对象:Observer(蓝)、Dep(绿)、Watcher(紫)

    依赖收集三大OBject.png

    1. 依赖收集准备阶段——Observer、Dep的实例化
    • 注意对象、根数组的不同处理方式。这里以核心代码+图讲解
    // 以下是initData调用的方法讲解,排列遵循调用顺序
    function observe (value, asRootData) {
      if (!isObject(value)) return // 非对象则不处理
      // 实例化Observer对象
      var ob;
      ob = new Observer(value);
      return ob
    }
    
    function Observer (value) {
      this.value = value; // 保存当前的data
      this.dep = new Dep(); // 实例化dep,数组进行依赖收集的dep(对应案例中的arr)
      def(value, '__ob__', this);    
      if (Array.isArray(value)) {
        if (hasProto) {
          // 这里会改写数组原型。__proto__指向重写数组方法的对象
          protoAugment(value, arrayMethods); 
        } else {
          copyAugment(value, arrayMethods, arrayKeys);
        }
        this.observeArray(value);
      } else {
        this.walk(value); 
      }
    }
    // 遍历数组元素,执行对每一项调用observe,也就是说数组中有对象会转成响应式对象
    Observer.prototype.observeArray = function observeArray (items) {
      for (var i = 0, l = items.length; i < l; i++) {
        observe(items[i]);
      }
    }
    // 遍历对象的全部属性,调用defineReactive
    Observer.prototype.walk = function walk (obj) {
      var keys = Object.keys(obj);
      // 如案例代码,这里的 keys = ['msg', 'arr']
      for (var i = 0; i < keys.length; i++) {        
        defineReactive(obj, keys[i]);
      }
    }
    
    function defineReactive (obj, key, val) {
      // 产生一个闭包dep
      var dep = new Dep();
      // 如果val是object类型,递归调用observe,案例代码中的arr会走这个逻辑
      var childOb = !shallow && observe(val);
      Object.defineProperty(obj, key, {    
        get: function reactiveGetter () { 
          // 求value的值
          var value = getter ? getter.call(obj) : val;
          if (Dep.target) { // Dep.target就是当前的Watcher
            // 这里是闭包dep
            dep.depend();
            if (childOb) {
              // 案例代码中arr会走到这个逻辑
              childOb.dep.depend(); // 这里是Observer里的dep,数组arr在此依赖收集
              if (Array.isArray(value)) {
                dependArray(value);
              }
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          // 下文派发更新里进行讲解
        }
      });
    }
    复制代码

    依赖收集-新.png

    1. 依赖收集触发阶段——Wather实例化、访问数据、触发依赖收集

    依赖收集触发阶段.png


2. 派发更新

  • 派发更新区分对象属性、数组方法进行讲解

  • 这里可以先想一下,以下操作会发生什么?

    • this.msg = 'new val'
    • this.arr.push(4)
  • 是的,毫无疑问都会先触发他们之中的get,那再触发什么呢?我们接下来看

    1. 对象属性修改触发set,派发更新。this.msg = 'new val'
    ...
    Object.defineProperty (obj, key, {
        get () {...},
        set: function reactiveSetter (newVal) {
          var value = getter ? getter.call(obj) : val;
          // 判断新值相比旧值是否已经改变
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          // 如果新值是引用类型,则将其转化为响应式
          childOb = !shallow && observe(newVal);
          // 这里通知dep的所有watcher进行更新
          dep.notify();
        }
    }        
    ...
    复制代码

    obj派发更新.png

    1. 数组调用方法。this.arr.push(4)
    // 数组方法改写是在 Observer 方法中
    function Observer () {
        if (hasProto) { 
            // 用案例讲解,也就是this.arr.__proto__ = arrayMethods
            protoAugment(value, arrayMethods); 
        }
    }   
    
    // 以下是数组方法重写的实现
    var arrayProto = Array.prototype; // 保存真实数组的原型
    var arrayMethods = Object.create(arrayProto); // 以真数组为原型创建对象
    // 可以看成:arrayMethods.__proto__ = Array.prototype
    var methodsToPatch = [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ];
    
    // 一个装饰器模型,重写7个数组方法
    methodsToPatch.forEach(function (method) {
      // 保存原生的数组方法
      var original = arrayProto[method];
      // 劫持arrayMethods对象中的数组方法
      def(arrayMethods, method, function mutator () {
        var args = [], len = arguments.length;
        while ( len-- ) args[ len ] = arguments[ len ];
    
        var result = original.apply(this, args);
        var ob = this.__ob__; // 当我门调用this.arr.push(),这里就能到数组对象的ob实例
        var inserted;
        switch (method) {
          case 'push':
          case 'unshift':
            inserted = args;
            break
          case 'splice':
            inserted = args.slice(2);
            break
        }
        if (inserted) { ob.observeArray(inserted); }
        // 由于数组对象在new Observer中实例化了一个dep,这里就能在ob实例中拿到dep属性
        ob.dep.notify();
        return result
      });
    })
    复制代码

    数组派发更新.png


  • 整个new Vue阶段、到依赖收集派发更新的全部流程就到这里结束了。可以纵观流程图看出,Vue应用就是一个个Vue组件组成的,虽然整个组件化、响应式流程很多,但核心的路径一旦走通,你就会恍然大悟。

  • 总体来说,Vue的源码其实是比较好上手的。整体代码流程非常的清晰,想要深入某一块逻辑可以结合流程图去仔细去研究,相信你一定会有所收获。

写这篇文章也算是自己的一个知识沉淀吧,毕竟很早之前就学习过Vue的源码了,但是也一直没做笔记。现在回顾一下,发现很多都有点忘了,但是缺乏一个快速记忆、回顾的笔记。如果要直接硬磕源码重新记忆,还是比较费时费力的~作为知识分享,希望可以帮助到想学习源码,想要进阶的你,大家彼此共勉,一同进步!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享