vue源码学习笔记三-数据响应式原理

107 阅读4分钟

MVVM

src=http___cdn.jsdelivr.net_gh_xugaoyi_image_store_blog_20200204123438.png&refer=http___cdn.jsdelivr.png

所谓响应式,其实本身就是对 MVVM (Modal View ViewModal)的理解

Vue 做了一层中间的 ViewModal,让视图上的改变能反映到 data 中, data 中的改变能反映到视图上。

ViewModal就是视图和数据的一个桥梁。

如同样是num + 1

Vue 中,这个桥梁是你看不见的,因为 Vue 帮你完成了视图和数据的变化传递。 而 React 就是侵入式的,因为要显式地声明 setState ,通过它,来设置变量的同时,设置视图的改变。

4.png

Vue 的神奇之处,不需要我们手动地显示调用 setState ,也就是这个桥梁, Vue 已经帮我们桥接上了。 要让 data 改变的同时,视图也发生改变,所以,问题的所在,就是我们需要监听,什么时候,这个变量发生了变量。 ES5 中,就有一个特性,可以做到对于数据的劫持(监听)。

它就是 Object.defineProperty 。

Object.defineProperty方法

var obj = {};
Object.defineProperty(obj, 'a', {
    get() {
        console.log('a属性');
        return 3
    },
    set() {
        console.log('改变a的属性')
    }
})
Object.defineProperty(obj, 'b', {
    value: 4
})

obj.a = 7;
console.log(obj)  // 此时发现a的值仍然没有变化  需要一个变量值来周转

1.png

getter/setter需要变量周转

var obj = {};
var temp;
Object.defineProperty(obj, 'a', {
    get() {
        console.log('a属性');
        return temp
    },
    set(val) {
        temp = val
        console.log('改变a的属性')
    }
})
Object.defineProperty(obj, 'b', {
    value: 4
})

obj.a = 7;
console.log(obj)  

2.png

defineReactive函数封装

let obj = {};
function defineReactive(data, key, val) {
    Object.defineProperty(data, key, {
        // 可枚举
        enumerable: true,
        // 可被配置,如delete
        configurable: true,
        get() {
            console.log('访问' + key + '属性');
            return val
        },
        set(newVal) {
            if (val === newVal) return;
            console.log('改变' + key + '的属性')
            val = newVal
        }
    })
}
defineReactive(obj, 'a', 3);
defineReactive(obj, 'b', 4);
console.log(obj.a)
console.log(obj.b)
obj.b++;
console.log(obj.b)

3.png

递归侦测全部属性

当对象层级比较深,defineReactive方法不能直接改变某个层级,需要递归侦测每个属性

5.png

代码目录结构

6.png

utils.js

export const def = function(obj,key,value,enumerable) {
    Object.defineProperty(obj,key,{
        value,
        enumerable,
        writable: true,
        configurable: true,
    })
}

defineReactive.js

import observe from "./observe";
export default function defineReactive(data, key, val) {
    if (arguments.length === 2) {
        val = data[key];
    }
    // 子元素observe,递归。这个递归不是自己调用自己,而是多个函数,类循坏调用
    let childOb = observe(val);
    Object.defineProperty(data, key, {
        // 可枚举
        enumerable: true,
        // 可被配置,如delete
        configurable: true,
        get() {
            console.log('访问' + key + '属性');
            return val
        },
        set(newVal) {
            if (val === newVal) return;
            console.log('改变' + key + '的属性')
            val = newVal;
            // 设置的值也需要observe
            childOb = observe(newVal)
        }
    })
}

Observer.js

import { def } from './utils';
import defineReactive  from './defineReactive';
// 将一个正常的object转换成每个层级的属性都是响应式的,可以被zhence的object
export default class Observer {
    constructor(value) {
        def(value,'__ob__',this,false);
        this.walk(value)
    }
    walk(value) {
        for(let k in value) {
            defineReactive(value,k)
        }
    }
}

