vue 快速入门 系列 —— 侦测数据的变化 - [vue api 原理]
其他章节请看:
侦测数据的变化 - [vue api 原理]
前面(侦测数据的变化 - [基本实现])我们已经介绍了新增属性无法被侦测到,以及通过 delete 删除数据也不会通知外界,因此 vue 提供了 vm.$set() 和 vm.$delete() 来解决这个问题。
vm.$watch() 方法赋予我们监听实例上数据变化的能力。
下面依次对这三个方法的使用以及原理进行介绍。
Tip: 以下代码出自 vue.esm.js,版本为 v2.5.20。无关代码有一些删减。中文注释都是笔者添加。
vm.$set
这是全局 Vue.set 的别名。向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。
语法:
- vm.$set( target, propertyName/index, value )
参数:
- {Object | Array} target
- {string | number} propertyName/index
- {any} value
以下是相关源码:
Vue.prototype.$set = set;
/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
function set (target, key, val) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(("Cannot set reactive property on undefined, null, or primitive value: " +
    ((target))));
  }
  // 如果 target 是数组,并且 key 是一个有效的数组索引
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 如果传递的索引比数组长度的值大,则将其设置为 length
    target.length = Math.max(target.length, key);
    // 触发拦截器的行为,会自动将新增的 val 转为响应式
    target.splice(key, 1, val);
    return val
  }
  // 如果 key 已经存在,说明这个 key 已经被侦测了,直接修改即可
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val
  }
  // 取得数据的 Observer 实例
  var ob = (target).__ob__;
  // 处理文档中说的 ”注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象“
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    );
    return val
  }
  // 如果数据没有 __ob__,说明不是响应式的,也就不需要做任何特殊处理
  if (!ob) {
    target[key] = val;
    return val
  }
  // 通过 defineReactive$$1() 方法在响应式数据上新增一个属性,该方法会将新增属性
  // 转成 getter/setter
  defineReactive$$1(ob.value, key, val);
  ob.dep.notify();
  return val
}
/**
 * Check if val is a valid array index.
 * 检查 val 是否是一个有效的数组索引
 */
function isValidArrayIndex (val) {
  var n = parseFloat(String(val));
  return n >= 0 && Math.floor(n) === n && isFinite(val)
}
vm.$delete
这是全局 Vue.delete 的别名。删除对象的 property。如果对象是响应式的,确保删除能触发更新视图。你应该很少会使用它。
语法:
- Vue.delete( target, propertyName/index )
参数:
- {Object | Array} target
- {string | number} propertyName/index
实现思路与 vm.$set 类似。请看:
Vue.prototype.$delete = del;
/**
 * Delete a property and trigger change if necessary.
 * 删除属性,并在必要时触发更改。
 */
function del (target, key) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(("Cannot delete reactive property on undefined, null, or primitive value: " +
    ((target))));
  }
  // 如果 target 是数组,并且 key 是一个有效的数组索引
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 触发拦截器的行为
    target.splice(key, 1);
    return
  }
  // 取得数据的 Observer 实例
  var ob = (target).__ob__;
  // 处理文档中说的 ”注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象“
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    );
    return
  }
  // key 不是 target 自身属性,直接返回
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key];
  // 不是响应式数据,终止程序
  if (!ob) {
    return
  }
  // 通知依赖
  ob.dep.notify();
}
vm.$watch
观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受简单的键路径。对于更复杂的表达式,用一个函数取代。
语法:
- vm.$watch( expOrFn, callback, [options] )
参数:
- {string | Function} expOrFn
- {Function | Object} callback
- {Object} [options]
- {boolean} deep
- {boolean} immediate
 
返回值:
- {Function} unwatch
例如:
// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
  // 做点什么
})
// 函数
vm.$watch(
  function () {
    return this.a + this.b
  },
  function (newVal, oldVal) {
    // 做点什么
  }
)
相关源码请看:
Vue.prototype.$watch = function (
    expOrFn,
    cb,
    options
  ) {
    var vm = this;
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {};
    options.user = true;
    // 通过 Watcher() 来实现 vm.$watch 的基本功能
    var watcher = new Watcher(vm, expOrFn, cb, options);
    // 在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value);
      } catch (error) {
        handleError(error, vm, ("callback for immediate watcher \"" +
        (watcher.expression) + "\""));
      }
    }
    // 返回一个函数,作用是取消观察
    return function unwatchFn () {
      watcher.teardown();
    }
  };
