vue 快速入门 系列 —— 侦测数据的变化 - [vue 源码分析]
其他章节请看:
侦测数据的变化 - [vue 源码分析]
本文将 vue 中与数据侦测相关的源码摘了出来,配合上文(侦测数据的变化 - [基本实现]) 一起来分析一下 vue 是如何实现数据侦测的。
Tip: 以下代码出自 vue.esm.js,版本为 v2.5.20。无关代码有一些删减。中文注释都是笔者添加。
/**
* Define a property.
* 定义属性的方法
*/
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
/**
* Parse simple path.
* 解析简单路径。比如 vm.$watch('a.b.c', function(){})
*/
var bailRE = /[^\w.$]/;
function parsePath (path) {
if (bailRE.test(path)) {
return
}
var segments = path.split('.');
return function (obj) {
// 例如 a.b.c
for (var i = 0; i < segments.length; i++) {
if (!obj) { return }
// 最后读取到 c
obj = obj[segments[i]];
}
return obj
}
}
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
* 依赖。对我们的依赖列表 dependList 进行了封装,这里提取出来了一个类,用于存储依赖(Watcher)。
*/
var Dep = function Dep () {
this.id = uid++;
// subs 也就是我们的依赖列表 dependList
this.subs = [];
};
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
};
Dep.prototype.removeSub = function removeSub (sub) {
remove(this.subs, sub);
};
// 收集依赖
Dep.prototype.depend = function depend () {
// Dep.target 也就是我们的全局变量(globalData),指向 Watcher。
if (Dep.target) {
// 收集依赖 Watcher
Dep.target.addDep(this);
}
};
// 通知依赖
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
// 类似我们的全局变量(globalData ),用于存储 Watcher
Dep.target = null;
var targetStack = [];
function pushTarget (target) {
targetStack.push(target);
Dep.target = target;
}
function popTarget () {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
// 接下来是侦测数组的变化
// 也就是通过拦截器来实现数组的侦测
var arrayProto = Array.prototype;
// arrayMethods就是拦截器
var arrayMethods = Object.create(arrayProto);
// 能改变数组的7个方法
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
/**
* Intercept mutating methods and emit events
* 给拦截器(arrayMethods)定义以上7个方法
*/
methodsToPatch.forEach(function (method) {
// cache original method
// 数组的原始方法
var original = arrayProto[method];
def(arrayMethods, method, function mutator () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
// 调用拦截器中的方法,拦截器接着会去调用数组中对应的方法
var result = original.apply(this, args);
// 数据变成响应式后,数据上就会挂载 __ob__(Observer 的实例) 属性,里面有数据的依赖
var ob = this.__ob__;
// 只有 push、unshift、splice 这三个方法能增加数据,而增加的数据也需要转为响应式
var inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break
case 'splice':
inserted = args.slice(2);
break
}
// 数组增加的数据也需要转为响应式
if (inserted) { ob.observeArray(inserted); }
// notify change
// 通知依赖
ob.dep.notify();
return result
});
});
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
* 1. 将数据转为响应式的主入口。
* 2. 在我们的实现中是通过 defineReactive() 将数据转为响应式,没有递归侦测所有的 key。比如
* data = {a: 1, b: {c:1}},我们只侦测了数据的第一层(data.a、data.b),孩子节点如果是对象,
* 也需要侦测 data.b.c。
* 3. 递归侦测调用顺序:Observer -> walk -> defineReactive$$1 -> observe -> Observer
* 4. 将对象和数组分别处理。
*/
var Observer = function Observer (value) {
this.value = value;
// 定义依赖,用于存储于数据有关的依赖
// 比如数据 let data = {a: [11,22]},某处使用了 data.a。当执行 data.a.push(33) 时,
// data.a 就应该通知其依赖
this.dep = new Dep();
this.vmCount = 0;
// 将 this 挂载到数据的 __ob__ 属性上。Array 的拦截器就可以通过数据取得 Observer 的 dep,从而通知依赖
def(value, '__ob__', this);
if (Array.isArray(value)) {
// 如果有原型,就通过更改原型的方式将拦截器挂载到数组上,否则就将拦截器中的方法依次拷贝到数组上
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
// 数组中的每一项也需要转为响应式
this.observeArray(value);
} else {
// 依次遍历对象中每个 key,将其转为响应式
this.walk(value);
}
};
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
// 通过 Object.defineProperty() 侦测对象
defineReactive$$1(obj, keys[i]);
}
};
/**
* Observe a list of Array items.
*/
Observer.prototype.observeArray = function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
* 通过更改原型来挂载拦截器,实现数组的侦测。
*/
function protoAugment (target, src) {
// 作用与 setPrototype 相同
target.__proto__ = src;
}
// 将拦截器中的方法拷贝到数组中,实现数组的侦测。
function copyAugment (target, src, keys) {
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
def(target, key, src[key]);
}
}
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
* 观察数据。如果数据不是对象,直接返回;如果已经是响应式,则返回 Observer 的实例;否则将值转为响应式
*/
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
// 不能是 vue 实例
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
/**
* Define a reactive property on an Object.
* 侦测数据变化。功能与我们的 defineReactive() 方法类似。
*/
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
// 每个 key 都有一个 Dep 用于存储依赖
// dep 就是我们的依赖列表
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
// 值如果是对象,也需要转为响应式
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
// 如果有值
if (Dep.target) {
// 收集依赖
dep.depend();
// 如果值(childOb)是对象,childOb也需要收集依赖
if (childOb) {
// 可能主要针对数组?
childOb.dep.depend();
// 数组中数据,如果需要也得收集依赖,因为里面的数据若发生变化,应该通知外界
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
// 新的值也需要转为响应式
childOb = !shallow && observe(newVal);
// 通知依赖
dep.notify();
}
});
}
/**
* 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.
* Watcher 相对比较复杂,稍微分析一下
*/
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
// vue 实例
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
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
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
// parse expression for getter
// expOrFn 可以是函数,也可以是表达式,例如 a.b.c,统一为 this.getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
warn(
"Failed watching path: \"" + expOrFn + "\" " +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
);
}
}
// 通过 get() 方法读取数据
this.value = this.lazy
? undefined
: this.get();
};
/**
* Evaluate the getter, and re-collect dependencies.
*
*/
Watcher.prototype.get = function get () {
// 会将自己赋值给 Dep.target
pushTarget(this);
var value;
var vm = this.vm;
try {
// 调用 Watcher 构造函数中分装的 getter() 方法
// 触发数据的 getter,从而收集依赖(Watcher)
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
};
/**
* Add a dependency to this directive.
*/
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
其他章节请看:
vue 快速入门 系列 —— 侦测数据的变化 - [vue 源码分析]的更多相关文章
- vue 快速入门 系列 —— 侦测数据的变化 - [vue api 原理]
其他章节请看: vue 快速入门 系列 侦测数据的变化 - [vue api 原理] 前面(侦测数据的变化 - [基本实现])我们已经介绍了新增属性无法被侦测到,以及通过 delete 删除数据也不会 ...
- 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 的核心深度集成,让构建单页面应用变得易如反掌. 什么是路由 ...
随机推荐
- Serverless & Cloudflare Workers
Serverless & Cloudflare Workers https://dash.cloudflare.com/6f3d5e68ab80892a372313b7c9b02a85/wor ...
- local JSON file loader in js
local JSON file loader in js "use strict"; /** * * @author xgqfrms * @license MIT * @copyr ...
- js 生成Excel
https://www.npmjs.com/package/xlsx 安装依赖 npm install xlsx Example import * as XLSX from "xlsx&qu ...
- NGK数字增益平台的算力是什么?
今年的币价回暖带来了新一轮的"信仰充值",部分投资者对比特币的兴趣从购买向更源头的算力转移.随着比特币开采数量逐渐减少,全网算力一直在增加,算力难度也是越来越高.同时在算力行业中竞 ...
- go-admin在线开发平台学习-2[程序结构分析]
紧接着上一篇,本文我们对go-admin下载后的源码进行分析. 首先对项目所使用的第三方库进行分析,了解作者使用的库是否是通用的官方库可以有助于我们更快地阅读程序.接着对项目的main()方法进行分析 ...
- 从崩溃的选课系统,论为什么更安全的 HTTPS 协议没有被全面采用
尽人事,听天命.博主东南大学研究生在读,热爱健身和篮球,正在为两年后的秋招准备中,乐于分享技术相关的所见所得,关注公众号 @ 飞天小牛肉,第一时间获取文章更新,成长的路上我们一起进步 本文已收录于 C ...
- C++算法代码——卡片游戏
题目来自:http://218.5.5.242:9018/JudgeOnline/problem.php?cid=1397&pid=2 题目描述 桌上有一叠牌,从第一张牌(即位于顶面的牌)开始 ...
- 将AOSP源码导入到Android Studio进行查看
生成iml和ipr文件 source build/envsetup.sh lunch aosp_x86-eng # 或者直接输入lunch,然后选择对应的target make idegen deve ...
- java自学第3期——继承、多态、接口、抽象类、final关键字、权限修饰符、内部类
一.继承: 关键字extends /* 定义一个父类:人类 定义父类格式:public class 父类名称{ } 定义子类格式:public class 子类名称 extends 父类名称{ } * ...
- 你真的懂 MP4 格式吗?
MP4 文件格式又被称为 MPEG-4 Part 14,出自 MPEG-4 标准第 14 部分 .它是一种多媒体格式容器,广泛用于包装视频和音频数据流.海报.字幕和元数据等.(顺便一提,目前流行的视频 ...