observe.js

import Observer from "./Observer";
export default function (value) {
    if (typeof value != 'object') return;
    let ob;
    if (typeof value.__ob__ !== 'undefined') {
        ob = value.__ob__;
    } else {
        ob = new Observer(value);
    }
    return ob;
}

index.js

import observe from './observe'
let obj = {
    a: {
        b: {
            c: {
                d: 5
            }
        }
    },
    e: 10,
    f: {
        g: {
            h: 20
        }
    }
}
observe(obj);
obj.a.b.c = 1000;
console.log(obj.a.b.c) // 1000

4.png

数组的响应式处理

数组的七个方法被改写

  1. push
  2. pop
  3. shift
  4. unshift
  5. splice
  6. sort
  7. reverse

1.png vue中数组的变化侦测(响应式)如何实现?

Array.prototype为原型,创建了arrayMethods对象,使用es6中Object.setPrototypeOf(o,arrayMethods)方法强制将数组的__proto__指向arrayMethods(o.__proto__ = arrayMethods)触发新定义的函数

array.js

import { def } from "./utils";

// 得到Array.prototype
const arrayPrototype = Array.prototype;

// 以Array.prototype为原型,创建了arrayMethods对象
export const arrayMethods = Object.create(arrayPrototype);

// 要被改写的七个数组方法
const methodsNeedChange = [
  "push",
  "pop",
  "shift",
  "unshift",
  "sort",
  "splice",
  "reverse",
];

methodsNeedChange.forEach((v) => {
  // 备份原来的方法
  const original = arrayPrototype[v];

  // 定义新方法
  def(
    arrayMethods,
    v,
    function () {
      const result = original.apply(this, arguments);
      const args = [...arguments];
      // 数组已经添加了__ob__,
      const ob = this.__ob__;
      // 数组三种方法中push,unshift,splice能够插入新项,插入的新项也要oberve
      let inserted = [];
      switch (v) {
        case "push":
        case "unshift":
          inserted = args;
          break;
        case "splice":
          inserted = args.slice(2);
          break;
      }
      // 让插入的项也变成响应式的
      if (inserted) {
        ob.observeArray(inserted);
      }
      return result;
    },
    false
  );
});

Observer.js

import { def } from "./utils";
import defineReactive from "./defineReactive";
import { arrayMethods } from "./array";
import observe from "./observe";
// 将一个正常的object转换成每个层级的属性都是响应式的,可以被侦测的object
export default class Observer {
  constructor(value) {
    def(value, "__ob__", this, false);
    // 检测是数组还是对象
    if (Array.isArray(value)) {
      // 如果是数组,将数组的原型指向arrayMethods
      Object.setPrototypeOf(value, arrayMethods);

      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }
  walk(value) {
    for (let k in value) {
      defineReactive(value, k);
    }
  }
  observeArray(arr) {
    for (let i = 0; (length = arr.length), i < length; i++) {
      // 每个都进行observe
      observe(arr[i]);
    }
  }
}

index.js

import observe from './observe'
let obj = {
    a: {
        b: {
            c: {
                d: 5
            }
        }
    },
    e: 10,
    f: {
        g: {
            h: 20
        }
    },
    k:[1,2,3]
}
observe(obj);
obj.a.b.c = 1000;
console.log(obj.a.b.c)
obj.k.splice(2,1,88,99)
console.log(obj.k)

2.png

依赖收集

需要用到数据的地方,称为依赖

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

5.png

Dep类和Watcher类

把依赖收集的代码封装成一个Dep类,转么管理依赖,每个Observe的实例,成员都有一个Dep实例 Watcher是一个中介,数据发生变化时通过Watcher中转,通知组件

DepWatcher 在设计模式中,就是发布-订阅者的模式。

Dep

Dep 发布者,它的工作就是依赖管理,它会有多个订阅者。每个变量有属于自己的 Dep ,每个变量所在的依赖位置不一样,所以他们的订阅者也不一样。然后在变量更新之后,就去通知所有的订阅者(Watcher),变量更新,触发视图更新;

Watcher

Watcher 订阅者,它接受 Dep 发过来的更新通知之后,就去执行视图更新了。本质是 watch 监听器,变量改变之后,执行一个回调函数。

781dcc347fb74dad7b5e88530ecc180c.png

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就是一个我们自己指定的全局的位置
        if (Dep.target) {
            this.addSub(Dep.target);
        }
    }
    // 通知更新
    notify() {
        // 浅克隆一份
        const subs = this.subs.slice();
        // 遍历
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    }
};

