其他章节请看:

vue 快速入门 系列

侦测数据的变化 - [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 快速入门 系列 —— 侦测数据的变化 - [vue api 原理]的更多相关文章

  1. vue 快速入门 系列 —— 侦测数据的变化 - [vue 源码分析]

    其他章节请看: vue 快速入门 系列 侦测数据的变化 - [vue 源码分析] 本文将 vue 中与数据侦测相关的源码摘了出来,配合上文(侦测数据的变化 - [基本实现]) 一起来分析一下 vue ...

  2. vue 快速入门 系列 —— 侦测数据的变化 - [基本实现]

    其他章节请看: vue 快速入门 系列 侦测数据的变化 - [基本实现] 在 初步认识 vue 这篇文章的 hello-world 示例中,我们通过修改数据(app.seen = false),页面中 ...

  3. vue 快速入门 系列

    vue 快速入门(未完结,持续更新中...) 前言 为什么要学习 vue 现在主流的框架 vue.angular 和 react 都是声明式操作 DOM 的框架.所谓声明式,就是我们只需要描述状态与 ...

  4. vue 快速入门 系列 —— Vue(自身) 项目结构

    其他章节请看: vue 快速入门 系列 Vue(自身) 项目结构 前面我们已经陆续研究了 vue 的核心原理:数据侦测.模板和虚拟 DOM,都是偏底层的.本篇将和大家一起来看一下 vue 自身这个项目 ...

  5. vue 快速入门 系列 —— 实例方法(或 property)和静态方法

    其他章节请看: vue 快速入门 系列 实例方法(或 property)和静态方法 在 Vue(自身) 项目结构 一文中,我们研究了 vue 项目自身构建过程,也知晓了 import Vue from ...

  6. vue 快速入门 系列 —— Vue 实例的初始化过程

    其他章节请看: vue 快速入门 系列 Vue 实例的初始化过程 书接上文,每次调用 new Vue() 都会执行 Vue.prototype._init() 方法.倘若你看过 jQuery 的源码, ...

  7. vue 快速入门 系列 —— vue 的基础应用(上)

    其他章节请看: vue 快速入门 系列 vue 的基础应用(上) Tip: vue 的基础应用分上下两篇,上篇是基础,下篇是应用. 在初步认识 vue一文中,我们已经写了一个 vue 的 hello- ...

  8. vue 快速入门 系列 —— vue-cli 上

    其他章节请看: vue 快速入门 系列 Vue CLI 4.x 上 在 vue loader 一文中我们已经学会从零搭建一个简单的,用于单文件组件开发的脚手架:本篇,我们将全面学习 vue-cli 这 ...

  9. vue 快速入门 系列 —— vue-router

    其他章节请看: vue 快速入门 系列 Vue Router Vue Router 是 Vue.js 官方的路由管理器.它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌. 什么是路由 ...

随机推荐

  1. SpringBoot 上传文件功能

    注意事项: springboot默认有以下文件配置要求, 可以自行在配置文件里面修改 spring: servlet: multipart: enabled: true #是否处理上传 max-fil ...

  2. Python pyecharts绘制折线图

    一.pyecharts绘制折线图line.add()方法简介 line.add()方法简介 add(name,x_axis,y_axis,is_symbol_show=True, is_smooth= ...

  3. 【linux】环境变量生命周期的操作方式

    目录 前言 1. 修改环境变量 1.1 手动指定 1.2 临时生效 1.3 永久生效 链接 前言 参考: 李柱明博客 本文主要记录 linux 环境变量配置的生命周期. 如,修改环境变量 PATH 是 ...

  4. 【LeetCode】604. Design Compressed String Iterator 解题报告(C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 维护当前字符和次数 日期 题目地址:https://l ...

  5. 【LeetCode】747. Largest Number At Least Twice of Others 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 寻找两次最大值 排序 大顶堆 日期 题目地址:htt ...

  6. 【剑指Offer】栈的压入、弹出队列 解题报告(Python)

    [剑指Offer]栈的压入.弹出队列 解题报告(Python) 标签(空格分隔): 剑指Offer 题目地址:https://www.nowcoder.com/ta/coding-interviews ...

  7. 使用VUE CLI3.0搭建项目vue2+scss+element简易版

    1.安装Vue CLI 3 //三选一即可cnpm install -g @vue/cli npm install -g @vue/cli yarn global add @vue/cli 注意: 1 ...

  8. Redis常见使用场景

    缓存 数据共享分布式 分布式锁 全局ID 计数器 限流 位统计 购物车 用户消息时间线timeline 消息队列 抽奖 点赞.签到.打卡 商品标签 商品筛选 用户关注.推荐模型 排行榜 1.缓存 St ...

  9. Java面向对象笔记 • 【第6章 Java常用类】

    全部章节   >>>> 本章目录 6.1 Object类 6.1.1 Object类概述 6.1.2 Object的常用方法 6.1.3 实践练习 6.2 String类和St ...

  10. TortoiseGit使用技巧

    汇总TortoiseGit使用技巧,包括提交代码,创建patch等等. 1.提交代码到本地仓库 在Git工程目录下右键, 点击 Git Commit -> "master". ...