美文网首页让前端飞Web前端之路我爱编程
源码阅读:Vue的响应式原理(一)

源码阅读:Vue的响应式原理(一)

作者: Lxylona | 来源:发表于2018-04-08 15:20 被阅读126次

    前言

    1. observer部分完整的源码注释放在github上了,有兴趣的可以去看看,如果发现有误情不吝赐教!observer
    2. 这篇文很长长长长长长长长,而且比较费脑,我也整理了很久,如果对这篇文感兴趣,请自带☕️和无限的耐心~
    3. 这篇文只是Vue的响应式原理的一部分,后面还有很多很多很多的知识本文没有涉及到。

    Vue的双向数据绑定和Angular很不一样。
    Angular采用的是“脏检查”的方法,当我们触发了某些事件(定时,异步请求,事件触发等),执行完事件之后,Angular会对所有“注册”过的值进行一遍“全面检查”,也就是遍历所有的值,判断是否和之前的一致。这种方法效率不高,因为我们修改一个小地方都会带来两次以上的全面检查,如果我们绑定的view比较多,就可能会存在比较明显的性能问题了。
    而Vue的处理方式则不同,它结合观察者模式发布-订阅模式,当我们改变了一个数值,它会主动通知与它相关的订阅者,告诉他们可以进行相关的操作了,这种方法和“脏检查”相比,更加优雅,效率会更高。

    1. 响应式的基石:Object.defineProperty(obj, prop, descriptor)

    MDN : Object.defineProperty(obj, prop, descriptor)

    我们都知道对象有两种属性,一种是数据属性,一种是访问器属性。
    数据属性有4个特性:configurable, enumerable, value, writable
    访问器属性也有4个特性:configurable, enumerable, get, set

    平时我们通过普通赋值的方法(比如:obj.a - 'a')添加的属性都是数据属性,且默认configurable,和enumerable都为true
    而用Object.defineProperty()可以添加数据属性或者访问器属性,默认configurableenumerable都为false

    关于getter/setter
    访问器属性是没有value的,但是他们可以用来劫持对另一个数据的访问,举个例子:

    var log = console.log.bind(console);
    var obj = {
        _year: 2017
    };
    Object.defineProperty(obj, 'year', {
        get: function getter () {
            return this._year;
        },
        set: function setter (value) {
            this._year = value;
        }
    });
    log(obj.year); // 2017
    obj.year = 2018;
    log(obj._year); // 2018
    

    这个例子中我们访问obj.year,会返回obj._year的值,我们修改obj.year,会修改obj._year的值。

    根据这个特性,我们可以实现视图-数据双向绑定:

    <body>
        <p id="test-p">lalal</p>
    </body>
    <script>
        var log = console.log.bind(console);
        var obj = {}
        Object.defineProperty(obj, 'test-p', {
            get: function getter () {
                return document.getElementById('test-p').innerHTML;
            },
            set: function setter (value) {
                document.getElementById('test-p').innerHTML = value;
            }
        });
        log(obj['test-p']);
        setTimeout(function changeData () {
            obj['test-p'] = 'hahah';
        }, 3000);
    </script>
    

    是不是特别好玩?

    2. 观察者模式

    维基百科:观察者模式

    在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。
    一图胜千言,我画了张简单的流程图,应该很容易看懂:

    观察者模式

    详细一点讲,流程大概是这样的:

    目标对象有这么几个方法:

    1. setState:设置对象的状态,该函数调用了NotifyObserver方法
    2. getState:取得对象当前的状态
    3. addObserver:添加观察者
    4. removeObserver:删除观察者
    5. NotifyObserver:通知观察者:我的状态改变了,该方法会调用各个观察者的Notify方法

    观察者对象有个方法:
    Notify:该方法会会调用目标对象的getState方法,然后对目标对象的新值作出一些反应,比如说,打印出来之类的。

    如果我们写一个最简单的观察者模式,那可能是这样的:

    var log = console.log.bind(console);
    
    function Oberser (target, cb) {
      (function(){ // 添加到目标对象
        target.addOberser && target.addOberser(this);
        console.log('oberser added')
      }).call(this);
      
      this.notify = cb;
    }
    
    var target = {
      _value: 2017,
      obersers: [],
      addOberser: function (oberser) { // 添加观察者
        this.obersers.push(oberser);
      },
      removeOberser: function (oberser) { // 删除观察者
        // ...
      },
      notifyOberser: function () { // 通知观察者
        this.obersers.map(oberser => oberser.notify && oberser.notify());
      }
    }
    
    Object.defineProperties(target, {
      value: {
        get: function () {
          return this._value;
        },
        set: function (newValue) {
          this._value = newValue;
          this.notifyOberser(); // 调用notifyOberser
        }
      }
    });
    
    var oberser1 = new Oberser(target, function () {
        log(`I'm observer1, the value of my target is ${target.value}`);
    });
    var oberser2 = new Oberser(target, function () {
        log(`I'm observer2, the value of my target is ${target.value}`);
    });
    
    target.value = 2018;
    /*
    oberser added
    oberser added
    I'm observer1, the value of my target is 2018
    I'm observer2, the value of my target is 2018
    */
    

    当然了如果我们要观察同一个对象中的多个属性,就不能用这种方法了,因为我们总不能一个属性更新,所有观察者都全部调用一遍吧?最好是每一个属性都能有自己的观察者。

    3. 正题

    怎么给每个对象都维护一个观察者的列表呢?Vue是这样做的:
    Vue在观察者模式中结合发布-订阅模式,其中涉及到了三个重要的对象:Observer, Dep, Watcher
    Observer负责观察目标数据的变化,如果数据变化了,那么通知Dep。
    Dep负责维护一个订阅者列表(收集依赖),当接收到Observer的通知时,他就通知所有订阅者:目标数据更新了。
    Watcher维护一个回调函数,当接收到Dep的通知时,执行回调函数。

    Vue响应式原理

    可以这么理解:Observer是教师,Dep是教学在线,Watcher是学生。教师不必维护自己的学生列表,教务处帮他维护。学生不必维护自己的课表,因为教务处也会帮他维护。每次教师布置了新作业等(比喻不是很恰当),他只需要跟教学在线说一声就可以了,教学在线就发邮件告诉每一个上了这门课的学生:有新作业了。学生就可以分别对这个新作业作出不同的反应。

    原理已经了解得差不多了,接下来看一下源码吧。
    先看一下 Observer类,Vue会给每一个响应式的数据添加一个observer,这个observer就负责观察这个数据有没有发生变化。
    中文注释是我加的,英文注释是作者加的,不要漏了英文注释,很重要!

    export class Observer {
      value: any; // 被观察的对象,比如vue的根属性data,在vue实例初始化的时候,vue会为data属性添加一个observer对象,介时observer对象的value属性指向data,而data的__ob__属性指向observer对象
      dep: Dep; // 每一个observer对象都有一个dep,负责收集依赖和通知
      vmCount: number; // number of vms that has this object as root $data
    
      constructor (value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        def(value, '__ob__', this) // value的__ob__属性指向这个observer对象本身,比如L38注释中说到的data属性
        if (Array.isArray(value)) { // 如果value为数组,那么增强这个数组
          const augment = hasProto
            ? protoAugment
            : copyAugment
          augment(value, arrayMethods, arrayKeys)
          this.observeArray(value) // 往下递归数组,如果数组中有元素为对象或者数组,也会给其添加observer
        } else { // 如果value为对象,那么往下递归对象,如果对象中有属性为对象或者数组,也会给其添加observer
          this.walk(value)
        }
      }
    
      /**
       * Walk through each property and convert them into
       * getter/setters. This method should only be called when
       * value type is Object.
       */
      walk (obj: Object) { // 遍历对象,把对象中的属性都转化为getter/setter对
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
          defineReactive(obj, keys[i]) // 很重要!
        }
      }
    
      /**
       * Observe a list of Array items.
       */
      observeArray (items: Array<any>) { // 遍历数组,如果数组中有元素为对象或者数组,也会给其添加observer
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
      }
    }
    

    Observer类的constructor函数中,可以看到对于数组和对象,Vue的处理是不一样的,为什么呢?
    数组是没有Object.defineProperty(obj, prop, descriptor)这个方法的,这就意味着我们没有办法监听数组中属性的添加,删除。
    如果你在Vue中处理过数组,你应该知道,在Vue中,数组只有7个常用方法可以触发视图的更新:push(), pop(), shift(), unshift(), splice(), sort(), reverse()。这是因为Vue对这些方法进行了增强,原理很简单,类似于这样(当然实际上要严谨一些,这里只是帮助理解):

    var log = console.log.bind(console);
    var arrayMethods = Object.create(Array.prototype); // 继承自Array.prototype,保留了数组原本的特性
    arrayMethods.unshift = function (value) { // 重写方法
        Array.prototype.unshift.call(this, value); // 调用原来的方法
         notify(); // 并进行通知
    }
    function notify () {
        console.log('unshift');
    }
    var arr = [1, 2, 3];
    arr.__proto__ = arrayMethods;
    arr.unshift(0);
    log(arr)
    /*
    [ 1, 2, 3 ]
    unshift
    [ 0, 1, 2, 3 ]
    */
    

    上面的代码截断了数组的原型链,我们新创建了一个对象arrayMethods,这个对象继承自Array.prototype,然后改写里面的unshift()方法。这样既保证我们保留了数组的length等属性,有可能使用自己定义的unshift()方法,我们在unshift()方法中调用了notify()函数。

    Vue是怎么给每一个对象都加上一个Observer对象的?上面代码中,在处理数组的函数observeArray里,可以看到Vue遍历了一遍数组,并对每一个元素调用了observe()函数。
    而在处理对象的函数walk中,对每一个属性都调用了defineReactive函数(这个函数非常重要,后面再说),这个defineReactive函数内部也对属性都调用了一遍observe()函数。
    也就是说,Vue是通过observe()函数来给对象添加observer的。看一下observe()函数:

    export function observe (value: any, asRootData: ?boolean): Observer | void {
      if (!isObject(value) || value instanceof VNode) { // 只有对象或数组才会进入这个函数
        return
      }
      let ob: Observer | void
      if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // value已经有了自己的observer
        ob = value.__ob__
      } else if (
        shouldObserve &&
        !isServerRendering() &&
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue
      ) {
        ob = new Observer(value) // value为对象且没有自己的observer,那么为他新建一个observer,注意这里说明了Vue对每一层的属性或元素递归添加了observer
      }
      if (asRootData && ob) {
        ob.vmCount++
      }
      return ob
    }
    

    注意到里面有一行代码:ob = new Observer(value),也就是说,Vue递归遍历了每一层的属性或元素,如果这个元素/属性的类型为对象/数组,那么它也会有一个自己的observer。

    好,现在我们已经明白了Vue怎么遍历数组来把数组转化为响应式的了,那接下来再看看Vue如何处理对象属性:
    高能预警!

    export function defineReactive ( // 每个属性都转化为getter/setter,并且每个类型为对象(包括数组)的属性都会拥有自己的observer
      obj: Object, 
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean // shallow为true的话,属性不会有自己的observer,也就是该属性将不具备响应性
    ) {
      const dep = new Dep() // 注意这个函数将会出现两个dep,这里第一个dep,将会被闭包进getter/setter函数中
    
      const property = Object.getOwnPropertyDescriptor(obj, key)
      if (property && property.configurable === false) {
        return
      }
    
      // cater for pre-defined getter/setters
      const getter = property && property.get
      const setter = property && property.set
      if ((!getter || setter) && arguments.length === 2) {
        val = obj[key] // 注意,这个val也会被闭包进getter/setter方法中,我之前还疑惑把属性都转化为getter/setter值是怎么存储数据的,就是把这个val闭包进去的
      }
    
      let childOb = !shallow && observe(val) // 每一个observer会有一个dep属性,所以这里有了第二个dep,这个dep会在该属性的属性被增删的时候通知订阅者
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val // 执行属性原本自己有的getter
          if (Dep.target) { // 如果存在 Dep.target 这个全局变量不为空,表示是在新建 Watcher 的时候调用的
            dep.depend() // 这里是第一个dep,当Dep.target依赖于这个属性的时候,他会调用该属性的getter,这是dep.depend()就会把Dep.target添加进自己的订阅列表,这样在属性的setter被调用的时候,这个dep就可以通知Dep.target了
            if (childOb) {
              childOb.dep.depend() // 第二个dep也会收集依赖,那么该属性的属性被添加或者删除的时候,这个dep就可以通知这个属性的订阅者了
              if (Array.isArray(value)) { // 如果value为一个数组,那么是无法通过getters来窃听对数组元素的访问的,所以要向下遍历数组,给里面的元素都收集依赖
                dependArray(value)
              }
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          const value = getter ? getter.call(obj) : val
          /* eslint-disable no-self-compare */
          if (newVal === value || (newVal !== newVal && value !== value)) { // 没有变化/newVal为NaN/value为NaN
            return
          }
          /* eslint-enable no-self-compare */
          if (process.env.NODE_ENV !== 'production' && customSetter) {
            customSetter()
          }
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          childOb = !shallow && observe(newVal)
          dep.notify()
        }
      })
    }
    

    这个函数逻辑比较复杂,让我们好好来捋一下思路:
    1. 闭包的妙用
    如果你好好观察一下属性的getter/setter方法,你会发现他们闭包了这几个变量:
    getter, setter, val, dep,childOb

    其中,getter, setter两个变量可能是我们自己定义的getter/setter方法,因为我们有时候也会有需要访问器属性的时候。

    val是我们原本使用自己的getter/setter想要访问的值,比如这篇文章第一个代码块的_year属性。
    我之前还在疑惑,因为访问器属性是没有自己的值的,Vue把对象的属性转化为访问器属性之后,要怎么维护之前的值,原来是闭包进来了!
    看一下源码,当我们调用属性的setter方法的时候,最后修改的是这个val的值,而我们调用getter方法的时候,返回的也是这个val的值。

    dep, childOb在第3小节一起讲。

    2.Dep
    是时候介绍一下Dep类了,不然后面的讲不下去。
    Dep的结构很简单,大概长这样:

    export default class Dep { // dep是dependence的缩写,他负责收集依赖,以及通知订阅者。每一个Observer对象有其自己的的dep
      static target: ?Watcher;
      id: number;
      subs: Array<Watcher>; // 订阅者列表,
    
      constructor () {
        this.id = uid++
        this.subs = []
      }
    
      addSub (sub: Watcher) { // 添加订阅者
        this.subs.push(sub)
      }
     
      removeSub (sub: Watcher) { // 删除订阅者
        remove(this.subs, sub)
      }
    
      depend () { // 添加依赖,也就是把当前Dep.target添加到这个dep实例的subs列表中
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }
    
      notify () { // 通知watcher,执行所有watcher的.update()方法,更新watcher的数据
        // stabilize the subscriber list first
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
    }
    

    可以看到,一个Dep类会有一个自己的id,维护自己的一个订阅者列表,并切可以添加,删除,通知订阅者。
    Dep类中有一个静态属性Dep.target,学过C++的同学应该知道,静态属性也就是类属性,是所有实例共享的。这个target是干什么用的呢?

    Vue在处理一个watcher的时候,就会把Dep.target的值设为当前的watcher,举个例子,这是我们的Vue实例(该例子从官网中复制过来的):

    var vm = new Vue({
      el: '#example',
      data: {
        message: 'Hello'
      },
      computed: {
        // 计算属性的 getter
        reversedMessage: function () {
          // `this` 指向 vm 实例
          return this.message.split('').reverse().join('')
        }
      }
    })
    

    现在假设处理到了reversedMessage,先把Dep.target指向它。很明显reversedMessage依赖了message,我们是不是要在message对应的dep的订阅者列表中加上reversedMessage?问题来了,我们要怎么添加这个依赖?

    首先因为reversedMessage会访问到message,也就是会调用message的getter方法,那我们可以在getter方法中进行依赖收集,但是getter方法是没办法传参的,所以它也没办法知道谁订阅了它。

    这时候Dep.target就起作用了,我们前面已经说过,Vue处理到了哪个watcher,就会把Dep.target指向它,那么此时的Dep.target肯定就是reversedMessage,我们只需要在getter函数中把Dep.target添加进订阅者列表就可以了!
    那么这时,当我们改变message的值时,会调用其setter函数,setter函数中dep就会调用dep.notify()方法,通知reversedMessage:我更新了!

    真的太妙了!

    3. 两个dep
    源码中我在注释中也提醒过了,这个函数中出现了两个dep,一个是在函数开头就新建的dep,另一个是属性自己的observer中的dep。
    dep的作用是什么?收集依赖并在适当的时候通知订阅者:目标数据更新了。

    在源码中,属性的getter方法中,给dep, childOb都添加了依赖,为什么在setter方法中,只通知了dep?或者说,childOb的意义在哪里呢?

    先看一个例子:

    var obj = {
      _a: { aa: 1}
    };
    Object.defineProperty(obj, 'a', {
      configurable: true,
      enumerable: true,
      get: function () {
        log('get a:' + this._a);
        return this._a;
      },
      set: function (newV) {
        log('set a:' + newV);
        this._a = newV;
      }
    });
    obj.a; // get a:[object Object]
      obj.a.bb = 2; // get a:[object Object]
    delete obj.a;
    // 删除该属性的时候,没有调用getter/setter函数!
    

    可以很明显看出getter/setter的缺陷:只能监听到属性的更改,不能监听到属性的删除与添加。

    我们都知道Vue提供了内置的Vue.set(), Vue.delete()方法来让我们响应式的添加和删除数组的元素或对象的属性。
    官方文档
    官方文档是这么说的:

    Vue.set()这个方法主要用于避开 Vue 不能检测属性被添加的限制。
    Vue.delete()这个方法主要用于避开 Vue 不能检测到属性被删除的限制。

    我们前面已经证明了,setter是不会在属性被删除或者添加的时候调用的,那么Vue是怎么在删除和添加的时候通知watcher的?其实跟数组方法的增强事同一个套路,把Vue.delete()的源码简化简化再简化之后:

    function del () {
      delete obj.a;
      childOb.notify(); // 通知obj的watcher:我有一个属性删除了。
    }
    

    所以为什么在getter方法中要添加childOb的依赖,就是为了在删除或者添加属性的时候进行通知。

    4. 如何向下收集依赖
    是这样的,假设数据是这样的let data = {a: {b: {c: {d: {e: 1}}}}},有一个模板引用了{{a.b.c}},那么我们修改a.b.c.d.e,这时watcher会被通知到吗?

    答案是不会。为什么?一步一步来看。

    1. 首先我们知道每一层的属性,也就是a, b, c, d, e,都有自己的observer,而且watcher订阅observer是通过getter方法来实现的,没有getter方法就没法订阅。

    2. 它调用了c的getter方法,因此c更改了(整个对象被替换),会有通知,c删除了,也会有通知。

    3. 我们修改了a.b.c.d.e

    4. 模板引用的是{{a.b.c}},那么它没有调用到e的getter方法。

    5. 因此我们修改了e,watcher就没办法知道了。

    Vue是怎么解决这个问题的呢?

    它是这样做的:当模板引用了{{a.b.c}}时,此时Dep.target是这个模板,然后,Vue从c开始往下遍历,对每个属性都"touch"一下,也就是强行调用一下getter方法,这样,模板就加入了所有属性的订阅者列表中。

    有兴趣的同学可以自己去看一下Vue源码中的traverse.js

    5. dependArray函数
    终于要进入尾声了🙂️,看源码真的很费心神,但是收获真的超级大呀!
    defineReactive函数中的getter方法中,对数组有一个额外的处理过程:如果value为数组,那么对其执行dependArray函数。

    想了好久才想明白为什么要进一步的处理。

    回到最开始,我们给一个对象添加一个observer,那么他会遍历所有的属性,把属性都转化为getter / setter。

    但是给数组添加一个observer,他只是添加了8个具有响应性的方法。(当然他也会给子对象添加observer)

    这时我们push,pop数组,是响应式的,数组的dep知道他要通知订阅者们。

    但是如果我们改变的是数组的元素,比如,对于一个数组var arr = [1, 2, 3, {a: 4}],现在我们这样操作arr[0] = 0,数组是不会有响应的。

    这也是为什么vue给数组加了两个方法Vue.set, Vue.delete来添加和删除元素的原因。

    再回到这个问题上,我们已经有了Vue.set, Vue.delete两个方法,那我们操作基本类型的元素基本没啥问题了,但是如果是像arr[3].a = 5这种呢?Vue的解决方法就是递归遍历数组,遇到类型为object的元素,就把当前的Dep.target添加到它的订阅者列表中,这时它的变化就可以被监听了。

    这一切的根本原因,就是数组没法通过getter/setter对象来监听元素的变化。

    最后附上dependArray的源码。

    function dependArray (value: Array<any>) {
      for (let e, i = 0, l = value.length; i < l; i++) {
        e = value[i]
        e && e.__ob__ && e.__ob__.dep.depend() // //如果数组元素也是对象,那么他们observe过程也生成了ob实例,那么就让ob的dep也收集依赖
        if (Array.isArray(e)) {
          dependArray(e)
        }
      }
    }
    

    相关文章

      网友评论

        本文标题:源码阅读:Vue的响应式原理(一)

        本文链接:https://www.haomeiwen.com/subject/alkphftx.html