关于vue数据绑定的极简原理

182 阅读2分钟

dep.js

/**
 * 对订阅者进行收集、存储和通知
 *
 * @class      Dep (name)
 */
export default class Dep {
    constructor() {
        this.subs = [];
    }

    addSub(sub) {
        this.subs.push(sub);
    }

    notify() {
        // 通知所有的订阅者(Watcher),触发订阅者的相应逻辑处理
        this.subs.forEach(sub => sub.update());
    }
}
Dep.target = null;
  • Dep类用来实例一个个的收集器,每个收集器用来存储对单个数据的订阅者,当该项数据发生变化时统一的去通知他们
  • Dep.target用来暂存当前订阅者

observer.js

import Dep from './dep';

function defineReactive(obj, key, val) {
    let dep = new Dep();

    // 给当前属性的值添加监听
    observer(val);
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: () => {
            console.log('get value: ', val)
            if (Dep.target) {
                dep.addSub(Dep.target)
            }
            return val;
        },

        set: (newVal) => {
            if (val === newVal) {
                console.log('无需更新')
                return;
            }
            console.log('new value setted: ', newVal)
            val = newVal;
            dep.notify();
        }
    })
}

export function observer(value) {
    if (!value || typeof value !== 'object') {
        return
    }

    Object.keys(value).forEach(key => defineReactive(value, key, value[key]));
}

  • 核心方法Object.defineProperty定义对象的属性访问器,从而达到对数据改变的监听。
  • observer首先判断数据是否是对象,Object.keys遍历对象的每个字段对其执行defineReactive添加监听
  • defineReactive首先进来实例化Dep,单个数据监听的收集器。这里再次对值observer实际上是类似递归,数据对象深层嵌套时继续监听。
  • enumerable、configurable可枚举、可配置。get获取属性时,会把监听者添加到dep中,这边实际上是个闭包。dep收集器会常驻内存保留着。
  • 当数据set设置时首先判断新值newVal是否与与旧值val相等,相等则无需更新,这边的旧值val也就是形参val也属于闭包。新旧值不等时则接下来由dep.notify来通知所有的监听者

watcher.js

import Dep from './dep';

export default class Watcher {
    constructor(vm, expOrFn, cb) {
        this.vm = vm; //vue数据主体
        this.expOrFn = expOrFn; // 监听的字段
        this.cb = cb; // 数据变化后的回调
        this.val = this.get(); // 初始化获取数据
    }

    // 订阅数据更新时调用
    update() {
        let val = this.get();
        this.val = val;
        this.cb.call(this.vm, this.val);
    }

    get() {
        // 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者
        Dep.target = this;
        let val = this.vm._data[this.expOrFn];

        Dep.target = null;
        return val;
    }
}

  • get获取此刻的数据,首先会将自身实例挂在到Dep.target,其目的是访问this.vm._data[this.expOrFn]数据时触发监听器observer.js中,然后再重置Dep.target;
  • update方法,数据更新后会由dep通知触发。从而执行回调this.cb,且把新值this.val传进回调,该方法是我们的最终目的,更新视图view;

vue.js

import Watcher from './watcher';
import { observer } from './observer';

export default class Vue {
    constructor(options = {}) {
        this.$options = options;
        this._data = this.$options.data;
        Object.keys(this._data).forEach(key => this._proxy(key));

        observer(this._data);
    }

    $watch(expOrFn, cb) {
        new Watcher(this, expOrFn, cb);
    }

    _proxy(key) {
        Object.defineProperty(this, key, {
            configurable: true,
            enumerable: true,
            get: () => this._data[key],
            set: (val) => {
                this._data[key] = val;
            }
        })
    }
}

  • 为使用方便把数据的监听同样添加到Vue实例中,通过$watch方法来定义监听的字段以及回调

main.js

import Vue from './vue';

let demo = new Vue({
    data: {
        name: 'cheche'
    }
})

demo.$watch("name", (value) => {
    console.log("【update view111】: ", value);
})

demo.name = "meihao";