(依赖收集vue) 联想学习

现在假设我们已经理解了vue响应式的原理,然或者希望这篇文章能有点作用
https://juejin.im/post/5cc14c87f265da035c6bc9bb
,现在我们创建一个girl,她的所有属性读写已经变得可观察了:

function defineReactive(obj, key, val, cb){
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: ()=>{
            console.log(`${key}属性被获取了`);
            return val
        },
        set:newVal=> {
            console.log(`${key}属性被修改了`);
            val = newVal;
            // cb();
        }
    })
}
let girl = { height: '1.7', weight: '48', whole: '90分'}
function observe(value) {
    Object.keys(value).forEach((key) => defineReactive(value, key, value[key]));
}
observe(girl)
girl.height
height属性被获取了
"1.7"
girl.weight
weight属性被获取了
"48"
girl.height = '1.9'
height属性被修改了
"1.9"

但是这就可以了吗,我们如果想知道这个女孩是否漂亮怎么办呢,假设啊,girl身高大于1.65且体重小于48kg就是漂亮啊,原谅我这么肤浅,那我们改该怎么做呢?

计算属性

假设有这么一个方法叫watcher(观察者),我们在girl对象上面创建了一个属性isBeautiful,现在我们定义了她的值取决于该对象的另外两个属性,当满足条件时就是true了,现在我们来实现一下这个方法:

// 检测监听属性的值更新时调用
function updateNotice(val) {
    console.log(`girl的isBeautiful属性为${val}`)
}
// obj 监听对象
// key 监听的属性
// cb 监听的回调函数
function watcher(obj, key, cb) {
    Object.defineProperty(obj, key, {
        get: function() {
            var val = cb();
            <!--通知监听属性的值-->
            updateNotice(val);
            return val;
        },
        set: function() {
            console.log('watcher属性不可以被修改');
        }
    })
}
watcher(girl, 'isBeautiful', () => {
  return (girl.height > 1.65 && girl.weight < 48) ? '漂亮' : '差点漂亮'
})
girl.isBeautiful
height属性被获取了
girl的isBeautiful属性为差点漂亮
"差点漂亮"
girl.height = '1.7'
height属性被修改了
"1.7"
girl.weight = '47'
weight属性被修改了
"47"
girl.isBeautiful
height属性被获取了
weight属性被获取了
girl的isBeautiful属性为漂亮
"漂亮"

perfect现在,要是只要我们获取下girl的isBeautiful属性就能知道她的类型了,就是有些麻烦,我们还是想观察者要是能检测到对应的依赖属性发生变化时,能主动通知我们她是否漂亮的属性的值就更完美了

依赖收集

现在我们怎么实现呢,不妨想想,上面说了(对应的依赖属性发生变化时)对应的就是每个可观察对象的set方法执行的时候,如果我们在set方法里面能够调用监听属性变化的方法updateNotice,不就可以实现主动通知了吗,道理是这个道理, 但是updateNotice方法需要接受回调值作为参数,而set方法的上下作用域是没有的,我们需要一个工具将监听器可观测对象连接起来,这个工具的作用就是收集监听器里面的回调参数cb及监听属性更新的方法updateNotice用来在可观测对象里面来使用。
下面我们给这个工具起个好听的名字依赖收集器

let Dep = {
  target: null // 用来接收监听器里面的回调参数和更新通知方法
}

target等于我们监听器里面的回调参数cb及监听属性更新的方法updateNotice,下面我们来优化下对应的updateNotice方法:

// obj 监听对象
// key 监听的属性
// cb 监听的回调函数
function watcher(obj, key, cb) {    
    // 所依赖的属性发生变化时调用
    function onDepUpdated () {
        var val = cb();
        updateNotice(val)
    }
    Object.defineProperty(obj, key, {
        get: function() {
            Dep.target = onDepUpdated;
            var val = cb();
            Dep.target = null;
            return val;
        },
        set: function() {
            console.log('watcher属性不可以被修改');
        }
    })
}

重点来了,大家可能会好奇Dep.target先是等于onDepUpdated后又为null的作用,解释下,当我们首次获取监听对象的监听属性时,举个列子girl.isBeautiful这是会执行对应的get方法,这个时候我们先是将对应的Dep.target = onDepUpdated(收集监听器里面的回调参数cb及监听属性更新的方法updateNotice),这一步非常关键,通过这样的操作,依赖收集器就获得了监听器的回调值以及updateNotice()方法,然后执行对应的cb方法,监听属性所依赖的属性这个时候被获取,将会触发对应的defineReactive的get方法,这个时候我们对应的依赖收集下,cb执行之后再将Dep.target = null,是不是豁然开朗,Dep.target作为全局变量,理所当然的能够被可观测对象的get/set所使用。下面我们将代码补充下:

function defineReactive(obj, key, val, cb){
    let deps = [];
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: ()=>{
            if (Dep.target && deps.indexOf(Dep.target) === -1) {
                deps.push(Dep.target)
            }
            console.log(`${key}属性被获取了`);
            return val
        },
        set:newVal=> {
            console.log(`${key}属性被修改了`);
            val = newVal;
            deps.forEach((dep) => {
                dep();
            })
        }
    })
}

正如上面写的,当被依赖的属性获取时,对应的deps便会收集依赖属性(计算属性)的onDepUpdated方法,当被依赖属性发生变化的时候,便会主动通知所有依赖者(计算属性),也就是执行对应的onDepUpdated方法。
下面贴上完整代码:

function defineReactive(obj, key, val, cb){
    let deps = [];
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: ()=>{
            if (Dep.target && deps.indexOf(Dep.target) === -1) {
                deps.push(Dep.target)
            }
            console.log(`${key}属性被获取了`);
            return val
        },
        set:newVal=> {
            console.log(`${key}属性被修改了`);
            val = newVal;
            deps.forEach((dep) => {
                dep();
            })
        }
    })
}
function observe(value) {
    Object.keys(value).forEach((key) => defineReactive(value, key, value[key]));
}
let Dep = {
  target: null // 用来接收监听器里面的回调参数和更新通知方法
}
// 检测监听属性的值更新时调用
function updateNotice(val) {
    console.log(`girl的isBeautiful属性为${val}`)
}
// obj 监听对象
// key 监听的属性
// cb 监听的回调函数
function watcher(obj, key, cb) {    
    // 所依赖的属性发生变化时调用
    function onDepUpdated () {
        var val = cb();
        updateNotice(val)
    }
    Object.defineProperty(obj, key, {
        get: function() {
            Dep.target = onDepUpdated;
            var val = cb();
            Dep.target = null;
            return val;
        },
        set: function() {
            console.log('watcher属性不可以被修改');
        }
    })
}

let girl = { height: '1.7', weight: '48', whole: '90分'}
observe(girl)
watcher(girl, 'isBeautiful', () => {
  return (girl.height > 1.65 && girl.weight < 48) ? '漂亮' : '差点漂亮'
})

girl.isBeautiful
height属性被获取了
weight属性被获取了
"差点漂亮"
girl.weight = '46'
weight属性被修改了
height属性被获取了
weight属性被获取了
girl的isBeautiful属性为漂亮
"46"

到此,我们已经实现了我们初始时候的设想,是不是对依赖收集也有了简单的了解了,如果能对你有一点点的帮助,再好不过了,
下面我们贴上优化后的代码,将依赖收集有关的功能、观察者、订阅者都功能集成下:

<!--订阅者-->
class Observer {
    constructor(data) {
        return this.walk(data);
    }
    walk(data) {
        const key = Object.keys(data);
        key.forEach( key => {
            this.defineProperty(data, key, data[key]);
        })
        return data;
    }
    defineProperty(obj, key, val) {
        const dep = new Dep();
        Object.defineProperty(obj, key, {
            get() {
                console.log(`${key}属性被获取了`);
                dep.addSub();
                return val
            },
            set(newVal) {
                console.log(`${key}属性被修改了`);
                val = newVal;
                dep.notify();
            }
        }) 
    }
}
<!--观察者-->
class Watcher {
    constructor(obj, key, cb, onComputedUpData) {
        this.obj = obj;
        this.key = key;
        this.cb = cb;
        this.onComputedUpData = onComputedUpData;
        return this.defineComputed();
    }
    defineComputed() {
        let _this = this;
        let DepUpdated = function() {
            let val = _this.cb();
            _this.onComputedUpData(val);
        }
        Object.defineProperty(_this.obj, _this.key, {
            get() {
                Dep.target = DepUpdated;
                let val = _this.cb();
                Dep.target = null;
                return val;
            },
            set() {
                console.log('wacther不能被修改');
            }
        })
    }
}
<!--依赖收集器-->
class Dep {
    constructor(){
        this.deps = [];
    }
    addSub() {
        if (Dep.target && this.deps.indexOf(Dep.target) == -1) {
            this.deps.push(Dep.target);
        }
    }
    notify() {
        this.deps.forEach(dep => {
            dep();
        })
    }
}
Dep.target = null;

<!--验证-->
let girl = new Observer({ height: '1.7', weight: '48', whole: '90分'})
watcher(girl, 'isBeautiful', () => {
  return (girl.height > 1.65 && girl.weight < 48) ? '漂亮' : '差点漂亮'
}, (val)=> {
    console.log(`girl的isBeautiful属性为${val}`)
})
image

扩展

终于结束了,来稍微总结下,依赖收集仅仅是用来实现计算属性的吗,下面我们看一个列子,我们使用vue的时候:

new Vue({
    template: 
        `<div>
            <span>text1:</span> {{text1}}
            <span>text2:</span> {{text2}}
        <div>`,
    data: {
        text1: 'text1',
        text2: 'text2',
        text3: 'text3'
    }
});

由于响应式的原因,大家都知道当我们修改对应的text1或者text2的时候,数据驱动对应的dom便会发生更新,但是当我们修改对应的text3呢,对应的set是不是也会执行对应的渲染呢,其实不然,那么vue是怎么做的呢,原理图如下

image
  • 每个组件实例都有相应的watcher实例

  • 渲染组件的过程,会把属性记录为依赖

  • 当我们操纵一个数据时,依赖项的setter会被调用,从而通知watcher重新计算,从而致使与之相关联的组件得以更新

联想下,

  1. 既然模板渲染需要用到某个数据,那么一定会对这个数据进行访问,所以只要拦截getter,进行依赖收集,
  2. 当依赖的数据被设置时,setter能获得这个通知,如果有依赖的话进行对应更新render
    如此的话,上面说的问题是不是就解决了,vue在渲染视图的时候便会将有用到的data进行依赖收集,也就是只有被Dep.target标记过的才会进行收集。

在vue里面,对应的观察者和观察目标又有哪些呢?

  • 依赖的数据是观察目标
  • 视图、计算属性、侦听器这些是观察者

差不多了,希望这篇文章对大家有帮助,如果发现有任何错漏的地方,也欢迎向我指出,谢谢大家~(鄙人才疏学浅,如有理解不到或是错误的地方还望各位看客见谅)

学习文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350