阅读 37

Vue源码分析① 响应式原理

在以前的前端开发中,用到的第三方库如JQuery,当我们所依赖的数据发生变化时,如果要在页面中产生相应的变化,通常直接操作Dom元素,改变其中的内容,从而使页面重新渲染,到达我们的目的。

    var el = $('#app')
    el.innerText = .......
复制代码

而在Vue项目中,我们的页面是由一个个组件所构成的,当我们组件所依赖的数据发生改变时,页面中相应的内容会自动地进行动态更新,因此我们说Vue是响应式的。

<body>
    <div id="app">
    </div>

    <script>
        new Vue({
            template: 
                `<div>
                    <h1 :style="{fontSize: title.size}">{{title.text}}</h1>
                    {{content}}
                </div>`,

            data(){        
                return {            
                    content: 'the content of app',
                    title: {
                        size: '14px',
                        text: 'the title of app'
                    }
                }
            }
        }).$mount('#app')
    </script>
</body>
复制代码

当vue实例中的content和title属性发生变化时,页面会自动重新渲染,那么,在Vue的底层是如何实现这么神奇的功能的呢?

在Vue中,主要使用观察者模式(设计模式中的一种)来实现这个响应式的功能

我们先介绍一下这个Vue源码中实现响应式的几个重要的角色:

  • Data:数据对象,即在Vue实例中所使用(也可以称为依赖)的数据,如上面代码中的data对象(不只是data)
  • Observer:主要功能是为Data中的每一个数据都加上getter/setter方法,使他们具有拦截操作符 ‘=’的功能
  • Dep: 依赖列表,Data中的每一个数据(如上文中的content)都会有一个Dep与该数据对应,Dep实际上是一个Watcher实例(等下会介绍)的列表,例如content所对应的Dep存储了使用content这个数据的所有Watcher
  • Watcher: 观察者,Watcher是一个中介的角色,负责对特定数据进行侦测,当数据发送改变时会通知到Watcher,而后Watcher在对相应的Vue实例进行相应的操作。

现在我们使用VSCode进行源码调试:

非Array类型数据对象的响应式原理

Observer源码

image-20210409142223517.png

如我们所见,Observer是一个构造函数,该函数接收一个value的参数,提供调试控制台我们可以得知value就是我们所依赖的数据对象。

我们开始逐行对源码进行分析:

//将传入的参数value暂存到Observer对象的value属性中,之后会使用到
this.value = value;

//创建一个Dep实例,存储数据的依赖列表
this.dep = new Dep();

this.vmCount = 0;

//该操作将this,即Observer实例
def(value, '__ob__', this);

//判断value是不是一个数组,是的话需要做特殊处理,
if (Array.isArray(value)) {
    //关于value为数组的情况下的处理操作等下单独讲
    ......
} else {
    this.walk(value);
}
复制代码

由上面代码可知,如果value不是Array类型是数据,那么我们执行walk方法,进行下一步的操作:

//从这里我们可以得知,walk是Observer原型上的方法
Observer.prototype.walk = function walk (obj) {

    //获取参数obj(即刚才传入的value)自身的所有可枚举的属性名,
    var keys = Object.keys(obj);
    
    //对获取到的属性名列表进行遍历
    for (var i = 0; i < keys.length; i++) {
    
        //对遍历的每一个key都调用defineReactive$$1方法
      	defineReactive$$1(obj, keys[i]);
    }
};
复制代码

由上面代码可知,我们对Vue实例中数据对象的每一个属性都调用了defineReactive$$1方法,这个方法是实现响应式的关键,源码如下:

