xiaofuzi / deep-in-vue

从源码的角度看vue的成长历程。

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

watch监测、计算属性实现原理

xiaofuzi opened this issue · 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函数中收集计算属性相关的所有指令并存储下来,然后在更新的时候动态的更新指令。

待更新。。。