三、数据响应式原理

知识点铺垫

var obj = {};
//getter和setter需要变量中转才能使用,所以不合适
var temp;
Object.defineProperty(obj, 'a', {
    //getter
    get() {
        return temp;
    },
    //setter
    set(val) {
        temp = val;
    }
    // value: 3,//value和get不能同时存在
    // writable:true,
    // enumerable:true
});
Object.defineProperty(obj, 'b', {
    value: 5,
    //是否可被枚举
    // enumerable:false
});
// obj.a++;
// for (var item in obj) {
//     console.log(item);//只输出a
// }
obj.a = 10;
console.log(obj);
  • 闭包形式演化

避免单独的temp变量

var obj = {};
//通过闭包,避免单独变量temp
function defindReactive(data, key, val) {
    Object.defineProperty(data, key, {
        enumerable:true,
        configurable:true,//可配置,比如能被外部delete
        get() {
            return val;
        },
        set(newval) {
            if (newval === val) {
                return;
            }
            val = newval;
        }
    });
}
defindReactive(obj,'a',10);
obj.a++;
console.log(obj);

殊理

  • 实际上整体殊理逻辑要分门别类:例如,arr和object分开讨论;
    同时整体看起来好像是单向,但看对象的话实际上observe->Observer-> defineReactive,如果有set操作则又进入observe

  • 其中如果defineReactive如果知道对象有子对象,会继续从observe开始循环;即每次都是处理一层而已

  • 至于Dep(依赖收集)和Watcher是收集依赖和触发更新的,殊理逻辑可以最后再看

1.png

什么是依赖

  • 需要用的数据的地方,称为依赖
  • Vue2.x,中等依赖,用到数据的组件是依赖
  • 在getter中收集依赖,在setter中触发依赖

Dep类和Watcher类

  • 依赖就是Watcher,只有在Watcher触发getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中
  • Dep使用发布订阅模式,当数据发生变化时候,会循环依赖列表,把所有Watcher都通知一遍
  • 代码实现的巧妙之处:Watcher把自己设置到全局的一个指定位置,然后读取数据,因为读取了数据,所以会触发这个数据的getter。在getter中就能得到当前正在读取数据的Watcher,并把这个Watcher收集到Dep中。
4.png

代码

  • index.js
import { observe } from './observe.js'
import Watcher from './Watcher.js'
var obj = {
    a: {
        m: {
            n: 5
        }
    },
    b: 10,
    g:[2,3,4]
};

observe(obj);
// obj.b++;
obj.g.push(5);
console.log(obj);
new Watcher(obj,'a.m.n',val=>{
    console.log(1111,val);
})
  • observe.js
import Observer from './Observer.js'
//创建observer函数,起辅助判别的作用:看obj身上有没有__ob__
export const observe = function (value) {
    //如果value不是对象,什么也不做;因为直接数值时候没有必要
    if (typeof value !== 'object') {
        return
    }
    var ob;
    if (typeof value.__ob__ !== 'undefined') {
        ob = value.__ob__;//不希望和常见属性重名
    } else {
        ob = new Observer(value);
    }
    return ob;
}
  • Observer.js
import { def } from './utils.js';
import defineReactive from './defineReactive.js'
import { arrayMethods } from './arr.js'
import { observe } from './observe.js'
import Dep from './Dep.js'

//Observer  将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
export default class Observer {
    constructor(value) {
        //每个Observer的实例,成员中都有一个Dep的实例
        this.dep = new Dep();
        def(value, '__ob__', this, false);
        //检查它是数组还是对象
        if (Array.isArray(value)) {
            //如果是数组,将这个数组的原型,指向arrayMethods
            Object.setPrototypeOf(value, arrayMethods);
            //让这个数组变得observe
            this.observeArray(value);
        } else {
            this.walk(value);
        }
    }
    //遍历
    walk(value) {
        for (let key in value) {
            defineReactive(value, key);
        }
    }
    //数组特殊遍历
    observeArray(arr) {
        for (let i = 0; i < arr.length; i++) {
            observe(arr[i]);
        }
    }
};
  • defineReactive.js
