# 响应式
# 概念
响应式(Reactivity)是一种允许我们以声明式的方式去适应变化的编程范例
通俗来说,就是数据变化了,响应的视图会更新(重新渲染)
# 实现思路
- 当值被访问(touch)时触发跟踪(track)函数,收集依赖(collect as dependency)。
- 检测值是否发生变化
- 当值变化(setter)时触发(trigger)函数通知(notify)该值相关的依赖更新(re-render)

# Vue2的实现
# 基本原理
Object.defineProperty
ES5的Object.defineProperty()方法支持在一个对象上定义个新属性prop,或者修改一个对象现有属性prop,并返回此对象
- 语法: Object.defineProperty(obj, prop, descriptor);
- 参数
- obj: 要定义属性的对象
- prop: 要定义或修改的属性的名称或Symbol
- descriptor:要定义或修改的属性描述符。对象里目前存在的属性描述符有两种主要形式:数据描述符和存储描述符。数据描述符是一个具有值的属性,改值可以是可写的,也可以是不可写的。存取描述符是由getter函数和setter函数所描述的属性
存取描述符的关键键值
get
属性的getter函数,默认为undefined。当访问该属性时,会调用次函数。执行时不传入任何参数,但是也会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象),该函数的返回值会被用作属性的值
set
属性的setter函数,默认为undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的this对象
# 实现方案
遍历数据data的所有属性,通过Object.defineProperty()拦截并改写(自定义)数据的属性的getter & setter函数,从而在访问对象属性和设置/修改对象属性的时候能够执行自定义的回调函数:在getter中进行依赖收集操作(track,访问过该属性的节点、组件、函数...都会被收集为依赖watcher),在setter中进行视图更新操作(tigger,通知前面收集到的依赖触发执行&视图重新渲染)
# 简单实现
// 重写数组的原型方法
let oldArrayPrototype = Array.prototype;
let proto = Object.create(oldArrayPrototype);
['push', 'pop', 'unshift', 'shift', 'sort', 'reverse', 'splice'].forEach(method => {
proto[method] = function() {
updateView();
oldArrayPrototype[method].call(this, ...arguments);
}
})
// 监听数据变化
function observer(target) {
if(typeof target !== 'object' || target === null) {
// 不是对象,无法更改属性值,直接返回
return target;
}
// 数组,重写原型方法
if(Array.isArray(target)) {
target.__proto__ = proto;
}
// 循环对象,重新定义属性的getter & setter
for(let key in target) {
defineReactive(target, key, target[key]);
}
}
// 定义响应式
function defineReactive(obj, key, val) {
observer(val);
Object.defineProperty(obj, key,{
get() {
// 在这里进行依赖收集
return val;
},
set(newValue) {
if(newVal !== val) {
observer(newVal);
// 在这里进行依赖触发
updateView();
val = newVal;
}
}
})
}
function updateView() {
console.log('视图更新')
}
以上省略了依赖收集 watcher 的具体实现,可参考 vue原理依赖收集–watcher (opens new window)
# 缺陷
影响性能(如增加首次渲染时间)、增加内存消耗,尤其数据层级很深时
- 原因:默认会进行递归
无法监听数组改变length,vue在检测数组的变化时需要重写push,pop,unshift,shift,reverse,sort,splice这7个能改变原数组的原型方法
- 原因:数组的length属性具有以下初始化键值length 属性初始为 non-configurable,无法删除 / 修改 length 属性,无法改写 length 属性的 getter & setter 函数,因此,通过改变 length 而变化的数组长度不能被 Object.defineProperty() 监测到。而 push, pop, unshift, shift, reverse, sort, splice 这几个内置的方法在操作数组时,都会改变原数组 length 的值,而 Object.defineProperty() 不能监测到数组长度的变化,因而不会触发视图更新。
// 表示对象的属性是否可以被枚举,如能否通过for-in循环返回该属性 enumberable: false // 表示对象的属性是否可以被删除,以及除value和writable特性(键值)外的其他特性(如get、set)是否可以被修改 configurable: false; // 表示对象的属性值是否可以被改变 writable: false
- 原因:数组的length属性具有以下初始化键值
对象上新增的属性不能被拦截
- 原因:Object.defineProperty()需要指定对象具体的属性名才能对其getter和setter进行拦截
- 补丁:Vue2提供了一个api:this.$set,使新增的属性也拥有响应式的效果。但是需要判断到底什么情况下需要使用$set,什么时候可以直接触发响应式
# Vue3的实现
# Proxy
Proxy是一个包含另一个对象或函数并允许你对其进行拦截的对象。
ES6的Proxy对象用于创建一个对象的代理,从而实现对其基本操作的拦截 & 自定义。
- 语法-- const p = new Proxy(target, handler);
- 参数
- target:要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
- handler:一个通常以函数作为属性的对象,它包含有Proxy的各个捕获器(trap),定义了在执行各种操作时代理p的行为。所有捕获器是可选的。如果没有定义某个捕获器,那么就会保留源对象的默认行为。Vue3用到的traps
- handler.get():属性读取操作
- handler.set():属性设置操作
- handler.deleteProperty():属性delete操作
- handler.has():属性in操作符
- handler.ownKeys(): Object.getOwnPropertyNames方法Object.getOwnPropertySymbols方法的捕获器
# Reflect
Reflect对象与Proxy对象一样,也是ES6为了操作对象而提供的新API。相比Object对象对象主要有如下特点/优势
- 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同事在Object和Reflect对象上部署,未来新的方法只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法
- 返回结果合理,不会报错,操作失败只会返回false。比如Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误;二Reflect.defineProperty(obj, name, desc)则会返回false
- 方法都是函数式的。Object存在某些命令操作,如name in obj和delete obj[name],则对应的Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)都是函数式操作
- Reflect对象的方法与Proxy对象的方一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。因此Proxy对象可以方便调用对应Reflect方法,完成默认行为,作为修改行为的基础
# WeakMap
WeakMap-Javascript|MDN (opens new window)
WeakMap对象是一组键/值对的集合,其中的键是弱引用。其键必须是对象,而只可以是任意的。原生的WeakMap持有的是每个键对象的『弱引用』,这意味着在没有其他应用存在时垃圾回收能正确进行。原生WeakMap的结构是特殊且有效的,其用于映射的key只有在其没有被回收才是有效的。
WeakMap键名所指向的对象,不计入垃圾回收机制,有助于防止内存泄露。所以WeakMap可以事先往对象上添加数据,又不会干扰垃圾回收机制
# 实现方案
用Proxy代理数据,创建响应式对象,拦截其getter和setter函数;依赖该数据/属性的方法(被称为副作用effect)默认先执行一次。触发所依赖属性的 get 方法,在 getter 函数中进行依赖收集(track,把当前属性与当前的 effect 建立联系,即映射表);当属性变化时,会触发其 set 方法,在 setter 函数中进行更新(trigger,依次触发映射表中依赖当前属性的 effect)。
# 关键方法
reactive
把数据变为响应式,遍历 & 自定义对象所有属性的getter & setter函数,返回proxy对象
effect
- effect方法本质是一个高级函数(如何或出参是函数).默认会立即执行传入的函数(此时会触发内部函数响应式对象的get方法,从而触发依赖收集),在依赖的数据变化时会在执行
- 含义:副作用(数据变化会触发响应的回调),相当于Vue2中watcher
# 具体实现
reactive
// 判断是否是对象 function isObject(val) { return typeof val === 'object' && val !== null; } // 1. 响应式的核心方法 function reactive(target) { // 创建响应式对象 return createReactiveObject(target); } let toProxy = new WeakMap(); // 弱引用映射表,es6;放的是『原对象:代理后的对象』 // 防止被代理过的对象再次被代理 let toRaw = new WeakMap();// “代理后的对象:原对象” // 判断当前对象有无某属性 function hasOwn(target, key) { return target.hasOwnProperty(key); } // 创建响应式对象 function createReactiveObject(target) { if(!isObject(target)) return target; let proxy = toProxy.get(target); if(proxy) { // 如果target已经有响应的代理后的对象,直接返回之前代理过的结果即可 return proxy; } if(toRaw.has(target)) { // 判断target是否已经是reactive对象 // target 已经是代理后的对象了,则无需再次代理 return target; } const baseHandler = { // reflect有点:不会报错&会有返回值;以后替代Object get(target, key, receiver) { //target:原对象,key:属性,receiver:当前代理对象proxy(target被代理后的对象) console.log('获取'); let res = Reflect.get(target, key, receiver); // res 是当前获取到的值 return isObject ? reactive(res) : res; // 按需实现递归 }, set(target, key, value, receiver) { // 识别是 修改属性or 新增属性 let hadKey = hasOwn(target, key); // 判断这个属性以前有没有 let oldValue = target[key]; let res = Reflect.set(target, key, value, receiver); if(!hadKey) { console.log('新增属性'); console.log('设置') } else if(value !== oldValue) { // 屏蔽无意义的修改(即修改前后值相同) console.log('修改属性') console.log('设置') } return res; }, deleteProperty(target, key) { console.log('删除'); let res = Reflect.deleteProperty(target, key); return res; } } // 创建观察者 let observer = new Proxy(target, baseHandler); toProxy.set(target, observer); toRaw.set(observer, target); return observer }effect
// reactive 中函数 createReactiveObject 的 baseHandler 修改如下 const baseHandler = { get(target, key, receiver) { let res = Reflect.get(target, key, receiver); // 收集依赖(把属性 & 对应的 effect 建立联系),即 订阅【把当前的 key 与 effect 对应起来】 track(target, key); // 如果目标上的 key 变化了,重新让数组中的 effect 执行即可 return isObject(res) ? reactive(res) : res; // 按需实现递归 }, set(target, key, value, receiver) { let hadKey = hasOwn(target, key); let oldValue = target[key]; let res = Reflect.set(target, key, value, receiver); if (!hadKey) { trigger(target, 'add', key); } else if (value !== oldValue) { trigger(target, 'edit', key); } return res; }, deleteProperty(target, key) { console.log('删除'); let res = Reflect.deleteProperty(target, key); return res; }, };// 2.依赖收集(发布订阅) // 取值会触发 get,get 触发 track(track 里存映射表,最外层是个 WeakMap);设置值时触发 set,set 触发 trigger,取出 effect 执行,更新视图 // 栈:先进后出 let activeEffectStacks = []; // 保存 reactiveEffect // 依赖的数据结构应该如下 // { // target: { // key: [fn, fn, fn,...] // 一个属性可能对应多个副作用(即有多个 effect 都依赖这个属性)【应去重,所以用 Set 数据结构】 // } // } let targetSMap = new WeakMap(); // 集合 和 hash 表 function track(target, key) { //若这个 target 中的 key 变化了,就执行栈中的方法 let effect = activeEffectStacks[activeEffectStacks.length - 1]; if (effect) { // 有对应关系,才创建关联【以下为动态创建依赖关系】 let depsMap = targetSMap.get(target); if (!depsMap) { // 首次没有,设置一个并设默认值 targetSMap.set(target, (depsMap = new Map())); } // 取对象的 key 对应的副作用数组 let deps = depsMap.get(key); if (!deps) { depsMap.set(key, (deps = new Set())); } if (!deps.has(effect)) { deps.add(effect); } } } function trigger(target, type, key) { let depsMap = targetSMap.get(target); if (depsMap) { // 有才需要触发 let deps = depsMap.get(key); if (deps) { // 将当前 key 对应的 effect 依次执行 deps.forEach((effect) => effect()); } } } // 响应式——副作用 function effect(fn) { // 需要把 fn 这个函数 变成 响应式的函数 let reactiveEffect = createReactiveEffect(fn); // 副作用 默认会先执行一次 reactiveEffect(); } function createReactiveEffect(fn) { let reactiveEffect = function () { // 创建的响应式的 effect return run(reactiveEffect, fn); // 2个目的:1、执行 fn;2、把这个 reactiveEffect 存到栈中 }; return reactiveEffect; } // 运行 fn & 把 effect 存起来 function run(effect, fn) { try { activeEffectStacks.push(effect); fn(); // 和 vue2 一样,利用 js 的单线程 } finally { // 即使前面报错,这里也会执行 activeEffectStacks.pop(); } }ref
- ref中可以类似_isRef来判断是否为ref类型
- reactive中get函数需要判断res是否为ref对象,若是直接返回value
// 如果传入ref的是一个对象,将调用reactive方法进行深层响应转换 const convert = raw => isObject(raw) ? reactive(raw) : raw function ref(raw) { raw = convert(raw); const v = { _isRef: true, get value() { track(v, ''); return raw; }, set value(newValue) { raw = convert(newValue); trigger(v, ''); } } return v; }computed
- 返回一个ref对象
- 原始值value应该放在闭包内,使用dirty字段决定是否被缓存
- 依赖触发trigger时,不会立即执行effect,而是执行effect options中的scheduler
function effect(fn, options = {}) { const effect = createReactiveEffect(fn, options); if(!options.lazy) { effect(); } return effect; } function createReactiveEffect(fn, options){ const effect = function() { return run(effect, fn); } effect.scheduler = options.scheduler; return effect; } function computed(getterOrOptions) { const getter = isFunction(getterOrOptions) ? getterOrOptions : getterOrOptions.get; const setter = isFunction(getterOrOptions) ? () => {} : getterOrOptions.set; let value; let dirty = true; let v; const runner = effect(getter, { lazy: true, scheduler: () => { dirty = true; trigger(v, ''); }, }); v = { _isRef: true, get value() { if (dirty) { value = runner(); dirty = false; } track(v, ''); return value; }, set value(newValue) { setter(newValue); }, }; return v; }
# 应用
| API | 特性 | 使用场景 |
|---|---|---|
| reactive | - 接受一个普通对象然后返回该普通对象的响应式代理 - 响应式转换是"深层的":会影响对象内部所有嵌套的属性 返回的代理对象不等于原始对象。建议仅使用代理对象而避免依赖原始对象 | 只能用于代理非基本数据类型Object |
| toRefs | 可以将一个响应式对象(reactive Object)转换为普通对象,同时又把该对象中的每一个属性转换为对应的响应时属性(ref) | 保留被解构的响应式对象(reactive object)的响应式特性(reactivity)[响应式对象被解构后悔丢失响应式],e.g:...toRefs(data) |
| ref | --- | - 一般用于给JS基本数据类型添加响应式(也支持非基本类型的object) - 基本类型有7个,只能使用ref:String, Number, BigInt,Boolean,Symbol,Null, Undefined |
| watch | --- | --- |
| computed | --- | --- |
# 优点
- 性能(如首次渲染时间)、内存小号等方面都优于vue2
- 可以监听数组改变length
- 对象上原有属性 & 新增属性都可以拦截
# 缺点
兼容性 IE11及以下版本不兼容ES6的Proxy
# 资料
vue3手写 isRef、isReactive、isReadonly、isProxy的判断 (opens new window)