watch监测、计算属性实现原理
xiaofuzi opened this issue · 0 comments
(注:这里是笔者自己尝试的一种实现方式,也许与vue的实现方式会有所区别)
这里对数据的响应式定义进行了调整,之前的tinyVue采用的是遍历 DOM 节点,获取指令对应的key然后将获得的key所对应的对象进行了getter/setter操作的处理,这种实现方式对watch功能和计算属性的实现不便,所以更改为直接将 option.data 对象转换为响应式对象,这样就不会受限于模板中声明的指令对应key的限制,从而未绑定指令的key也可以转换为可监测的。
tinyVue中是通过给每个key建立对应的binding对象来实现响应式的(setter/getter监测由binding对象内部实现)。tinyVue则维护数据模型得到的所有的binding,存储在_bindings中(关于binding的详细说明可参考这里)。
什么是watch监测?
简单的说就是监测到某一属性的变化后执行相应的回调。
例如:
var vm = new TinyVue({
data: {
name: 'xiaofu',
info: {
height: 170
}
},
watch: {
name: function (newValue, oldValue) {
console.log(newValue);
},
info: function (info, oldInfo) {
console.log(info);
},
'info.height': function (height, oldHeight) {
console.log(height);
}
},
ready () {
this.name = 'xiaoyang';
this.info.height = 180;
}
});
上述例子中,分别定义了name
,info
,info.height
的watch函数,watch回调函数包含两个参数,分别为新值和旧值。
- 值变化时触发watch回调
- 子属性值变化时触发回调
- 父属性变化会触发子属性值的回调(具体可看深层次响应式的实现,父属性赋值会触发为子属性的辅助)
- 可直接监测子属性的值
- 新值与旧值相等则不会触发回调
因为子属性的变化也需要触发父属性的回调,所以在这里采用冒泡的实现方式,即当一个属性监测到它发生了变化时,它会通知它的父级发生了变化,父级再往上传,从而实现了当监测info
的时候,如果info.height
发生了变化,那么info
也会知道已发生了变化。
计算属性的实现
这里完全禁止了计算属性的赋值操作,因为给计算属性赋值其实是没必要的,用了反而会影响逻辑,因为赋值操作比较分散,增加理解的难度。
- 其值由其它值计算而得到
- 其值会动态的监测依赖值的变化并及时更新
- 当其值变化后,会更新与其相关的指令
- 计算属性可以依赖于计算属性
这里比较难以实现的是依赖的收集,这里先将计算属性对应的求值函数定义为该计算属性的getter函数,如下所示:
defineComputedProperty () {
let key = this.key,
obj = this.vm,
self = this;
def(obj, key, {
get () {
let getter = self.vm._opts.computed[key];
if (isFunc(getter)) {
self.value = getter.call(self.vm);
return self.value;
}
},
set () {
//console.warn('computed property is readonly.');
}
});
}
当我们对计算属性取值时,会调用对应的求值函数来得到计算属性的值,依赖的收集也可以在这里进行,当求值函数执行的时候,求值的过程中会发生依赖项的getter操作,通过监测发生的getter操作即可得到依赖项。
如下所示:
def(obj, key, {
get () {
observer.isObserving && observer.emit('get', self);
return self.value;
},
set (value) {
if (value !== self.value) {
self.oldValue = self.value;
if (!isObj) {
self.value = value;
self.update(value);
} else {
for (let prop in value) {
self.value[prop] = value[prop];
}
}
observer.emit(self.key, self);
self.refresh();
}
}
});
observer.isObserving && observer.emit('get', self);
首先通过 observer.isObserving来标识一次计算属性的取值过程,然后监测 emit 的 get 事件,事件传回来的self即该计算属性的依赖项,这样就得到了计算属性的依赖项,收集完毕后我们只要监测在setter中触发的emit事件即可实现计算属性的更新(与watch的实现是有所区别的)。
在指令更新上计算属性也需要单独处理,当计算属性更新后,需要通知其所有子属性绑定的指令执行更新操作。
如下所示:
_bind (el, directive) {
el.removeAttribute(prefix + '-' + directive.name);
directive.el = el;
let key = directive.key,
binding = this._bindings[key];
if (!binding) {
/**
* computed property binding hack
* 针对计算属性子属性
*/
//get computed property key
let computedKey = key.split('.')[0];
binding = this._bindings[computedKey];
if (binding.isComputed) {
binding.directives.push(directive);
} else {
console.error(key + ' is not defined.');
}
}
binding.directives.push(directive);
}
在_bing函数中收集计算属性相关的所有指令并存储下来,然后在更新的时候动态的更新指令。
待更新。。。