import { observe } from './observe.js'
import Dep from './Dep.js'
export default function defindReactive(data, key, val) {
    //此处dep是闭包中的
    const dep = new Dep();
    if (arguments.length == 2) {
        val = data[key];
    }
    //子元素要进行observe,至此形成了递归;这个递归不是函数调用自己,是多个函数形成循环调用
    let childOb = observe(val);
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get() {
            //如果现在处于依赖收集阶段
            if (Dep.target) {
                dep.depend();
                if (childOb) {
                    childOb.dep.depend();
                }
            }
            return val;
        },
        set(newval) {
            if (newval === val) {
                return;
            }
            val = newval;
            //当设置了新值,新值也要被observe,不然不是响应式的
            childOb = observe(newval);
            //发布订阅模式,通知dep
            dep.notify();
        }
    });
}
  • arr.js
import { def } from './utils.js';
const arrayPrototype = Array.prototype;
//以Array.prototype为原型,创建arrayMethods对象
// 针对数组形成响应式,需要改写数组的七种方法
// push
// pop
// shift
// unshift
// splice
// sort
// reverse
export const arrayMethods = Object.create(arrayPrototype);

// Object.setPrototypeOf(o,arrayMethods);
// o.__proto__=arrayMethods;

const methodsNeedChange = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'];
methodsNeedChange.forEach(methodName => {
    //备份原来的方法
    const original = arrayPrototype[methodName];
    //定义新的方法
    def(arrayMethods, methodName, function () {
        //把类数组对象变为数组,不然下面的slice无法调用
        const args=[...arguments];
        //把这个数组身上的__ob__取出来,__ob__已经被添加了,为什么已经被添加了?因为数组肯定不是最高层
        //比如obj.g属性是数组,obj不能是数组,第一次遍历obj这个对象的第一层的时候,已经给g属性,添加了__ob__属性
        const ob = this.__ob__;

        // 'push',
        // 'unshift',
        // 'splice',
        //这三者涉及插入新项,所以也需要变成observe
        let inserted = [];
        switch (methodName) {
            case 'push':
            case 'unshift':
                inserted = args;
                break;
            case 'splice':
                inserted = args.slice(2);
                break;
        }
        //判断有没有需要插入新项,让新项变成响应的
        if (inserted) {
            ob.observeArray(inserted);
        }
        //此处this是被数组打点调用的,而且不能用箭头函数
        original.apply(this, arguments);
        ob.dep.notify();
    }, false);
})
  • Dep.js
var uid = 0;
export default class Dep {
    constructor() {
        this.id = uid++;
        //用数组存储自己的订阅者.放的是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; i < subs.length; i++) {
            subs[i].update();
        }
    }
}
  • Watcher.js
import Dep from "./Dep";

var uid = 0;
export default class Watcher {
    constructor(target, expression, callback) {
        this.id = uid++;
        this.target = target;
        this.gettr = parsePath(expression);//'a.b.c.d'
        this.callback = callback;
        this.value = this.get();
    }
    update() {
        this.getAndInvoke(this.callback);
    }
    get() {
        //进入依赖收集阶段,让全局的Dep,target设置为watcher本身,那么就是进入依赖收集阶段
        Dep.target = this;
        const obj = this.target;
        var value;
        try {
            value = this.gettr(obj);
        } finally {
            //收集完毕,退出,其他watcher需要
            Dep.target = null;
        }
        return value;
    }
    //获取数据并更新
    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) {
    //作用是:把'a.b.c.d'这种转换成a{b:{c:{d:{}}}}
    var segments = str.split('.');
    return (obj) => {
        for (let i = 0; i < segments.length; i++) {
            if (!obj) {
                return;
            }
            obj = obj[segments[i]];
        }
        return obj;
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容