/**
 * Remove self from all dependencies' subscriber list.
 * 取消观察。也就是从所有依赖(Dep)中把自己删除
 */
Watcher.prototype.teardown = function teardown () {
  if (this.active) {
    // remove self from vm's watcher list
    // this is a somewhat expensive operation so we skip it
    // if the vm is being destroyed.
    if (!this.vm._isBeingDestroyed) {
      remove(this.vm._watchers, this);
    }
    // this.deps 中记录这收集了自己(Wtacher)的依赖
    var i = this.deps.length;
    while (i--) {
      // 依赖中删除自己
      this.deps[i].removeSub(this);
    }
    this.active = false;
  }
};
/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
var Watcher = function Watcher (
  vm,
  expOrFn,
  cb,
  options,
  isRenderWatcher
) {
  this.vm = vm;
  if (isRenderWatcher) {
    vm._watcher = this;
  }
  vm._watchers.push(this);
  // options
  if (options) {
    // deep 监听对象内部值的变化
    this.deep = !!options.deep;
    this.user = !!options.user;
    this.lazy = !!options.lazy;
    this.sync = !!options.sync;
    this.before = options.before;
  } else {
    this.deep = this.user = this.lazy = this.sync = false;
  }
  this.cb = cb;
  this.id = ++uid$1; // uid for batching
  this.active = true;
  this.dirty = this.lazy; // for lazy watchers
  // 存储依赖(Dep)。Watcher 可以通过 deps 得知自己被哪些 Dep 收集了。
  // 可用于取消观察
  this.deps = [];
  this.newDeps = [];
  this.depIds = new _Set();
  this.newDepIds = new _Set();
  this.expression = process.env.NODE_ENV !== 'production'
    ? expOrFn.toString()
    : '';
  // parse expression for getter
  // expOrFn可以是简单的键路径或函数。本质上都是读取数据的时候收集依赖,
  // 所以函数可以同时监听多个数据的变化
  // 函数: vm.$watch(() => {return this.a + this.b},...)
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  // 键路径: vm.$watch('a.b.c',...)
  } else {
    // 返回一个读取键路径(a.b.c)的函数
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = noop;
      process.env.NODE_ENV !== 'production' && warn(
        "Failed watching path: \"" + expOrFn + "\" " +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
      );
    }
  }
  this.value = this.lazy
    ? undefined
    : this.get();
};
/**
 * Evaluate the getter, and re-collect dependencies.
 */
Watcher.prototype.get = function get () {
  // 把自己入栈,读数据的时候就可以收集到自己
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    // 收集依赖
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    // 对象内部的值发生变化,也需要通知依赖。
    if (this.deep) {
      // 把当前值的子值都触发一遍收集依赖的逻辑即可
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
};
/**
 * Recursively traverse an object to evoke all converted
 * getters, so that every nested property inside the object
 * is collected as a "deep" dependency.
 */
function traverse (val) {
  _traverse(val, seenObjects);
  seenObjects.clear();
}
function _traverse (val, seen) {
  var i, keys;
  var isA = Array.isArray(val);
  // 不是数组和对象、已经被冻结,或者虚拟节点,直接返回
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    var depId = val.__ob__.dep.id;
    // 拿到 val 的 dep.id,防止重复收集依赖
    if (seen.has(depId)) {
      return
    }
    seen.add(depId);
  }
  // 如果是数组,循环数组,将数组中的每一项递归调用 _traverse
  if (isA) {
    i = val.length;
    while (i--) { _traverse(val[i], seen); }
  } else {
    keys = Object.keys(val);
    i = keys.length;
    // 重点来了:读取数据(val[keys[i]])触发收集依赖的逻辑
    while (i--) { _traverse(val[keys[i]], seen); }
  }
}
其他章节请看:
vue 快速入门 系列 —— 侦测数据的变化 - [vue api 原理]的更多相关文章
- vue 快速入门 系列 —— 侦测数据的变化 - [vue 源码分析]
		其他章节请看: vue 快速入门 系列 侦测数据的变化 - [vue 源码分析] 本文将 vue 中与数据侦测相关的源码摘了出来,配合上文(侦测数据的变化 - [基本实现]) 一起来分析一下 vue ... 
- vue 快速入门 系列 —— 侦测数据的变化 - [基本实现]
		其他章节请看: vue 快速入门 系列 侦测数据的变化 - [基本实现] 在 初步认识 vue 这篇文章的 hello-world 示例中,我们通过修改数据(app.seen = false),页面中 ... 
