其他章节请看:

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. JS根据日期获取判断星期几

    /** * 根据日期字符串获取星期几 * @param dateString 日期字符串(如:2020-05-02) * @returns {String} */ function getWeek(d ...

  2. C++常用工具库(C语言文件读写,日志库,格式化字符串, 获取可执行文件所在绝对路径等)

    前言 自己常用的工具库, C++ 和C语言实现 使用cmake维护的项目 持续更新..... 提供使用范例, 详见example文件夹 windows使用的VS通过了的编译. Linux(Ubuntu ...

  3. 平衡二叉树(c++)实现(存在问题:插入节点后,问题:调整树的结构存在问题)

    !!版权声明:本文为博主原创文章,版权归原文作者和博客园共有,谢绝任何形式的 转载!! 作者:mohist 更新那时间: 22:13  03-02-2020  逻辑存在问题:插入节点后,调整数的结构不 ...

  4. 【LeetCode】152. Maximum Product Subarray 解题报告(Python & C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 双重循环 动态规划 参考资料 日期 题目地址:htt ...

  5. 【LeetCode】519. Random Flip Matrix 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 题目地址:https://leetcode.com/problems/random-fl ...

  6. 【LeetCode】200. Number of Islands 岛屿数量

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 DFS BFS 日期 题目地址:https://le ...

  7. 【LeetCode】458. Poor Pigs 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...

  8. 【LeetCode】821. Shortest Distance to a Character 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 过两遍数组 日期 题目地址:https://leet ...

  9. 郑厂长系列故事——体检(hdu 4519)

    郑厂长系列故事--体检 Time Limit: 500/200 MS (Java/Others)    Memory Limit: 65535/32768 K (Java/Others)Total S ...

  10. 第二十一个知识点:CRT算法如何提高RSA的性能?

    第二十一个知识点:CRT算法如何提高RSA的性能? 中国剩余定理(The Chinese Remainder Theorem,CRT)表明,如果我们有两个等式\(x = a \mod N\) 和\(x ...