function defineReactive$$1 (
    obj,	//数据对象,即最开始的value属性
    key,	//obj中的属性名,如content、title
    val,	//obj对象上与key对应的属性值,在刚才我们没有传入这个参数,所以为undefined
    customSetter,	
    shallow
  ) {
    
    //创建一个Dep实例,这个非常重要,存储了依赖 obj[key] 这个数据的依赖列表
    var dep = new Dep();
	
    //获取key的属性描述符,我们知道,如果一个属性是不可配置的(即configurable为false),那么我们就无法为其添加访问器(getter/setter)
    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
      return
    }

    // 获取该属性的访问器
    var getter = property && property.get;
    var setter = property && property.set;
    
    //如果属性上还没有配置getter方法,且方法参数的长度为2(即只有obj和key参数,没有提供val参数)
    //我们通过obj[key]获取该属性在数据对象上的默认值
    if ((!getter || setter) && arguments.length === 2) {
      val = obj[key];
    }
	
    /**
    	我们通过上面知道,构建Observer实例主要是为数据对象value的每一个属性都增加getter/setter方法来拦截访问操作,
    	如果value对象里面又嵌套了对象,如我们上面调试控制台的value: 
            value: {
                content: 'the content of app',
                title: {
                    size: '14px',
                    text: 'the title of app'
                }
            }
    	由于value中的属性title也是一个对象,也就是title也有自己的属性,那么我们需要对title这个属性也调用observe方法,即对	
    	title中的每个属性都添加getter/setter方法,如此递归,直到属性不再是对象类型。
    	
    	之所以要这么做,是因为如果只对title增加的getter/setter方法,当我们改变title的属性时,如 title.size = '20px',由于
    	title的值(一个指向title对象的指针)并不会修改,也就不会触发title的setter方法,那么我们也就无法侦测到数据的变化了
    	
    	这里的shallow作为参数传入,如果该参数有值,即进行浅侦测,不会对对象类型的属性进行递归侦测
    */
    var childOb = !shallow && observe(val);
        
    //这里开始进行真正的操作
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        //获取obj[key]的值,用于等下作为返回值返回
        var value = getter ? getter.call(obj) : val;
          
        /**
        可能有人会好奇为什么要判断Dep.target是否有值呢?
        我们之前有说,dep中存放的是一个Watcher列表,而Watcher代表了一个中介,负责为一个组件侦测数据的变化
        而Watcher在创建后会放在Dep.target这个位置上。
        
        可能有人会问为什么要放在这里,其实Dep.target只是一个全局的属性,只是为了让我们在任何地方都可以方便的存取,换成别的
        全局变量也是可以的。
        */
        if (Dep.target) {
          //这里负责将Watcher加入到dep的依赖列表里,注意:这里的这个Watcher是针对于组件的,而不仅仅是一个属性
          dep.depend();
            
          //如果该属性有子属性,那么也会为这个属性的依赖列表进行添加操作
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
          
        //返回属性值,因为这是我们需要满足的最基本操作
        return value
      },
      set: function reactiveSetter (newVal) {
      
        //先对属性进行修改操作,返回值是修改前的旧值
        var value = getter ? getter.call(obj) : val;
          
        //这里会对属性的旧值和新值进行比较,如果两个值相等,那么没有必要通知Watcher,直接返回
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        
        //如果方法有传入参数作为setter方法,那么我们调用它
        if (customSetter) {
          customSetter();
        }
          
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
          
       	//这里与上面getter方法的操作相同
        childOb = !shallow && observe(newVal);
          
        //对dep进行通知,而dep接收到通知后会对遍历它存储的每个Watcher,对每个Watcher进行通知
        dep.notify();
      }
    });
  }
复制代码

至此,关于Observer的源码分析就结束了

Dep源码

//从代码可知,Dep是一个构造函数
var Dep = function Dep () {
    this.id = uid++;
    
    //这里存储了dep所管理的Watcher列表
    this.subs = [];
};

//接收一个Watcher实例的参数,将其加入到依赖列表中
Dep.prototype.addSub = function addSub (sub) {
    this.subs.push(sub);
};

//接收一个Watcher实例的参数,将其从依赖列表中删除
Dep.prototype.removeSub = function removeSub (sub) {
    remove(this.subs, sub);
};

