Vue3.0——reactive和ref原理
Vue3采用了新的Proxy
实现数据拦截,但Proxy
还是只能代理一层。对于深层的对象需要递归代理,相比2.0在初始化递归,3.0的运行时递归,大大减少了初始化的递归性能消耗。
Proxy
首先简单了解下Proxy
,方便后面学习reactive
和ref
原理。
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
基础示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| const handler = { get: function (target, prop) { console.log('getting key: ' + prop); return target[prop]; }, set: function (target, prop, value) { console.log('setting key: ' + prop + ', value: ' + value); target[prop] = value; return true; } };
const p = new Proxy({}, handler); p.a = 1; p.b = undefined;
console.log(p.a, p.b);
console.log('c' in p, p.c);
|
Proxy
一起的还有另外一位小伙伴Reflect
。
Reflect
对象的方法与Proxy
对象的方法一一对应,只要是Proxy
对象的方法,就能在Reflect
对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect
方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy
怎么修改默认行为,你总可以在Reflect
上获取默认行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const handler = { get: function (target, prop, receiver) { console.log('getting key: ' + prop); return Reflect.get(target, prop, receiver); }, set: function (target, prop, value, receiver) { console.log('setting key: ' + prop + ', value: ' + value); return Reflect.set(target, prop, value, receiver); } };
|
Proxy API具体用法详见官方文档。MDN文档传送门
为什么要选用Proxy
Vue3.0 为什么选用Proxy
而不是沿用 Object.defineProperty
呢?
Proxy 的优势如下:
- Proxy 可以直接监听对象而非属性;
- Proxy 可以直接监听数组的变化;
- Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
- Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
- Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
Object.defineProperty 的优势如下:
- 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。
对于监听数组变化我们可以来做一个对比
Proxy代理数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| let arr = [1, 1, 2, 3]; let handler = { get (target, prop, receiver) { console.log('getting key: ' + prop); return Reflect.get(target, prop, receiver); }, set (target, prop, value, receiver) { console.log('setting key: ' + prop + ', value: ' + value); return Reflect.set(target, prop, value, receiver); } } let proxyArray = new Proxy(arr, handler);
proxyArray.push(4);
proxyArray[0] = 0;
|
**Object.defineProperty **代理数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| const orginalProto = Array.prototype; const arrayProto = Object.create(orginalProto); const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; methodsToPatch.forEach(method => { arrayProto[method] = function () { orginalProto[method].apply(this, arguments); console.log('数组改变:', method , ...arguments); } }) let arr = [1, 1, 2, 3]; arr.__proto__ = arrayProto; arr.push(4); arr[0] = 0; console.log(arr);
|
上面两种实现数组代理方式对比,实现上Proxy
来的更便捷,且能监听到通过数组下标改变数组值的形式。而Object.defineProperty
则是通过重写原型上的方式来监听数组的变化,而且监听不到通过数组下标改变数组值的形式。
reactive 原理
一些工具函数,后面会一一用到。
1 2 3 4 5 6
| const isObject = val => val !== null && typeof val === 'object';
const hasChanged = (value, oldValue) => value !== oldValue && (value === value && oldValue === oldValue);
const hasOwn = (val, key) => Object.hasOwnProperty.call(val, key);
|
首先我们需要一个reactive
方法用来创建响应式数据
1 2 3 4
| function reactive(target) { return createReactiveObject(target, baseHandlers); }
|
createReactiveObject
方法创建一个Proxy实例
1 2 3 4 5 6 7 8
| const baseHandlers = { get, set }
function createReactiveObject(target, baseHandlers) { return new Proxy(target, baseHandlers); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| ```javascript // 生成get function createGetter() { return function get(target, key, recevier) { const result = Reflect.get(target, key, recevier); console.log(`getting key: ${key}`);
// 深层代理 if (isObject(result)) { return reactive(result); } return result } } // 生成set function createSetter() { return function set(target, key, value, recevier) { const oldValue = target[key]; const hadKey = hasOwn(target, key); const result = Reflect.set(target, key, value, recevier);
if (!hadKey) { console.log(`add key:${key},value:${value}`); // trigger(target, 'add' /* ADD */, key, value); } else if (hasChanged(value, oldValue)) { console.log(`set key:${key},value:${value}`); // trigger(target, 'set' /* SET */, key, value, oldValue); }
return result } }
const get = createGetter(); const set = createSetter();
|
测试reactive
1 2 3 4 5 6 7 8 9 10 11 12 13
| const proxyObj = reactive({ id: 1, name: 'tab', childObj: { age: 24 } })
proxyObj.name = 'xxTab';
proxyObj.childObj.age = 2;
|
总结reactive`原理:
reactive
本质上就是一个Proxy
实例,无非就是在set
和get
拦截器上加了一些自身的逻辑。比如get
拦截器收集依赖,set
拦截器触发相关依赖。
ref 原理
还是一样需要一个暴露出来的ref
方法.
1 2 3 4 5 6 7
| function createRef(rawValue, shallow = false) { return new RefImpl(rawValue, shallow) }
function ref(value) { return createRef(value) }
|
RefImpl
核心类,实现ref的初始化,以及相应的get和set
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class RefImpl { constructor(_rawValue, _shallow = false) { this._rawValue = _rawValue; this._shallow = _shallow; this.__v_isRef = true; this._value = _shallow ? _rawValue : convert(_rawValue); } get value() { return this._value; } set value(newVal) { if (hasChanged(toRaw(newVal), this._rawValue)) { this._rawValue = newVal; this._value = this._shallow ? newVal : convert(newVal); } } }
|
工具方法
1 2 3 4 5 6
| const convert = val => (isObject(val) ? reactive(val) : val);
function toRaw(observed) { return (observed && toRaw(observed['__v_raw'])) || observed }
|
测试ref
1 2 3 4 5 6
| let num = ref(1) num.value = 10;
let data = ref([1, 2, 3]) data.value.push(4) console.log(data.value[3])
|
总结ref
原理:
核心类 RefImpl
,我们可以看到在类中使用了经典的 get/set
存取器,来进行追踪和触发。
convert
方法让我们知道了 ref 不仅仅用来包装一个值类型,也可以是一个对象/数组,然后把对象/数组再交给 reactive
进行代理。
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
| const isObject = val => val !== null && typeof val === 'object';
const hasChanged = (value, oldValue) => value !== oldValue && (value === value && oldValue === oldValue);
const hasOwn = (val, key) => Object.hasOwnProperty.call(val, key);
function createGetter () { return function get (target, key, recevier) { const result = Reflect.get(target, key, recevier); console.log(`getting key: ${key}`);
if (isObject(result)) { return reactive(result); } return result } }
function createSetter () { return function set (target, key, value, recevier) { const oldValue = target[key]; const hadKey = hasOwn(target, key); const result = Reflect.set(target, key, value, recevier);
if (!hadKey) { console.log(`add key:${key},value:${value}`); } else if (hasChanged(value, oldValue)) { console.log(`set key:${key},value:${value}`); }
return result } }
const get = createGetter(); const set = createSetter();
const baseHandlers = { get, set }
function createReactiveObject (target, baseHandlers) { return new Proxy(target, baseHandlers); }
function reactive (target) { return createReactiveObject(target, baseHandlers); }
const proxyObj = reactive({ id: 1, name: 'tab', childObj: { age: 24 } })
proxyObj.name = 'xxTab'; proxyObj.childObj.age = 2;
const convert = val => (isObject(val) ? reactive(val) : val);
function toRaw (observed) { return (observed && toRaw(observed['__v_raw'])) || observed }
class RefImpl { constructor(_rawValue, _shallow = false) { this._rawValue = _rawValue; this._shallow = _shallow; this.__v_isRef = true; this._value = _shallow ? _rawValue : convert(_rawValue); } get value () { return this._value; } set value (newVal) { if (hasChanged(toRaw(newVal), this._rawValue)) { this._rawValue = newVal; this._value = this._shallow ? newVal : convert(newVal); } } }
function createRef (rawValue, shallow = false) { return new RefImpl(rawValue, shallow) }
function ref (value) { return createRef(value) }
function shallowRef (value) { return createRef(value, true) }
function isRef (r) { return Boolean(r && r.__v_isRef === true); }
function unRef (ref) { return isRef(ref) ? ref.value : ref; }
|