vue响应式系统--observe、watcher、dep

更新日期: 2019-04-09阅读: 2.3k标签: 响应式

vue的响应式系统

Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的JavaScript 对象,而当你修改它们时,视图会进行更新,这使得状态管理非常简单直接,我们可以只关注数据本身,而不用手动处理数据到视图的渲染,避免了繁琐的 dom 操作,提高了开发效率。

vue 的响应式系统依赖于三个重要的类:Dep 类、Watcher 类、Observer 类,然后使用发布订阅模式的思想将他们揉合在一起(不了解发布订阅模式的可以看我之前的文章发布订阅模式与观察者模式)。



Observer

Observe扮演的角色是发布者,他的主要作用是调用definereactive函数,在defineReactive函数中使用Object.defineProperty 方法对对象的每一个子属性进行数据劫持/监听。

部分代码展示

defineReactive函数,Observe的核心,劫持数据,在setter中向Dep(调度中心)添加观察者,在getter中通知观察者更新。

function defineReactive(obj, key, val, customSetter, shallow){
    //监听属性key
    //关键点:在闭包中声明一个Dep实例,用于保存watcher实例
    var dep = new Dep();

    var getter = property && property.get;
    var setter = property && property.set;
    
    if(!getter && arguments.length === 2) {
        val = obj[key];
    }
    //执行observe,监听属性key所代表的值val的子属性
    var childOb = observe(val);
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            //获取值
            var value = getter ? getter.call(obj) : val;
            //依赖收集:如果当前有活动的Dep.target(观察者--watcher实例)
            if(Dep.target) {
                //将dep放进当前观察者的deps中,同时,将该观察者放入dep中,等待变更通知
                dep.depend();
                if(childOb) {
                    //为子属性进行依赖收集
                    //其实就是将同一个watcher观察者实例放进了两个dep中
                    //一个是正在本身闭包中的dep,另一个是子属性的dep
                    childOb.dep.depend();
                }
            }
            return value
        },
        set: function reactiveSetter(newVal) {
            //获取value
            var value = getter ? getter.call(obj) : val;
            if(newVal === value || (newVal !== newVal && value !== value)) {
                return
            }
            if(setter) {
                setter.call(obj, newVal);
            } else {
                val = newVal;
            }
            //新的值需要重新进行observe,保证数据响应式
            childOb = observe(newVal);
            //关键点:遍历dep.subs,通知所有的观察者
            dep.notify();
        }
    });
}


Dep

Dep 扮演的角色是调度中心/订阅器,主要的作用就是收集观察者Watcher和通知观察者目标更新。每个属性拥有自己的消息订阅器dep,用于存放所有订阅了该属性的观察者对象,当数据发生改变时,会遍历观察者列表(dep.subs),通知所有的watch,让订阅者执行自己的update逻辑。

部分代码展示

Dep的设计比较简单,就是收集依赖,通知观察者

//Dep构造函数
var Dep = function Dep() {
    this.id = uid++;
    this.subs = [];
};
//向dep的观察者列表subs添加观察者
Dep.prototype.addSub = function addSub(sub) {
    this.subs.push(sub);
};
//从dep的观察者列表subs移除观察者
Dep.prototype.removeSub = function removeSub(sub) {
    remove(this.subs, sub);
};
Dep.prototype.depend = function depend() {
    //依赖收集:如果当前有观察者,将该dep放进当前观察者的deps中
    //同时,将当前观察者放入观察者列表subs中
    if(Dep.target) {
        Dep.target.addDep(this);
    }
};
Dep.prototype.notify = function notify() {
    // 循环处理,运行每个观察者的update接口
    var subs = this.subs.slice();
    for(var i = 0, l = subs.length; i < l; i++) {
        subs[i].update();
    }
};

//Dep.target是观察者,这是全局唯一的,因为在任何时候只有一个观察者被处理。
Dep.target = null;
//待处理的观察者队列
var targetStack = [];

function pushTarget(_target) {
    //如果当前有正在处理的观察者,将他压入待处理队列
    if(Dep.target) {
        targetStack.push(Dep.target);
    }
    //将Dep.target指向需要处理的观察者
    Dep.target = _target;
}

function popTarget() {
    //将Dep.target指向栈顶的观察者,并将他移除队列
    Dep.target = targetStack.pop();
}


Watcher

