响应式2
vue2响应式实现
提供shallow,决定是否需要深度响应
/*******************新增 shallow*******************/
export function defineReactive(obj, key, val, shallow) {
/****************************************************/
const property = Object.getOwnPropertyDescriptor(obj, key);
// 读取用户可能自己定义了的 get、set
const getter = property && property.get;
const setter = property && property.set;
// val 没有传进来话进行手动赋值
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
const dep = new Dep(); // 持有一个 Dep 对象,用来保存所有依赖于该变量的 Watcher
/*******************新增****************************/
// 将新的val也收集响应 observe(val)
!shallow && observe(val);
/******************************************************/
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
}
return value;
},
set: function reactiveSetter(newVal) {
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
},
});
}
/*
util.js
export function isObject(obj) {
return obj !== null && typeof obj === "object";
}
*/
export function observe(value) {
if (!isObject(value)) {
return;
}
let ob = new Observer(value);
return ob;
}
代理模式
import { def } from "./util";
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args);
/*****************这里相当于调用了对象 set 需要通知 watcher ************************/
// 待补充
/**************************************************************************** */
return result;
});
});
export class Observer {
constructor(value) {
/******新增 *************************/
this.dep = new Dep();
/************************************/
this.walk(value);
}
/**
* 遍历对象所有的属性,调用 defineReactive
* 拦截对象属性的 get 和 set 方法
*/
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
}
export function defineReactive(obj, key, val, shallow) {
const property = Object.getOwnPropertyDescriptor(obj, key);
// 读取用户可能自己定义了的 get、set
const getter = property && property.get;
const setter = property && property.set;
// val 没有传进来话进行手动赋值
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
const dep = new Dep(); // 持有一个 Dep 对象,用来保存所有依赖于该变量的 Watcher
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
/******新增 *************************/
if (childOb) {
// 当前 value 是数组,去收集依赖
if (Array.isArray(value)) {
childOb.dep.depend();
}
}
/************************************/
}
return value;
},
set: function reactiveSetter(newVal) {
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
dep.notify();
},
});
}
上面已经重写了array方法,不可以直接覆盖全局的array方法,如果当前value是数组,在observer中拦截array方法。
import { arrayMethods } from './array' // 上边重写的所有数组方法
/* export const hasProto = "__proto__" in {}; */
export class Observer {
constructor(value) {
this.dep = new Dep();
/******新增 *************************/
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
/************************************/
} else {
this.walk(value);
}
}
/**
* 遍历对象所有的属性,调用 defineReactive
* 拦截对象属性的 get 和 set 方法
*/
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
}
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
*/
function protoAugment(target, src) {
/* eslint-disable no-proto */
target.__proto__ = src;
/* eslint-enable no-proto */
}
/**
* Augment a target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
function copyAugment(target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i];
def(target, key, src[key]);
}
}
上面代码中,实现了数组的操作方法,但是并没有给数组中的元素添加响应式,所以我们需要给数组中的元素添加响应式。
export class Observer {
constructor(value) {
this.dep = new Dep();
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);
}
}
/**
* 遍历对象所有的属性,调用 defineReactive
* 拦截对象属性的 get 和 set 方法
*/
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
}
}
如果是多维数组,则需要对多维数组中的元素进行依赖
export function defineReactive(obj, key, val, shallow) {
const property = Object.getOwnPropertyDescriptor(obj, key);
// 读取用户可能自己定义了的 get、set
const getter = property && property.get;
const setter = property && property.set;
// val 没有传进来话进行手动赋值
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
const dep = new Dep(); // 持有一个 Dep 对象,用来保存所有依赖于该变量的 Watcher
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
if (Array.isArray(value)) {
childOb.dep.depend(); // [["hello", "wind"],["hello", "liang"]] 这个整体进行了依赖的收集
/******新增 *************************/
dependArray(value); // 循环数组中的元素,如果是数组的话进行依赖收集。
/************************************/
}
}
}
return value;
},
...
}
function dependArray(value) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i];
if (Array.isArray(e)) {
e && e.__ob__ && e.__ob__.dep.depend();
dependArray(e); // 递归进行
}
}
}
同时也需要对插入的数据进行依赖收集,如果是数组,进行数组的依赖收集。
如果是数组,需要将数组中的元素进行响应式处理,对于新添加的元素也进行响应式处理。收集依赖的时候,需要对数组中的数组进行依赖收集。
数组的set和delete方法
数组set
list[0]不会触发watcher收集。数组只能通过重写的push,splice方法去触发
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
export function set(target, key, val) {
if (Array.isArray(target)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
// targe 是对象的情况
// ...
}
数组del
/**
* Delete a property and trigger change if necessary.
*/
export function del(target, key) {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1);
return;
}
// targe 是对象的情况
// ...
}
对象set和delete方法
- 对象set方法
// 例子
import { observe, set, del } from "./reactive";
import Watcher from "./watcher";
const data = {
obj: {
a: 1,
b: 2,
},
};
observe(data);
const updateComponent = () => {
const c = data.obj.c ? data.obj.c : 0;
console.log(data.obj.a + data.obj.b + c);
};
new Watcher(updateComponent);
data.obj.c = 3;
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
export function set(target, key, val) {
if (Array.isArray(target)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
// targe 是对象的情况
// key 在 target 中已经存在
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
const ob = target.__ob__;
// target 不是响应式数据
if (!ob) {
target[key] = val;
return val;
}
// 将当前 key 变为响应式的
defineReactive(target, key, val);
return val;
}
上面的代码虽然设置了set,但是不会新收集watcher, 需要手动调用。
这里将代码中的对象的dep进行收集对象元素中dep收集的watcher
export function defineReactive(obj, key, val, shallow) {
const property = Object.getOwnPropertyDescriptor(obj, key);
// 读取用户可能自己定义了的 get、set
const getter = property && property.get;
const setter = property && property.set;
// val 没有传进来话进行手动赋值
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
const dep = new Dep(); // 持有一个 Dep 对象,用来保存所有依赖于该变量的 Watcher
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
/******新位置 *************************/
childOb.dep.depend();
/**********************************/
if (Array.isArray(value)) {
// childOb.dep.depend(); //原来的位置
dependArray(value);
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
},
});
}
function dependArray(value) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i];
/******新位置 *************************/
e && e.__ob__ && e.__ob__.dep.depend();
/**********************************/
if (Array.isArray(e)) {
// e && e.__ob__ && e.__ob__.dep.depend(); // 原位置
dependArray(e);
}
}
}
并不知道 c 被哪些 Watcher 依赖,我们只知道和 c 同属于一个对象的 a 和 b 被哪些 Watcher 依赖,但大概率 c 也会被其中的 Watcher 依赖。所以我们可以在set中手动执行一下obj的Dep,依赖c的Watcher大概率会被执行,相应的c也会成功收集到依赖。
c中的watcher不会在收集,这种情况下,只能通过父级收集依赖,触发依赖,来更新后续步骤。
- 对象delete方法
如果要是删除a属性,删除后执行他相应的dep就行。
同上诉的思想一样,这里也需要更新父级。这样就可以完成delete。
/**
* Delete a property and trigger change if necessary.
*/
export function del(target, key) {
if (Array.isArray(target)) {
target.splice(key, 1);
return;
}
// targe 是对象的情况
const ob = target.__ob__;
if (!hasOwn(target, key)) {
return;
}
delete target[key];
if (!ob) {
return;
}
ob.dep.notify();
}
vue3响应式实现
reactive
在vue2中需要重写数组的方法,来达到对数组响应式。
但是在vue3中,却是不需要,因为vue3中使用的是proxy来做数据拦截,可以原生支持数组的响应式。
这里来解释一下object.defineProperty
和Proxy
object.defineproperty实际上是通过定义或者修改对象属性的描述符来实现数据劫持,缺点是只能拦截get和set操作,无法拦截delete,in,方法调用等属性。动态添加新属性,保证后续使用的属性要在初始化生命data的时候定义,通过this.
s
e
t
设置新属性。通过
d
e
l
e
t
e
删除属性,响应式丢失,通过
t
h
i
s
.
set设置新属性。通过delete删除属性,响应式丢失,通过this.
set设置新属性。通过delete删除属性,响应式丢失,通过this.delete()删除属性。通过数组索引替换/新增元素,响应式丢失,使用this.$set来设置新元素。使用数组push,pop,shift,unshif,splice,sort,reverse等原生方法来改变原数组的时候,会响应式丢失,需要使用重写/增强后的push,pop,shift,unshift,splice,sort,reverse方法。一次只能对一个属性实现数据劫持,需要遍历对所有的属性进行劫持。数据结构复杂的时候,属性值为引用类型数据,需要通过递归进行处理。
其实object.defineProperty也能拦截Array。但是,还是有如下原因。1. 数组和普通对象在使用场景下会有区别,在项目中使用数组的目的是为了遍历,比较少会使用array[index]=xxx的形式。2. 数组长度是多变的,不可能和普通对象一样在data选项中提前声明好的所有元素。通过array[index]=xxx方式赋值的时候,一旦index超过了现有最大的索引值,那么当前添加的新元素也不会具有响应式。3. 数组存储的元素比较多,不可能为每个数组元素都这是getter/setter。4.无法拦截数组原生方法,比如push,pop,shift,unshift等的调用,最终任然需要重写和增强方法。
proxy主要是用来创建一个对象的代理,从而实现基本操作的拦截和自定义(比如属性查找, 赋值, 枚举, 函数调用等),本质上是通过拦截对象内部方法的执行实现代理,而对象本身根据规范定义的不同又会分为常规对象和异质对象。
- get()属性读取操作捕获的捕获器。
- set()属性设置操作的捕获器。
- deleteProperty()是delete操作符的捕捉器。
- ownkeys()是object.getOwnPropertyNames方法和Object.getOwnPropertySymbols方法的捕获器。
- has()是in操作符的捕获器。
export function reactive(target: object) {
is(isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
target为传进来的对象
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlerrs: ProxyHandler<any>,
proxyMap: WeakMap<Target>
) {
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 非对象类型直接返回
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 目标数据的 __v_raw 属性若为 true,且是【非响应式数据】或 不是通过调用 readonly() 方法,则直接返回
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 目标对象已存在相应的 proxy 代理对象,则直接返回
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 只有在白名单中的值类型才可以被代理监测,否则直接返回
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 创建代理对象
const proxy = new Proxy(
target,
// 若目标对象是集合类型(Set、Map)则使用集合类型对应的捕获器,否则使用基础捕获器
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
}
createReactiveObject()函数需要做一些前置判断处理。
- 目标数据是原始值类型,直接返回原数据。
__v_raw
属性为true,而且非响应式数据或不是通过调用readonly()方法。直接返回原数据。- 若目标数据中已经存在响应的proxy代理对象,则直接返回对应的代理对象。
- 若目标数据中不存在对应的白名单数据类型中,则直接返回原数据。
支持响应式的数据类型为- 可拓展的对象,即他的上面上可以添加新的属性
__v_skip
属性不存在或者是值为false的对象- 数据类型为object, Array, Map, Set, WeakMap, WeakSet对象。
- 其他数据都被认为无效的响应式对象。
// 白名单
function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
Handlers捕获器
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
这些对应的就是读取,设置,删除,判断是否存在对应的属性,获取对象自身的属性值。
get捕获器
class BaseReactiveHandler implements ProxyHandler<Target> {
constructor(
protected readonly _isReadonly = false,
protected readonly _isShallow = false,
) {}
get(target: Target, key: string | symbol, receiver: object) {
const isReadonly = this._isReadonly,
isShallow = this._isShallow
// 当直接通过指定 key 访问 vue 内置自定义的对象属性时,返回其对应的值
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return isShallow
} else if (key === ReactiveFlags.RAW) {
if (
receiver ===
(isReadonly
? isShallow
? shallowReadonlyMap
: readonlyMap
: isShallow
? shallowReactiveMap
: reactiveMap
).get(target) ||
// receiver is not the reactive proxy, but has the same prototype
// this means the reciever is a user proxy of the reactive proxy
Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)
) {
return target
}
// early return undefined
return
}
// 判断是否为数组类型
const targetIsArray = isArray(target)
// 数组对象
if (!isReadonly) {
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
// 重写/增强数组的方法
// - 查找方法: includes, indexof, lastIndexOf
// - 修改原数组的方法: push, pop, unshift, shift, splice
return Reflect.get(arrayInstrumentations, key, receiver)
}
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}
// 获取对应属性值
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (isShallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - skip unwrap for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
- 若当前数据对象是数组,则重写/增强数组对应的方法。
- 数组的查找方法: includes, indexOf, lastIndexOf
- 修改原数组的方法: push, pop, unshift, shift, splice
- 若当前数据对象是普通对象,且非只读的则通过track(target, TrackOpTypes.GET, key)进行依赖收集
- 若当前数据对象是浅层响应的,则直接返回其对应属性值。
- 当前对象是ref类型的,则会自动脱ref
- 若当前数据对象的属性值是对象类型
- 若当前属性值属于是只读的,则通过readonly(res)向外返回其结果
- 否则会将当前的属性值以reactie(res)向外返回proxy代理对象
- 否则直接想外返回对应的属性值
数组类型捕获
数组的查找方法中包含includes, indexof, lastIndexOf,这些方法通常情况下是能够按照预期工作的,但是还需要对某些情况进行特殊处理。
- 查找的目标数据是响应式数据本身。
const obj = {}
const proxy = reactive([obj])
console.log(proxy.includes(proxy[0])) // false
- 产生原因: 这里涉及到了两次读取操作,第一次是proxy[0],这个时候会触发get捕获器并为obj生成对应代理对象并返回。第二次是proxy.includes()的调用,会遍历数组的每个元素,就是触发get捕获其,生成一个新的代理对象并返回,这两次生成的代理独享不是同一个,因此返回false
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()
function createArrayInstrumentations() {
const instrumentations: Record<string, Function> = {}
// instrument identity-sensitive Array methods to account for possible reactive
// values
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
const arr = toRaw(this) as any
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
// we run the method using the original args first (which may be reactive)
const res = arr[key](...args)
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
return arr[key](...args.map(toRaw))
} else {
return res
}
}
})
// instrument length-altering mutation methods to avoid length being tracked
// which leads to infinite loops in some cases (#2137)
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
pauseTracking()
pauseScheduling()
const res = (toRaw(this) as any)[key].apply(this, args)
resetScheduling()
resetTracking()
return res
}
})
return instrumentations
}
set捕获器
处理数组索引index和length
数组的index和length是会相互影响的。
- 当Number(key)<target.length => 证明是修改操作。对应的是TriggerOpTypes.SET类型,即当前操作不会改变length的值,不需要触发和length相关副作用的执行
- 当Number(key)>=target.length => 证明是新增操作,TriggerOpTypes.ADD类型,当前操作会改变length的值,需要触发和length相关副作用函数的执行。
set(
target: object,
key: string | symbol,
value: unknown,
receiver: object,
): boolean {
// 保存旧的数据
let oldValue = (target as any)[key]
// 若原数据值属于只读且ref类型,并且新数据值不属于ref类型,则意味着修改失败
if (!this._isShallow) {
const isOldValueReadonly = isReadonly(oldValue)
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
value = toRaw(value)
}
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
if (isOldValueReadonly) {
return false
} else {
oldValue.value = value
return true
}
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
// 是否存在对应的key
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
// 设置对应值
const result = Reflect.set(target, key, value, receiver)
// 若目标对象是原始原型链上的内容(非自定义添加),则不触发依赖更新
if (target === toRaw(receiver)) {
if (!hadKey) {
// 若目标对象不在对应的key上,则为新增操作。
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 目标对象存在对应的值,则为修改操作。
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
deleteProperty has ownKeys捕获器
这三个捕获器内容非常简洁,其中has和ownKeys本质上也属于读取操作,因此需要通过track()进行依赖搜集,而deleteProperty相当于修改操作。因此需要trigger()触发更新
deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key)
}
return result
}
ownKeys(target: object): (string | symbol)[] {
track(
target,
TrackOpTypes.ITERATE,
isArray(target) ? 'length' : ITERATE_KEY,
)
return Reflect.ownKeys(target)
}