Vue 响应式原理

167 阅读5分钟

一、响应式数据

  • 响应式数据是单向的,这和双向绑定不一样,指的是能监控到数据变化,并且更新视图

  • vue2 的响应式原理的核心是使用的es5中的 Object.defineProperty API来实现数据相应式的,这个api提供了 getter 和 setter 方法来实现数据劫持,进而追踪依赖,使得 vue 可以来监听对象的属性访问或者对象的属性修改

二、Object.defineProperty 缺点

  1. 深度监听,需要递归到底,一次性计算量大
  2. 无法监听新增、删除属性(需要 vue.set 和 vue.delete)
  3. 无法原生监听数组,需要特殊处理

三、核心代码

  • 在new Vue之后,vue 会将在 data 中定义好的数据使用 Object.defineProperty 变成相应式数据,并给每一个数据都分配一个 dep 实例,该实例就是用来在运行 get 函数时让 watcher 做依赖收集,当数据发生变化时,便运行 set 函数,在该函数里面便会进行派发更新。vue 里面用到了观察者模式,默认组件渲染的时候,会创建一个 watcher,并且赋值给了 Dep.target,当渲染视图的时候,会取 data 中的数据, 会走每个属性的 get 方法,然后判断 Dep.target 是否有值,如果有则调用了 dep.depend(),就让这个属性的 dep 记录 watcher,同时也让 watcher 知道了哪些 dep 收集了自己。渲染完成之后,数据也劫持完了,依赖也收集完了,vue 就会将 Dep 上的 target 设置为 null。当数据更新的时候,运行了 set 方法,进行依赖派发更新 dep.notify(),其实也就是把 dep 中收集的 watcher 实例遍历一次,然后一次运行 watcher 实例上的 update 函数,这个函数里面,会再次运行的 render 函数或者其他的依赖函数。如果是render函数的话,视图自然就会更新了

  • 其中里面有很多个细节点要注意:

  1. 若对象上的属性为数组,就改写数组的原型链,重写数组的方法,不会对数组中的每一项进行 defineProperty
  2. 若数组里面包含引用类型,则需要再次递归,进行 defineProperty
  3. 若一个数据已经被观测过了,就添加一个__ob__属性, 表示已经被观测过了 observe.js
import { isArray, isObject } from "../utils";
import { arrayMethods } from "./array";
import Dep from "./dep";

// 1.每个对象都有一个__proto__属性 它指向所属类的原型   fn.__proto__ = Function.prototype
// 2.每个原型上都有一个constructor属性 指向 函数本身 Function.prototype.constrcutr = Function


class Observer {
    constructor(value) {
        // 不让__ob__ 被遍历到
        // value.__ob__ = this; // 我给对象和数组添加一个自定义属性

        // 如果给一个对象增添一个不存在的属性,我希望也能更新视图 
        // 给对象和数组都增加dep属性  {}.__ob__.dep  [].__ob__.dep
        this.dep = new Dep();
        Object.defineProperty(value, '__ob__', {
            value: this,
            enumerable: false // 标识这个属性不能被列举出来,不能被循环到
        })
        if (isArray(value)) {
            // 更改数组原型方法, 如果是数组 我就改写数组的原型链
            value.__proto__ = arrayMethods; // 重写数组的方法
            this.observeArray(value);
            // 数组 如何依赖收集 , 而且数组更新的时候 如何触发更新?  [].push .pop...
        } else {
            this.walk(value); // 核心就是循环对象
        }
    }
    observeArray(data) { // 递归遍历数组,对数组内部的对象再次重写 [[]]  [{}]
        // vm.arr[0].a = 100;
        // vm.arr[0] = 100;
        data.forEach(item => observe(item)); // 数组里面如果是引用类型那么是响应式的
    }
    walk(data) {
        Object.keys(data).forEach(key => { // 要使用defineProperty重新定义
            defineReactive(data, key, data[key]);
        });
    }
}
// vue2 应用了defineProperty需要一加载的时候 就进行递归操作,所以好性能,如果层次过深也会浪费性能
// 1.性能优化的原则:
// 1) 不要把所有的数据都放在data中,因为所有的数据都会增加get和set
// 2) 不要写数据的时候 层次过深, 尽量扁平化数据 
// 3) 不要频繁获取数据
// 4) 如果数据不需要响应式 可以使用Object.freeze 冻结属性 

// [[[]],{}]  让数组里的引用类型都收集依赖
function dependArray(value) {
    for (let i = 0; i < value.length; i++) {
        let current = value[i];
        current.__ob__ && current.__ob__.dep.depend();
        if (Array.isArray(current)) {
            dependArray(current)
        }
    }
}