Watcher扮演的角色是订阅者/观察者,他的主要作用是为观察属性提供回调函数以及收集依赖(如计算属性computed,vue会把该属性所依赖数据的dep添加到自身的deps中),当被观察的值发生变化时,会接收到来自dep的通知,从而触发回调函数。,

部分代码展示

Watcher类的实现比较复杂,因为他的实例分为渲染 watcher(render-watcher)、计算属性 watcher(computed-watcher)、侦听器 watcher(normal-watcher)三种, 
这三个实例分别是在三个函数中构建的:mountComponent 、initComputed和Vue.prototype.$watch。

normal-watcher:我们在组件钩子函数watch 中定义的,都属于这种类型,即只要监听的属性改变了,都会触发定义好的回调函数,这类watch的expression是我们写的回调函数的字符串形式。

computed-watcher:我们在组件钩子函数computed中定义的,都属于这种类型,每一个 computed 属性,最后都会生成一个对应的 watcher 对象,但是这类 watcher 有个特点:当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。这类watch的expression是计算属性中的属性名。

render-watcher:每一个组件都会有一个 render-watcher, 当 data/computed 中的属性改变的时候,会调用该 render-watcher 来更新组件的视图。这类watch的expression是 function () {vm._update(vm._render(), hydrating);}。

除了功能上的区别,这三种 watcher 也有固定的执行顺序,分别是:computed-render -> normal-watcher -> render-watcher。

这样安排是有原因的,这样就能尽可能的保证,在更新组件视图的时候,computed 属性已经是最新值了,如果 render-watcher 排在 computed-render 前面,就会导致页面更新的时候 computed 值为旧数据。

这里我们只看其中一部分代码

function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm;
    if(isRenderWatcher) {
        vm._watcher = this;
    }
    vm._watchers.push(this);
    // options
    if(options) {
        this.deep = !!options.deep; //是否启用深度监听
        this.user = !!options.user; //主要用于错误处理,侦听器 watcher的 user为true,其他基本为false
        this.lazy = !!options.lazy; //惰性求职,当属于计算属性watcher时为true
        this.sync = !!options.sync; //标记为同步计算,三大类型暂无
    } else {
        this.deep = this.user = this.lazy = this.sync = false;
    }
    //初始化各种属性和option
    
    //观察者的回调
    //除了侦听器 watcher外,其他大多为空函数
    this.cb = cb;
    this.id = ++uid$1; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    // 解析expOrFn,赋值给this.getter
    // 当是渲染watcher时,expOrFn是updateComponent,即重新渲染执行render(_update)
    // 当是计算watcher时,expOrFn是计算属性的计算方法
    // 当是侦听器watcher时,expOrFn是watch属性的名字,this.cb就是watch的handler属性
    
    //对于渲染watcher和计算watcher来说,expOrFn的值是一个函数,可以直接设置getter
    //对于侦听器watcher来说,expOrFn是watch属性的名字,会使用parsePath函数解析路径,获取组件上该属性的值(运行getter)
    
    //依赖(订阅目标)更新,执行update,会进行取值操作,运行watcher.getter,也就是expOrFn函数
    if(typeof expOrFn === 'function') {
        this.getter = expOrFn;
    } else {
        this.getter = parsePath(expOrFn);
    }
    this.value = this.lazy ? undefined : this.get();
};    
//取值操作
Watcher.prototype.get = function get() {
    //Dep.target设置为该观察者
    pushTarget(this);
    var vm = this.vm;
    //取值
    var value = this.getter.call(vm, vm);
    //移除该观察者
    popTarget();
    return value
};
Watcher.prototype.addDep = function addDep(dep) {
    var id = dep.id;
    if(!this.newDepIds.has(id)) {
        //为观察者的deps添加依赖dep
        this.newDepIds.add(id);
        this.newDeps.push(dep);
        if(!this.depIds.has(id)) {
            //为dep添加该观察者
            dep.addSub(this);
        }
    }
};
//当一个依赖改变的时候,通知它update
Watcher.prototype.update = function update() {
    //三种watcher,只有计算属性 watcher的lazy设置了true,表示启用惰性求值
    if(this.lazy) {
        this.dirty = true;
    } else if(this.sync) {
        //标记为同步计算的直接运行run,三大类型暂无,所以基本会走下面的queueWatcher
        this.run();
    } else {
        //将watcher推入观察者队列中,下一个tick时调用。
        //也就是数据变化不是立即就去更新的,而是异步批量去更新的
        queueWatcher(this);
    }
};

