关于vue.js:Vue2源码解读三数据变化侦测数据响应式原理

38次阅读

共计 6500 个字符,预计需要花费 17 分钟才能阅读完成。

Vue2.X 官网文档中曾经论述了深刻响应式原理,简略来讲就是数据批改之后,被 es5 里边 Object .defineProperty,setter 拦挡到了,告诉 watcher,watcher 对函数进行渲染,这个过程种要创立新的虚构 dom 节点,比照旧的虚构 dom 节点,比照完之后做成一个补丁,把补丁打在实在 dom 构造中,实在 dom 再更新,视图产生扭转。

Object.defineProperty()数据劫持 / 数据代理

利用 javascript 引擎赋予的性能,检测对象属性变动
Object.defineProperty() 办法会间接在一个对象上定义一个新属性,或者批改一个对象的现有属性,并返回此对象。

var obj = {};
Object.defineProperty(obj, 'a', {value: 3})
Object.defineProperty(obj, 'b', {value: 5})
console.log(obj) // {a:3,b:5}
console.log(obj.a, obj.b) // 3 5

Object.defineProperty()能够设置额定暗藏的属性

Object.defineProperty(obj, 'a', {
    // value: 3,
    
    get(){},
    // 是否可写
    writable: true
})

Object.defineProperty()真正对数据的操作是他它本身的 getter 函数 (读取) 和 setter 函数 (设置) 来进行的:

Object.defineProperty(obj, 'a', {
    // getter 函数
    get(){console.log(ole.log('拜访 a 属性');
        return 7;
    },
    // setter 函数
    set(nVal) {console.log('批改 a 属性为'+nVal)
    }
})
console.log(obj.a); // 7
obj.a = 10;
console.log(obj.a); // 7

由以上示例可知,当拜访 obj 的 a 属性时,值为 7,当批改 a 属性的值之后,从打印后果看出,setter 函数的确执行了,然而新值并没有赋给 getter 的返回值,此时的 getter 和 setter 短少了一个连贯的桥梁:变量,所以下面的代码稍作改变:

var temp = '';
Object.defineProperty(obj, 'a', {
    // getter 函数
    get(){console.log(ole.log('拜访 a 属性');
        return temp;
    },
    // setter 函数
    set(nVal) {console.log('批改 a 属性为'+nVal);
        temp = nVal;
    }
})
console.log(obj.a); // 7
obj.a = 10;
console.log(obj.a); // 10

到这里 Object.defineProperty()的用法就曾经很分明了,接下来要做的就是怎么让它更好看优雅~

defineReactive 函数

var obj = {};
function defineReactive (data, key, val) {
// val 在 defineReactive 函数里给 getter,setter 函数营造了一个闭包环境,这样就不必再申明一个长期变量了
    Object.defineProperty(data, key, {
        // 可枚举
        enumerable: true,
        // 能够被配置,比方能够被 delete
        configurable: true,
        // getter 函数
        get(){console.log(ole.log('拜访 a 属性');
            return val;
        },
        // setter 函数
        set(nVal) {console.log('批改 a 属性为'+nVal);
            if (nVal === val) return;
            val = nVal;
        }
  })
}
defineReactive(obj, 'a', 10);
conso.log(obj.a); // 10
obj.a++; // 赋值时调用了 setter 函数
console.log(obj.a); // 11 

这个代码里边解决变量的问题,不过这个例子只满足单层的对象,那么像 obj:{a:{m:{n:5}}} 这种简单构造的数据,就须要进行逐层遍历,递归地调用 Object.defineProperty()去解决~

var obj = {
 a:{
   m:{n:5}
 }
}

拜访 obj.a.m.n 属性,不能每次都设置 a 属性,所以当有简单构造的对象时,defineReactive 函数须要作参数判断:

function defineReactive (data, key, val) {if (arguments.length == 2) {val = data[key]
    }
    ...
}
defineReactive(obj, 'a')
console.log(obj.a.m.n); // 当拜访 obj.a.m.n 属性,只拜访到了 a 这一层

递归侦测对象全副属性

对于 obj 的 a.m.n 属性,须要循环递归地实现 Object.defineProperty(),此时创立一个类 Observer,它次要是将一个一般对象的任何属性都能被侦测到的工具类:

stateDiagram-v2
    Observer(观察者) --> 将一个失常的 object 转换为每个层级的属性都是响应式(能够被侦测的)的 object            
Observer.js
export default class Observer {constructor(value) {
        // 每一个 Observer 的实例身上,都有一个 dep
        this.dep = new Dep()
        // 给实例(this,构造函数中的 this 不是示意类自身,而是示意实例)增加了__ob__属性,值就是 value(这次 new 的实例)def(value, '__ob__', this, false)
        // console.log('我是 Observer 结构器', value)
        // Observer 类的目标:将一个失常的 object 转换为每个层级的属性都是响应式(能够被侦测的)的 object
        // 查看它是数组还是对象
        if (Array.isArray(value)) {
            // 如果是数组,要十分强行,将这个数组的原型,指向 arrayMethods
            // setPrototypeOf 强制地定义 value 的原型
            Object.setPrototypeOf(value, arrayMethods)
            // 让这个数组变得 observe
            this.observeArray(value)
        } else {this.walk(value)
        }
    }
    // 遍历
    walk(value) {for (let k in value) {defineReactive(value, k)
        }
    }
    // 数组的非凡遍历
    observeArray(arr) {for (let i = 0, l = arr.length; i < l;i++) {
            // 逐项进行 observe
            observe(arr[i])
        }
    }
}

这个类被创立进去被实例化为对象才有意义,所以如何被实例化值得思考 – 创立一个 observe 函数

observe.js
import Observer from "./Observer";
// 这个函数只为对象服务
export const observe = function (value) {
    // 如果 value 不是对象,什么都不做
    if (typeof value !== object) return
    // 定义 ob,ob 就是要存储 Observer 的实例
    var ob;
    if (typeof value.__ob__ !== 'undefined') {
        // __ob__ 就是存储 Observer 类的实例的,区别于其余常见的属性
        ob = value.__ob__; // value 就是要侦测的对象,就是 defineReactive 中传入的 data,然而实用于 val = data[key]
    } else {ob = new Observer(value);
    }
    return ob;
}

obj 必然是先调用 observe 触发,再看这个对象有没有__ob__,如果没有,调用 New Observer(),将产生的实例增加到__ob__上,此时 obj 的 a 属性 active 的闭包环境了,因为 a 属性的值是 m:{n:5}}, 在 setter 函数中被设置的时候,m:{n:5}}作为新的对象又触发了 observe,也就是图上的遍历下一层属性。

总结起来就是对象 obj 先触发 observe,在 observe 实例化 Observer 作为 obj 的__ob__属性,而在 Observer 类的构造函数中,对对象进行了遍历,每一次遍历又调用了 defineReactive 设置属性,在设置属性时对子元素进行了 observe,至此造成了递归。

数组的响应式解决

Vue2 以 Array.prototype 为原型,创立了一个 arrayMethods 对象,而后用 ES6 中的 setPrototypeOf 强制地使 arr 的__proto__指向了 arrayMethods 对象,这样就能够调用 arrayMethods 对象中被重写的七个数组办法了,它们别离是 push、pop、shift、unshift、splice、sort、reverse。

array.js
import {def} from './utils.js';
const arrayPrototype = Array.prototype;
// 以 Array.prototype 为原型创立 arrayMethods 对象
// 裸露 arrayMethods
export const arrayMethods = Object.create(arrayPrototype)
// console.log(arrayMethods)

// 要被改写的七个数组办法
const methodsNeedChange = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
methodsNeedChange.forEach((methodName) => {// console.log('methodName', methodName)
    // 备份原来的办法, 因为 push,pop 等 7 个函数的性能不能被剥夺
    const original = arrayPrototype[methodName];
    // 把这个数组身上的__ob__取出来,__ob__曾经被增加了,因为数组必定不是最高层,比方 obj.g 属性是数组,obj 不能是数组,第一次遍历 obj 这个对象的第一层的时候,曾经给 g 属性(就是这个数组)增加了__ob__属性
    // 定义新的办法
    def(arrayMethods, methodName, function(){
        // 复原原来的性能
        const result =  original.apply(this, arguments);
        // 把类数组对象变成数组
        const args = [...arguments];
        // console.log(arguments);
        const ob = this.__ob__;
        // 有三种办法 push/unshift/splice 可能插入新项,当初要把插入的新项也要变为 observe 的
        let inserted = [];
        switch (methodName) {
            case 'push':
            case 'unshift':
                inserted = args;
                break;
            case 'splice':
                // splice 格局是 splice(下标,数量,插入的新项)inserted = args.slice(2)
                break;
        }
        // 判断有没有要插入的新项,让新项也变成响应式的
        if (inserted) {ob.observeArray(inserted);
        }
        console.log('lalala')
        ob.dep.notify()
        return result;
    }, false)
})

依赖收集

在 getter 中收集依赖,在 setter 中触发依赖

  • 把依赖收集的代码封装成一个 Dep 类,它专门用来治理依赖,每个 Observer 的实例,成员中都有一个 Dep 的实例
  • Watcher 是一个中介,数据发生变化时通过 Watcher 直达,告诉组件;
  • 依赖就是 Watcher。只有 Watcher 触发的 getter 才会收集依赖,哪个 Watcher 触发了 getter,就把哪个 Watcher 收集到 Dep 中;
  • Dep 应用公布订阅模式,当数据发生变化时,会循环依赖列表,把所有的 Watcher 都告诉一遍。
    Dep 类就是用来收集依赖,Watcher 就是依赖。

    Dep.js
    var uid = 0;
    export default class Dep {constructor() {// console.log('我是 dep 类的结构器')
          this.id = uid++;
          // 用数组存储本人的订阅者。subs 是英语 subscribes 订阅者的意思。// 这个数组外面放的是 Watcher 的实例
          this.subs = [];}
      // 增加订阅
      addSub (sub) {this.subs.push(sub)
      }
      // 增加依赖
      depend () {
          // Dep.target 就是一个咱们本人指定的全局的地位,你用 window.target 也行,只有全局惟一,没有歧义就行
          if (Dep.target) {
              // getter 函数就会从全局惟一的这个中央读取正在读取数据的 Watcher,并把这个 Watcher 收集到 Dep 当中
              this.addSub(Dep.target)
          }
      }
      // 告诉更新
      notify () {console.log('我是 notify')
          // 浅克隆一份
          const subs = this.subs.slice();
          // 遍历
          for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}
      }
    }
    import Dep from "./Dep";
    var uid = 0;
    export default class Watcher {constructor(target, expression, callback) {// console.log('我是 Watcher 类的结构器')
          this.id = uid++;
          this.target = target;
          this.getter = parsePath(expression)
          this.callback = callback;
          this.value = this.get()}
      update () {this.run()
      }
      get() {
          // 进入依赖收集阶段。让全局的 Dep.target 设置为 Watcher 自身,那么就是进入依赖收集阶段
          Dep.target = this;
          const obj = this.target;
          var value;
          // 只有能找,就始终找,try{}避免找不到
          try {value = this.getter(obj)
          } finally {
              // 退出依赖收集阶段,此 Watcher 把依赖收集阶段的资格让给别的 Watcher
              // 所有 Watcher 都在竞争,以后哪个 Watcher 正在读 getter,哪个 Watcher 就是 Dep 的 target
              Dep.target = null;
          }
          return value
      }
      run() {this.getAndInvoke(this.callback)
      }
      getAndInvoke(cb) {const value = this.get()
          if (value !== this.value || typeof value === 'object') {
              const oldValue = this.value;
              this.value = value;
              // this.callback()
              cb.call(this.target, value, oldValue)
          }
      }
    }
    function parsePath(str) {var segments = str.split('.');
      console.log('segments:', segments)
      return (obj) => {for (let i =0; i < segments.length; i++) {if (!obj) return;
              obj = obj[segments[i]]
          }
          return obj
      }
    }


    残缺代码:vue2 数据响应式原理

正文完
 0