(依赖收集vue) 联想学习

228 阅读4分钟

现在假设我们已经理解了vue响应式的原理,然或者希望这篇文章能有点作用juejin.cn/post/684490…,现在我们创建一个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}`)
})

扩展

终于结束了,来稍微总结下,依赖收集仅仅是用来实现计算属性的吗,下面我们看一个列子,我们使用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是怎么做的呢,原理图如下

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

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

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

联想下,

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

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

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

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

学习文章