//update执行后,运行回调cb
Watcher.prototype.run = function run() {
    if(this.active) {
        var value = this.get();
        if(
            value !== this.value ||
            isObject(value) ||
            this.deep
        ) {
            var oldValue = this.value;
            this.value = value;
            //运行 cb 函数,这个函数就是之前传入的watch中的handler回调函数
            if(this.user) {
                try {
                    this.cb.call(this.vm, value, oldValue);
                } catch(e) {
                    handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
                }
            } else {
                this.cb.call(this.vm, value, oldValue);
            }
        }
    }
};

//对于计算属性,当取值计算属性时,发现计算属性的watcher的dirty是true
//说明数据不是最新的了,需要重新计算,这里就是重新计算计算属性的值。
Watcher.prototype.evaluate = function evaluate() {
    this.value = this.get();
    this.dirty = false;
};

//收集依赖
Watcher.prototype.depend = function depend() {
    var this$1 = this;

    var i = this.deps.length;
    while(i--) {
        this$1.deps[i].depend();
    }
};


总结

Observe是对数据进行监听,Dep是一个订阅器,每一个被监听的数据都有一个Dep实例,Dep实例里面存放了N多个订阅者(观察者)对象watcher。

被监听的数据进行取值操作时(getter),如果存在Dep.target(某一个观察者),则说明这个观察者是依赖该数据的(如计算属性中,计算某一属性会用到其他已经被监听的数据,就说该属性依赖于其他属性,会对其他属性进行取值),就会把这个观察者添加到该数据的订阅器subs里面,留待后面数据变更时通知(会先通过观察者id判断订阅器中是否已经存在该观察者),同时该观察者也会把该数据的订阅器dep添加到自身deps中,方便其他地方使用。

被监听的数据进行赋值操作时(setter)时,就会触发dep.notify(),循环该数据订阅器中的观察者,进行更新操作。


来自:https://segmentfault.com/a/1190000018795293


链接: https://fly63.com/article/detial/2781

移动端web app要使用rem实现自适应布局:font-size的响应式

rem是相对于根元素html,这样就意味着,我们只需要在根元素确定一个px字号,则可以来算出元素的宽高。

使用现代CSS的响应式版面

通过模块化缩放,使用传统属性和calc()来动态缩放你的字体大小.为字体大小使用百分比.给文本内容和媒体查询使用em,针对不同视口尺寸使用不同缩放值.视口越小,缩放比例越小,使用媒体查询或者media()函数基于视口来改变比例和基础字号

web响应式图片的5种实现

在目前的前端开发中,我们经常需要进行响应式的网站开发。本文着重介绍一下弹性图片,也就是响应式图片的解决方案:js或服务端、srcset 、sizes 、picture标签、svg图片

HTML5+CSS3响应式垂直时间轴,高端,大气

HTML5+CSS3响应式垂直时间轴,使用了HTML5标签<section>,时间轴中所有的内容包括标题、简介、时间和图像都放在.cd-timeline-block的DIV中,多个DIV形成一个序列,并把这些DIV放在<section>中。

实现响应式_CSS变量

CSS 变量是 CSS 引入的一个新特性,目前绝大多数浏览器已经支持了,它可以帮助我们用更少的代码写出同样多的样式,大大提高了工作效率,本篇文章将教你如何使用 CSS 变量(css variable)。CSS中原生的变量定义语法是:--*,变量使用语法是:var(--*),其中*表示变量名称

vue响应式原理及依赖收集

Vue通过设定对象属性的setter/getter方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。

Responsive Web Design 响应式网页设计

常见的布局方案:固定布局:以像素作为页面的基本单位,不管设备屏幕及浏览器宽度,只设计一套尺寸;可切换的固定布局:同样以像素作为页面单位,参考主流设备尺寸

响应式布局的实现

响应式布局,即 Responsive design,在实现不同屏幕分辨率的终端上浏览网页的不同展示方式。通过响应式设计能使网站在手机和平板电脑上有更好的浏览阅读体验。响应式布局的关键不仅仅在于布局

深入响应式原理

说到响应式原理其实就是双向绑定的实现,说到 双向绑定 其实有两个操作,数据变化修改dom,input等文本框修改值的时候修改数据1. 数据变化 -> 修改dom;2. 通过表单修改value -> 修改数据

媒体类型和响应式设计

常用的三种为:all,print和screen;媒体类型引用方法:link标签,xml方式,@import和css3新增的@media四种

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!