vue响应式系统之observe、watcher、dep的源码解析


Posted in Javascript onApril 09, 2019

Vue的响应式系统

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

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

vue响应式系统之observe、watcher、dep的源码解析

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(),循环该数据订阅器中的观察者,进行更新操作。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jQuery中:text选择器用法实例
Jan 03 Javascript
jquery+CSS3实现淘宝移动网页菜单效果
Aug 31 Javascript
javascript跨域的方法汇总
Oct 23 Javascript
javascript原生ajax写法分享
Apr 10 Javascript
JavaScript使用delete删除数组元素用法示例【数组长度不变】
Jan 17 Javascript
Vue框架中正确引入JS库的方法介绍
Jul 30 Javascript
浅谈关于.vue文件中style的scoped属性
Aug 19 Javascript
ES6中Array.includes()函数的用法
Sep 20 Javascript
vuex中使用对象展开运算符的示例
Sep 25 Javascript
详解vuex中mapState,mapGetters,mapMutations,mapActions的作用
Apr 13 Javascript
对vue中v-on绑定自定事件的实例讲解
Sep 06 Javascript
微信小程序input抖动问题的修复方法
Mar 03 Javascript
浅谈发布订阅模式与观察者模式
Apr 09 #Javascript
vue使用keep-alive保持滚动条位置的实现方法
Apr 09 #Javascript
浅谈JavaScript闭包
Apr 09 #Javascript
使用Three.js实现太阳系八大行星的自转公转示例代码
Apr 09 #Javascript
webpack4实现不同的导出类型
Apr 09 #Javascript
Vue中使用create-keyframe-animation与动画钩子完成复杂动画
Apr 09 #Javascript
基于three.js实现的3D粒子动效实例代码
Apr 09 #Javascript
You might like
古巴咖啡 Cubita琥爵咖啡 独特的加勒比海风味咖啡
2021/03/06 新手入门
php GD绘制24小时柱状图
2008/06/28 PHP
PHP可逆加密/解密函数分享
2012/09/25 PHP
php截取字符串函数substr,iconv_substr,mb_substr示例以及优劣分析
2014/06/10 PHP
php基于dom实现读取图书xml格式数据的方法
2017/02/03 PHP
ThinkPHP中create()方法自动验证实例
2017/04/26 PHP
高性能web开发 如何加载JS,JS应该放在什么位置?
2010/05/14 Javascript
Javascript保存网页为图片借助于html2canvas库实现
2014/09/05 Javascript
form.submit()不能提交表单的原因分析
2014/10/23 Javascript
Javascript基础教程之argument 详解
2015/01/18 Javascript
原生js仿jquery一些常用方法(必看篇)
2016/09/20 Javascript
AngularJS自定义插件实现网站用户引导功能示例
2016/11/07 Javascript
微信小程序 用户数据解密详细介绍
2017/01/09 Javascript
iscroll动态加载数据完美解决方法
2017/07/18 Javascript
VUE长按事件需求详解
2017/10/18 Javascript
Vue2.0 axios前后端登陆拦截器(实例讲解)
2017/10/27 Javascript
Vue在页面右上角实现可悬浮/隐藏的系统菜单
2018/05/04 Javascript
jQuery AJAX 方法success()后台传来的4种数据详解
2018/08/08 jQuery
24个解决实际问题的ES6代码片段(小结)
2020/02/02 Javascript
js实现时钟定时器
2020/03/26 Javascript
Python利用Beautiful Soup模块创建对象详解
2017/03/27 Python
详解python上传文件和字符到PHP服务器
2017/11/24 Python
Python3 Random模块代码详解
2017/12/04 Python
Python数据报表之Excel操作模块用法分析
2019/03/11 Python
python 实现的IP 存活扫描脚本
2020/12/10 Python
皇家道尔顿官网:Royal Doulton
2017/12/06 全球购物
全球性的奢侈品梦工厂:Forzieri(福喜利)
2019/02/20 全球购物
英国领先的游戏零售商:GAME
2019/09/24 全球购物
美国在线面料商店:Fashion Fabrics Club
2020/01/31 全球购物
先进德育工作者事迹材料
2014/01/24 职场文书
中介公司区域经理岗位职责范本
2014/03/02 职场文书
党员干部批评与自我批评反四风思想汇报
2014/09/21 职场文书
销售内勤岗位职责范本
2015/04/13 职场文书
2015年城管个人工作总结范文
2015/04/20 职场文书
详解TypeScript中的类型保护
2021/04/29 Javascript
《艾尔登法环》1.03.3补丁上线 碎星伤害调整
2022/04/06 其他游戏