Skip to main content

2 posts tagged with "vue2"

View All Tags

· 3 min read

vue2 主要是基于 Object.defineProperty 做的数据双向绑定,这里从 observer、watcher、dep 出发做一个简单的数据绑定实例

主要步骤

  1. observer 递归监听 data 对象的所有属性
  2. watcher 订阅器,视图、指令等需要绑定某个属性时会新建一个订阅器,并加入 Dep
  3. Dep 存放绑定某个属性的所有订阅器,可以添加订阅器和通知变化
  4. 数据更改时,通知 Dep 去执行相应的更新函数

vue原理图示

Dep

订阅器集合,也就是绑定了某个属性的视图或其他对象的集合。

class Dep {
constructor() {
this.deps = [];
}
// 添加订阅器
addDep(dep) {
this.deps.push(dep);
}
// 更新所有订阅器
notify(newData) {
for (let dep of this.deps) {
dep.update(newData);
}
}
}

Observer

递归监听 data 对象的属性

// defineReactive 监听对象属性
function defineReactive(obj, key, val) {
// deps 订阅器集合,作为闭包,方便处理订阅器的添加和更新操作
let deps = new Dep();

Object.defineProperty(obj, key, {
get() {
// 判断是否需要添加订阅器
// Dep.target 用于暂存订阅器
Dep.target && deps.addDep(Dep.target);
},
set(newData) {
// 对比数据,如果变化了就通知订阅器
if (newData !== val) {
deps.notify(newData);
}
},
});
}
function observer(data) {
if (!data || typeof data !== "object") {
return null;
}
for (key in data) {
if (data.hasOwnProperty(key)) {
if (typeof data[key] === "object") {
// 递归监听
observer(data[key]);
}
defineReactive(data, key, data[key]);
}
}
}

Watcher

  • 绑定数据时,需要新建一个 watcher 实例,比如视图绑定、computed、watch
  • 订阅器初始化时,会自动将自己添加进 deps 里
// Watcher 订阅器
class Watcher {
constructor(id, vm, key, cb) {
console.log(`订阅器${id}`);
this.id = id; // 订阅器标识
this.vm = vm; // 当前环境
this.cb = cb; // 更新回调
this.key = key; // 当前监听属性
// 触发getter,将自身添加到属性的订阅器数组deps中
Dep.target = this;
let val = vm.data[key]; // 强制执行 get,加入到订阅器集合
Dep.target = null;
}
update(newData) {
console.log(`订阅器${this.id}数据更新了,newData: ${newData}`);
// this.cb()
}
}

触发更新

// 新增订阅器
this.data = {
name: "jack",
action: "eating",
};
observer(this.data);
new Watcher(1, this, "name");
new Watcher(2, this, "action");
this.data.name = "Tom";

· 6 min read

Vue2.x 不能检测以下数组的变动:1.直接修改数组长度;2.通过数组下标赋值。第一点主要是收到defineProperty的限制,第二点要究其原因,还是得从源码分析。

直接修改数组长度

Vue2.x 利用Object.defineProperty劫持对象的访问器,在属性值发生变化时我们可以获取变化,实现对数据变化的双向绑定。但是Object.defineProperty并不能监控数组长度的变化,因为数组对象中的 length 属性的 configurablefalse,不允许访问器对该属性进行操作

关于数据双向绑定,可以参考基于 defineProperty 和 proxy 实现数据双向绑定

通过数组下标赋值

vue 怎么监听数组

Observer,是实现 vue 响应式的核心模块,用于监听对象和依赖追踪

// Observer 监听对象
var Observer = function Observer(value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, "__ob__", this);
if (Array.isArray(value)) {
// 将重写的数组方法挂载到数组原型对象上
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};

可以看到,observer 遇到数组会走单独的处理逻辑,首先将重写的数组方法挂到数组原型方法上,然后进一步通过observeArray处理数组

// observeArray
Observer.prototype.observeArray = function observeArray(items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
// 如果items[i]是数组,会新建 Observer 对象,然后继续执行observe, 直到遍历完毕
}
};
// observe
function observe(value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
// 如果要观测的数据不是一个对象或者是 VNode 实例,则不做监听
return;
}
var ob;
if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
// 已经添加过observer实例,直接返回
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob;
}

可以看到,vue 并没有对数组的所有元素进行监听,所以通过数组下标修改或增加元素,会发现依赖该数组元素的视图并不会更新

重写数组方法

Vue 2.x 针对数组的部分原型方法做了特殊处理,通过调用这些重写的方法,可以对新增的元素的索引进行监听,确保新增元素是响应式的

var methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];

methodsToPatch.forEach(function (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);
var ob = this.__ob__;
var inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
if (inserted) {
ob.observeArray(inserted);
}
// 重新触发依赖更新
ob.dep.notify();
return result;
});
});

通过源码可以看到,在操作数组时,如果使用 push、unshift、splice 添加或修改元素,会重新触发依赖的更新函数,渲染对应视图或对象

Vue.set

除了上述方法,我们还可以通过Vue.set来实现对数组的更新,并渲染对应视图。贴一波 set 函数源码,源码分析可以看下注释

function set (target, key, val) {
......
if (Array.isArray(target) && isValidArrayIndex(key)) { // 处理数组元素
target.length = Math.max(target.length, key)
// 使用重写过的splice更新数组
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
// 是对象原有属性,赋值即可触发更新函数
target[key] = val
return val
}
const ob = target.__ob__
// __ob__指向target的observer实例
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.'
)
// 对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性
// 所以会直接返回值
return val
}
// target非响应式对象,不用对属性监听,直接赋值并返回
if (!ob) {
target[key] = val
return val
}
// 给新增的对象属性添加依赖
defineReactive(ob.value, key, val)
// 触发依赖,重新渲染页面
ob.dep.notify()
return val
}