其他章节请看:

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. FastAPI 学习之路(六十)打造系统的日志输出

    我们要搭建日志系统,我们使用loguru,挺不错的一个开源的日志系统.可以使用 pip install loguru 我们在common创建log.py使用方式也很简单 import os impor ...

  2. 【LeetCode】974. Subarray Sums Divisible by K 解题报告(C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 动态规划 前缀和求余 日期 题目地址:https:/ ...

  3. UVA11754 - Code Feat

    Hooray!  Agent Bauer has shot the terrorists, blown upthe bad guy base, saved the hostages, exposed ...

  4. 1002 - Country Roads(light oj)

    1002 - Country Roads I am going to my home. There are many cities and many bi-directional roads betw ...

  5. 最大流问题的Ford-Fulkerson模板

    详细讲解:http://blog.csdn.net/smartxxyx/article/details/9293665 下面贴上我的第一道最大流的题: hdu3549 1 #include<st ...

  6. 《HelloGitHub》第 69 期

    兴趣是最好的老师,HelloGitHub 让你对编程感兴趣! 简介 HelloGitHub 分享 GitHub 上有趣.入门级的开源项目. https://github.com/521xueweiha ...

  7. TriggerBN ++

    目录 motivation settings results motivation 用两个BN(一个用于干净样本, 一个用于对抗样本), 结果当使用\(\mathrm{BN}_{nat}\)的时候, ...

  8. NFS 部署

    目录 NFS 部署 NFS简介 NFS应用 NFS工作流程图 NFS部署 服务端 客户端 测试NFS文件同步功能 NFS配置详解 NFS部分参数案例 统一用户 搭建考试系统 搭建步骤 配合NFS实现文 ...

  9. 啥是Gossip协议?

    你好呀,我是歪歪. 元旦的时候我看到一个特别离谱的谣言啊,具体是什么内容我就不说了,我怕脏了大家的眼睛. 但是,我看到一个群里传的那叫一个绘声绘色,大家讨论的风生水起的,仿佛大家就在现场似的. 这事吧 ...

  10. Jsonschema2pojo从JSON生成Java类(命令行)

    1.说明 jsonschema2pojo工具可以从JSON Schema(或示例JSON文件)生成Java类型, 在文章Jsonschema2pojo从JSON生成Java类(Maven) 已经介绍过 ...