Watcher.js

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 {
            value = this.getter(obj);
        } finally {
            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;
            cb.call(this.target, value, oldValue);
        }
    }
};

function parsePath(str) {
    var segments = str.split('.');

    return (obj) => {
        for (let i = 0; i < segments.length; i++) {
            if (!obj) return;
            obj = obj[segments[i]]
        }
        return obj;
    };
}

Observer.js

import { def } from "./utils";
import defineReactive from "./defineReactive";
import { arrayMethods } from "./array";
import observe from "./observe";
import Dep from './Dep'
// 将一个正常的object转换成每个层级的属性都是响应式的,可以被侦测的object
export default class Observer {
  constructor(value) {
    this.dep = new Dep();
    def(value, "__ob__", this, false);
    // 检测是数组还是对象
    if (Array.isArray(value)) {
      // 如果是数组,将数组的原型指向arrayMethods
      Object.setPrototypeOf(value, arrayMethods);

      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }
  walk(value) {
    for (let k in value) {
      defineReactive(value, k);
    }
  }
  observeArray(arr) {
    for (let i = 0; (length = arr.length), i < length; i++) {
      // 每个都进行observe
      observe(arr[i]);
    }
  }
}

defineReactive.js

import observe from "./observe";
import Dep from "./Dep";
export default function defineReactive(data, key, val) {
  let dep = new Dep();

  if (arguments.length === 2) {
    val = data[key];
  }
  // 子元素observe,递归。这个递归不是自己调用自己,而是多个函数,类循坏调用
  let childOb = observe(val);
  Object.defineProperty(data, key, {
    // 可枚举
    enumerable: true,
    // 可被配置,如delete
    configurable: true,
    get() {
      console.log("访问" + key + "属性");
      // Dep.target 是我们弄的唯一标识,当有这个标识的时候,添加依赖
      if (Dep.target) {
        // 添加依赖
        dep.depend();
        // 如果有子属性,也要将它加入依赖
        if (childOb) {
          // 给子属性添加依赖
          childOb.dep.depend();
        }
      }
      return val;
    },
    set(newVal) {
      if (val === newVal) return;
      console.log("改变" + key + "的属性");
      val = newVal;
      // 设置的值也需要observe
      childOb = observe(newVal);
      // notify val=newVal 之后,不然在 callback 回调中一直是旧值
      dep.notify();
    },
  });
}

index.js

import observe from './observe'
import Watcher from './Watcher'
let obj = {
    a: {
        b: {
            c: {
                d: 5
            }
        }
    },
    e: 10,
    f: {
        g: {
            h: 20
        }
    },
    k:[1,2,3]
}
observe(obj);
console.log(obj.a.b.c)
new Watcher(obj,'a.b.c', (val) => {
    console.log('val----------',val)
})
obj.a.b.c = 1000;
console.log('obj.a.b.c----------',obj.a.b.c)

8.png

9.png

总结

当创建Vue实例时,vue会遍历data选项的属性,利用Object.defineProperty为属性添加gettersetter对数据的读取进行劫持(getter用来依赖收集,setter用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。

每个组件实例会有相应的watcher实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有computed watcher,user watcher实例),之后依赖项被改动时,setter方法会通知依赖与此datawatcher实例重新计算(派发更新),从而使它关联的组件重新渲染

资源来源

B站视频地址