VUE源码解析(二):你绝对没见过的最详细的Vue数据驱动视图

4 篇文章 0 订阅
2 篇文章 0 订阅

用过Vue的都知道Vue是数据驱动视图,Input可以实现双向绑定,这篇我将带大家从源码的角度解析


开始之前我们先看一个案例,思考一下打印的顺序和打印结果!

<div id="app">
	<button ref="button">{{ message.name }}</button>
</div>
<script>
 const app = new Vue({
     data: {
         message: { name: '测试' }
     },
     updated() {
         console.log(this.$refs.button.innerText, 333)
     },
     mounted() {
         this.message.name = '挂载'
         this.$nextTick(() => {
             console.log(this.$refs.button.innerText, 111)
         })
         console.log(this.$refs.button.innerText, 222)
     }
 }).$mount("#app") 
</script>

在上一篇我们已经讲过Vue创建一个实例的过程及其代码执行顺序,也说过Vue源码中除了nextTick不存在任何异步代码,从下面这张图我们很清楚的知道Vue的钩子函数执行顺序是

beforeCreate:在回调beforeCreate之前,会初始化vue会用到的事件和属性等,但是这里通过this还拿不到data的值,因为data的值还没有绑定到实例vm上,全部在实例的$options中。
created::在回调created之前,就是beforeCreate之后,会初始化inject、provid、props、methods、watch、computed,也是在这一步之前data的值被绑定到了实例中,同时添加了监听,所以在created中都可以通过this在实例中访问到data的值和methods等等,注意:这里修改实例data的值是在生成虚拟dom之前,所以不会触发更新,因为还没有建立起发布订阅之间的联系。
beforeMount::在回调beforeMount之前,就是created之后,Vue实例中的初始化已经全部完成,在这之间就开始回去判断传入的options是否有有el属性,这里不做细讲,可以看上一篇文章,最后根据传入属性是否有template来生产对应的render函数,注意:这里修改实例data的值是在生成虚拟dom之前,所以不会触发更新,因为还没有建立起发布订阅之间的联系
mounted::在回调mounted之前,就是beforeMount之后,这个阶段就是根据render函数根元把生成的虚拟dom挂载到根元素上,注意:如果在这里修改data的值,就有一个很重要的过程,就是在建立虚拟Dom的过程中,会创建一个Watcher,这个Watcher是啥呢,就是保存我HTML元素使用一次这个{{}}调用,就是订阅一次data的数据,就会存一个Dep的实例,Dep是啥,Dep实例有两个属性,一个是唯一的id,一个是subs就是订阅的的组件Watcher。
所以只有在mounted钩子函数修改data的值才会触发更新。
beforeUpdate::在mounted之后,回调就是updated之前,这一步做了啥,就是把所有触发了更新的Watcher存在一个队列里。注意:记住别人问你为什么beforeUpdate和updated始终在mounted之后,你就问答因为更新函数使用的是Vue的nextTick,nextTick是一个异步的玩意儿,其它的钩子函数回调都是同步的。
updated::在回调updated之前,就是beforeUpdate之后,这一步就是更新值,重新生成虚拟dom挂载。注意:在这个钩子函数里面不能写触发更新的操作,不然会陷入死循环。
记住以下几点:一个组件一个Watcher,一个data中的数据一个Observer,对象数组这些可分解成多个Observer,Watcher的作用就是用来管理data的订阅和发布,经典发布订阅不用多说了吧!
在这里插入图片描述
现在我们来看一下源码是如何数据驱动视图的,上一篇已经讲了初始化会做什么,所以这里就直接进入初始化data,这个函数里面直接会去调用observe

function initData (vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function' // 这里就是判断data是对象还是方法,是方法就用getData取出data
      ? getData(data, vm)
      : data || {};
    if (!isPlainObject(data)) { // 这里判断如果data是一个方法,返回的不是对象
      data = {};
      warn(
        'data functions should return an object:\n' +
        'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
        vm
      );
    }
    // proxy data on instance
    var keys = Object.keys(data);
    var props = vm.$options.props;
    var methods = vm.$options.methods;
    var i = keys.length;
    while (i--) {
      var key = keys[i];
      {
        if (methods && hasOwn(methods, key)) {
          warn(
            ("Method \"" + key + "\" has already been defined as a data property."),
            vm
          );
        }
      }
      if (props && hasOwn(props, key)) {
        warn(
          "The data property \"" + key + "\" is already declared as a prop. " +
          "Use prop default value instead.",
          vm
        );
      } else if (!isReserved(key)) {
        proxy(vm, "_data", key);
      }
    }
    observe(data, true /* asRootData */); // 调用observe
  }

我们看一下observe方法内部

function observe (value, asRootData) {
    if (!isObject(value) || value instanceof VNode) {
      return
    }
    var ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // 这里是干啥的呢,就是已经监听了的数据不用在监听了
      ob = value.__ob__;
    } else if (
      shouldObserve &&
      !isServerRendering() &&
      (Array.isArray(value) || isPlainObject(value)) &&
      Object.isExtensible(value) &&
      !value._isVue
    ) {
      ob = new Observer(value); // 这里就是构造出一个Observer对数据进行监听
    }
    if (asRootData && ob) {
      ob.vmCount++;
    }
    return ob
  }

我们看一下构造函数Observer

var Observer = function Observer (value) {
    this.value = value;
    this.dep = new Dep(); // 这里创建一个Dep,下面还有个地方也创建了Dep,这里代表data的每个数据都有唯一的触发者
    this.vmCount = 0;
    def(value, '__ob__', this);
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value); // 如果是数组就逐个调用observer创建Observer
    } else {
      this.walk(value); // 不是就直接走这里
    }
  };
 
  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
 Observer.prototype.walk = function walk (obj) {
    var keys = Object.keys(obj);
    for (var i = 0; i < keys.length; i++) {
      defineReactive$$1(obj, keys[i]); // 这里就是大家众所周知的vue的数据驱动视图原理了
    }
  };

  /**
   * Observe a list of Array items.
   */
  Observer.prototype.observeArray = function observeArray (items) {
    for (var i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  };

数据驱动视图的核心就是在defineReactive$$1里面

function defineReactive$$1 (
    obj,
    key,
    val,
    customSetter,
    shallow
  ) {
    var dep = new Dep(); // 这里也创建一个Dep,这是因为对象可能存在多层级的情况,所以每个层级都会分配一个触发者

    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
      return
    } // 这里就是判断你定义的data是否可以枚举

    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
      val = obj[key];
    }

    var childOb = !shallow && observe(val); // 这里是要去检查对象是否存在多层级,比如 message: { name: "测试" }
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend(); // 这里会在创建虚拟dom的时候来执行,上面已经讲了,就是Watcher会去保存谁订阅这个数据,这个就把触发者Dep存在Watcher里面去
          if (childOb) {
            childOb.dep.depend(); // 这里同理
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        // 这里就是你改变了data的值就会去通知Watcher,凡是订阅了这条数据的去执行更新,这里利用了闭包去保存dep
        dep.notify(); 
      }
    });
  }

最后总结一下

Vue数据驱动,就是利用Object.defineProperty或Proxy,重写set和get方法,在利用发布订阅的方式修改实例的值,最后重新生成虚拟dom,重新渲染视图,上面讲了一个Watcher对应一个组件,一个数据会保存一份Dep,哪个组件订阅这份数据,就把这份Dep存到Watcher中,Dep中的订阅者也会存下这个Watcher,这样我们既可以知道哪份数据被订阅了,也能知道谁订阅这份数据,最后通过触发Dep的Watcher原型上的update方法实现视图的更新。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值