function defineReactive(obj, key, value) { // vue2 慢的原因 主要在这个方法中
    let childOb = observe(value); // 递归进行观测数据,不管有多少层 我都进行defineProperty
    // childOb 如果有值 那么就是数组或者对象
    // childOb.dep 就可以是数组或者对象的 dep
    let dep = new Dep(); // 每个属性都增加了一个dep 闭包
    Object.defineProperty(obj, key, {
        get() {

            // 初次渲染时候 Dep.target 就是 watcher
            if (Dep.target) {

                // 对取值的 data 属性做依赖收集,比如 data:{a:1} 中的 a ,做依赖收集
                dep.depend();

                // 取属性的时候,会对对应的值(对象本身和数组)进行依赖收集
                // 比如 data:{a:[1,2]} , 这里是对 [1,2] 做依赖收集
                if (childOb) {

                    // 让数组和对象也记住当前的 watcher
                    childOb.dep.depend();

                    // 对数组中的引用类型进行依赖收集 (可能是数组套数组的可能)
                    if (Array.isArray(value)) {
                        dependArray(value);
                    }
                }

            }
            return value; // 闭包,次此value 会像上层的value进行查找
        },
        // 一个属性可能对应多个watcher, 数组也有更新
        set(newValue) {  // 如果设置的是一个对象那么会再次进行劫持
            if (newValue === value) return
            observe(newValue);
            value = newValue
            dep.notify(); // 拿到当前的dep里面的watcher依次执行
        }
    })
}
export function observe(value) {
    // 1.如果value不是对象,那么就不用观测了,说明写的有问题
    if (!isObject(value)) {
        return;
    }
    if (value.__ob__) {
        return; // 一个对象不需要重新被观测
    }
    // 需要对对象进行观测 (最外层必须是一个{} 不能是数组)

    // 如果一个数据已经被观测过了 ,就不要在进行观测了, 用类来实现,我观测过就增加一个标识 说明观测过了,在观测的时候 可以先检测是否观测过,如果观测过了就跳过检测
    return new Observer(value)
}


// 1.默认vue在初始化的时候 会对对象每一个属性都进行劫持,增加dep属性, 当取值的时候会做依赖收集
// 2.默认还会对属性值是(对象和数组的本身进行增加dep属性) 进行依赖收集
// 3.如果是属性变化 触发属性对应的dep去更新
// 4.如果是数组更新,触发数组的本身的dep 进行更新
// 5.如果取值的时候是数组还要让数组中的对象类型也进行依赖收集 (递归依赖收集)
// 6.如果数组里面放对象,默认对象里的属性是会进行依赖收集的,因为在取值时 会进行JSON.stringify操作


array.js

// 获取数组的老的原型
let oldArrayPrototype = Array.prototype;

// 让 arrayMethods 通过 __proto__ 能获取到数组的方法
// 等价于 arrayMethods.__proto__ == oldArrayPrototype
export let arrayMethods = Object.create(oldArrayPrototype);


let methods = [ // 只有这七个方法 可以导致数组发生变化
    'push',
    'shift',
    'pop',
    'unshift',
    'reverse',
    'sort',
    'splice'
]
methods.forEach(method => {
    arrayMethods[method] = function (...args) {
        // 数组新增的属性 要看一下是不是对象,如果是对象 继续进行劫持
        // 需要调用数组原生逻辑
        oldArrayPrototype[method].call(this, ...args)
        // todo... 可以添加自己逻辑 函数劫持 切片
        let inserted = null;
        let ob = this.__ob__;
        switch (method) {
            case 'splice': // 修改 删除  添加  arr.splice(0,0,100,200,300)
                inserted = args.slice(2); // splice方法从第三个参数起 是增添的新数据
                break;
            case 'push':
            case 'unshift':
                inserted = args;// 调用push 和 unshift 传递的参数就是新增的逻辑
                break;
        }
        // inserted[] 遍历数组 看一下它是否需要进行劫持
        if (inserted) ob.observeArray(inserted)
    }
});

// 属性的查找:是先找自己身上的,找不到去原型上查找


// 拓展
// 1、Object.create用法
// 用法: Object.create(object, [propertiesObject])
// 创建一个新对象,继承object的属性,可添加propertiesObject添加属性,并对属性作出详细解释(此详细解释类似于defineProperty第二个参数的结构)

dep.js

let id = 0;
// dep.subs = [watcher];
// watcher.deps = [dep]
class Dep {
    constructor() { // 要把watcher放到dep中
        this.subs = [];
        this.id = id++;
    }
    depend() {

        // 要给watcher也加一个标识 防止重复
        // this.subs.push(Dep.target); // 让dep记住这个watcher, watcher还要记住dep  相互的关系

        Dep.target.addDep(this); // 在watcher中在调用dep的addSub方法

    }
    addSub(watcher) {
        this.subs.push(watcher); // 让dep记住watcher
    }
    // 通知该 dep 对应的所有的 watcher
    notify() {
        this.subs.forEach(watcher => watcher.update());
    }
}

Dep.target = null; // 这里我用了一个全局的变量 ,类似 window.target  静态属性
export default Dep;

watcher.js

import Dep from "./dep";
let id = 0;
class Watcher {
    constructor(vm, fn, cb, options) { // 要将dep放到watcher中
        this.vm = vm;
        this.fn = fn;
        this.cb = cb;
        this.options = options;
        this.id = id++;
        this.depsId = new Set();
        this.deps = [];
        this.getter = fn; // fn 就是页面渲染逻辑 updateComponents
        this.get(); // 表示上来后就做一次初始化
    }
    addDep(dep) {
        let did = dep.id;
        if (!this.depsId.has(did)) {
            this.depsId.add(did);
            this.deps.push(dep); // 做了保存id的功能 并且让 watcher 记住 dep
            dep.addSub(this);
        }
    }
    get() {
        Dep.target = this; // Dep.target = watcher
        this.getter(); // 页面渲染的逻辑  vm.name / vm.age  
        Dep.target = null; // 渲染完毕后 就将标识清空了, 只有在渲染的时候才会进行依赖收集
    }
    update() {
        console.log('update')

        // 可以做异步更新处理
        this.get();
    }
}


export default Watcher