/**
	我们知道Dep.target是一个全局变量,存放的是当前操作环境中的Watcher实例
	其实不止是dep有一个Watcher列表,Watcher实例中也会有一个Dep实例的列表,从这个列表中Watcher可以知道自己被哪些Dep所管理
	因此我们知道,Dep和Watcher之间其实是一个双向绑定的关系:
	Watcher ——————→ Dep
	   ↑		    |
	   |_____________|
*/
Dep.prototype.depend = function depend () {
    if (Dep.target) {
        Dep.target.addDep(this);
    }
};

//这个方法比较重要,dep遍历自己管理的Watcher列表,对他们逐个进行通知
Dep.prototype.notify = function notify () {
    // stabilize the subscriber list first
    var subs = this.subs.slice();
    if (!config.async) {
        subs.sort(function (a, b) { return a.id - b.id; });
    }
    for (var i = 0, l = subs.length; i < l; i++) {
        subs[i].update();
    }
};
复制代码

Watcher源码

 var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    
   //vm就是我们当前环境中的Vue实例,也就是组件,_watchers存放了该Vue实例的所有Watcher,这里将创建中的Watcher加入_watchers
    vm._watchers.push(this);
   
   //这是构造Watcher实例中一些可选的参数,其中lazy与Vue实例的computed有关
    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.lazy = !!options.lazy;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid$2; 
    this.active = true;
    this.dirty = this.lazy; 
    
    //这个属性存储了当前Watcher实例被哪些Dep实例所管理
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    
    /**
    	如果传入的参数表达式是函数,则将getter属性赋值为该参数
    	比如 {{title.text}} 这个表达式指向一个指,因此不是函数类型
    	而 {{getTitle}}  这里如果getTitle是Vue实例的一个函数,那么这个表达式就是函数类型
    */
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      
      //如果不是函数类型,那么将表达式解析成一个函数,比如将 title.text 解析为一个函数,执行该函数会获得title[text]的值
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        warn(
          "Failed watching path: \"" + expOrFn + "\" " +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        );
      }
    }
        
    //调用get方法,位于Watcher原型上
    this.value = this.lazy
      ? undefined
      : this.get();
  };


 Watcher.prototype.get = function get () {
    //将Watcher放在Dep.target上
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
      /**
      	执行getter方法,由上面Watcher的构造函数可知,getter为函数类型的参数表达式或对参数表达式解析后的结果
      	这个方法非常重要,我们需要知道一个前提条件,当代码执行到这里时,Observer实例已经构造完成,也就是数据对象的每个属性
      	都具有访问器getter/setter了
      	
      	当我们执行this.getter时,如果它是一个函数,比如是一个计算属性
            getAllMessage(){
            	return this.title.text + this.content
            }
        执行这个函数会依次触发title.text的getter和this.content的getter,由上述Observer源码分析可知,在getter方法会执行
        dep.append()方法,即将Dep.target上的Watcher实例加入到与该属性对应的Dep实例上,现在title.text和content的dep上都已		 经有该Watcher了
      */
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
      } else {
        throw e
      }
    } finally {
      if (this.deep) {
        traverse(value);
      }
      
      //当把Watcher添加到对应的dep中后,会执行该方法,将Dep.target设置为null
      popTarget();
      this.cleanupDeps();
    }
    return value
  };
复制代码

Array类型的数据对象的响应式原理

Array类型与非Array类型的数据的响应式配置有如下不同:

由于对数组的修改可以通过push、pop、shift、unshift等方法实现。而这些方法都位于Array的原型上,因此需要对这些方法进行拦截,在修改数组元素的同时,通知该数组的依赖。Vue中通过Array.prototype生成一个实例,并让该实例对原来Array.prototype的方法进行增强,并替换需要侦测的Array对象的原型。

通过增强原型方法,原型上的方法具有了和Object类型中的访问器相同的能力,在之中对数组的修改进行通知。

总结

image-20210409211950388.png

文章分类
前端
文章标签