- vue 快速入门 系列
		vue 快速入门(未完结,持续更新中...) 前言 为什么要学习 vue 现在主流的框架 vue.angular 和 react 都是声明式操作 DOM 的框架.所谓声明式,就是我们只需要描述状态与 ... 
- vue 快速入门 系列 —— Vue(自身) 项目结构
		其他章节请看: vue 快速入门 系列 Vue(自身) 项目结构 前面我们已经陆续研究了 vue 的核心原理:数据侦测.模板和虚拟 DOM,都是偏底层的.本篇将和大家一起来看一下 vue 自身这个项目 ... 
- vue 快速入门 系列 —— 实例方法(或 property)和静态方法
		其他章节请看: vue 快速入门 系列 实例方法(或 property)和静态方法 在 Vue(自身) 项目结构 一文中,我们研究了 vue 项目自身构建过程,也知晓了 import Vue from ... 
- vue 快速入门 系列 —— Vue 实例的初始化过程
		其他章节请看: vue 快速入门 系列 Vue 实例的初始化过程 书接上文,每次调用 new Vue() 都会执行 Vue.prototype._init() 方法.倘若你看过 jQuery 的源码, ... 
- vue 快速入门 系列 —— vue 的基础应用(上)
		其他章节请看: vue 快速入门 系列 vue 的基础应用(上) Tip: vue 的基础应用分上下两篇,上篇是基础,下篇是应用. 在初步认识 vue一文中,我们已经写了一个 vue 的 hello- ... 
- vue 快速入门 系列 —— vue-cli 上
		其他章节请看: vue 快速入门 系列 Vue CLI 4.x 上 在 vue loader 一文中我们已经学会从零搭建一个简单的,用于单文件组件开发的脚手架:本篇,我们将全面学习 vue-cli 这 ... 
- vue 快速入门 系列 —— vue-router
		其他章节请看: vue 快速入门 系列 Vue Router Vue Router 是 Vue.js 官方的路由管理器.它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌. 什么是路由 ... 
随机推荐
- 【LeetCode】800. Similar RGB Color 解题报告(C++)
			作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 遍历 日期 题目地址:https://leetcode ... 
- 【LeetCode】1019. Next Greater Node In Linked List 解题报告 (Python&C++)
			作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 单调递减栈 日期 题目地址:https://leetc ... 
- 【LeetCode】926. Flip String to Monotone Increasing 解题报告(Python)
			作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 Prefix计算 动态规划 参考资料 日期 题目地址 ... 
- Codeforces 567B:Berland National Library(模拟)
			time limit per test : 1 second memory limit per test : 256 megabytes input : standard input output : ... 
- RabbitMQ学习笔记六:RabbitMQ之消息确认
			使用消息队列,必须要考虑的问题就是生产者消息发送失败和消费者消息处理失败,这两种情况怎么处理. 生产者发送消息,成功,则确认消息发送成功;失败,则返回消息发送失败信息,再做处理. 消费者处理消息,成功 ... 
- ret2dl_resolve
			ret2dl_resolve是一种比较复杂的高级ROP技巧,利用它之前需要先了解动态链接的基本过程以及ELF文件中动态链接相关的结构. 我根据raycp师傅的文章,动手调试了一下: https://r ... 
- Java初学者作业——学生成绩等级流程图练习
			返回本章节 返回作业目录 在Word 中编写算法实现学生成绩等级的输出,并绘制对应算法的流程图. 功能要求:输入学生成绩,输出对应成绩等级,输出规则如下: 学生成绩区间 对应成绩等级 [90,100] ... 
- HTML网页设计基础笔记 • 【第6章 背景和阴影】
			全部章节 >>>> 本章目录 6.1 背景属性 6.1.1 背景颜色 6.1.2 背景图片 6.1.3 背景图片的重复方式 6.2 背景图片的定位 6.2.1 backg ... 
- 编写Java程序随机输入日期计算星期几,打印任意一年的日历
			需求说明: 随机输入日期计算星期几,打印任意一年的日历 已知,1900年1月1日是星期1,用户随机输入年月日,计算星期几 实现思路: 一.知道1900年1月1日为星期一,求输入的年份月份与1900年1 ... 
- Samba服务器搭建与配置
			Samba服务简介Samba的起源:对于windows的网上邻居来讲,共享文件的方式用的是SMB和CIFS协议以及NETBIOS协议Linux/Unix之间用的是NFS协议.  但是Linux和Wi ... 
