반응성이란 무엇인가요? Vue는 어떻게 반응성을 달성하나요? 다음 기사는 Vue3의 반응성 원칙에 대한 심층적인 이해를 제공할 것입니다. 도움이 되기를 바랍니다.
Vue에 관해서라면 면접관이 가장 먼저 묻는 질문 중 하나는 Vue의 반응성 원칙이 어떻게 구현되는지에 대한 것입니다. 이전에 Vue2의 반응성에 대해 알아보고 오늘은 Vue3의 반응성 메커니즘에 대해 이야기해 보겠습니다. [관련 추천 : vuejs 동영상 튜토리얼, 웹 프론트엔드 개발]
JavaScript의 변수에는 응답성이라는 개념이 없습니다. 코드의 실행 논리는 하향식입니다. Vue 프레임워크에서 응답성은 주요 기능 중 하나입니다. 먼저 예제를 살펴보겠습니다
let num = 1; let double = num * 2; console.log(double); // 2 num = 2; console.log(double); // 2
double 변수와 num 변수의 관계가 반응하지 않는다는 것을 명확하게 알 수 있습니다. double을 계산하는 논리를 함수로 요약하면 변수 num의 값이 변경될 때, 이 함수를 다시 실행하면 double의 값이 num의 변화에 따라 변경됩니다. 이를 일반적으로 반응형이라고 부릅니다.
let num = 1; // 将计算过程封装成一个函数 let getDouble = (n) => n * 2; let double = getDouble(num); console.log(double); // 2 num = 2; // 重新计算double,这里当然也没有实现响应式,只是说明响应式实现的时候这个函数应该再执行一次 double = getDouble(num); console.log(double); // 4
실제 개발 과정은 현재의 간단한 상황보다 훨씬 더 복잡하겠지만, 이를 함수로 캡슐화하여 구현할 수 있습니다. 이제 문제는 num 변수의 변경에 따라 double의 값을 어떻게 다시 계산할 수 있느냐 하는 것입니다. ?
num 변수의 값이 매번 수정되면 getDouble 함수가 이를 알고 실행할 수 있으며, num 변수의 변경에 따라 double도 그에 따라 변경됩니다.
Vue에는 defineProperty, Proxy 및 value setter라는 세 가지 반응형 솔루션이 사용되었습니다. defineProperty API는 이전 기사에서 자세히 설명했습니다. Vue2 응답성에 대해 자세히 알고 싶다면 여기를 클릭하세요. --->vue 응답성 원칙
Vue2의 핵심 부분은 객체 obj를 정의할 때 num 속성을 읽을 때 get 함수가 실행되고 num 속성이 다음과 같을 때 사용합니다. 수정되면 set 함수가 실행됩니다. set 함수에 double을 계산하는 논리만 작성하면 num이 변경될 때마다 그에 따라 double이 할당되며 이는 응답합니다.
let num = 1; let detDouble = (n) => n * 2; let obj = {} let double = getDouble(num) Object.defineProperty(obj,'num',{ get() { return num; } set(val){ num = val; double = getDouble(val) } }) console.log(double); // 2 obj.num = 2; console.log(double); // 4
defineProperty 결함: obj.num 속성을 삭제하면 set 함수가 실행되지 않으므로 Vue2에서는 데이터를 삭제하는 $delete
특수 함수가 필요합니다. 그리고 obj 객체에 존재하지 않는 속성은 하이재킹할 수 없으며 배열의 길이 속성을 수정하는 것도 유효하지 않습니다.
Proxy라는 이름만으로도 Proxy라는 뜻을 알 수 있는데, Proxy의 중요한 의미는 Vue2 응답성의 단점을 해결한다는 점입니다.
프록시 사용법:
var proxy = new Proxy(target, handler);
프록시 객체의 모든 사용법은 위 형식이며, 유일한 차이점은 handler 매개변수를 작성하는 방법입니다. 그 중 new Proxy()는 Proxy 인스턴스를 생성한다는 의미이고, target 매개변수는 차단할 대상 개체를 나타내며, handler 매개변수도 차단 동작을 맞춤설정하는 데 사용되는 개체입니다.
var proxy = new Proxy({}, { get: function(target, propKey) { return 35; } }); proxy.time // 35 proxy.name // 35 proxy.title // 35
프록시에서 13가지 유형의 맞춤형 차단 지원
proxy.foo
和proxy['foo']
。proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。propKey in proxy
的操作,返回一个布尔值。delete proxy[propKey]
的操作,返回一个布尔值。Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。Object.preventExtensions(proxy)
,返回一个布尔值。Object.getPrototypeOf(proxy)
,返回一个对象。Object.isExtensible(proxy)
,返回一个布尔值。Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。new proxy(...args)
。在ES6中官方新定义了 Reflect 对象,在ES6之前对象上的所有的方法都是直接挂载在对象这个构造函数的原型身上,而未来对象可能还会有很多方法,如果全部挂载在原型上会显得比较臃肿,而 Reflect 对象就是为了分担 Object的压力。
(1) 将Object
对象的一
些明显属于语言内部的方法(比如Object.defineProperty
),放到Reflect
对象上现阶段,某些方法同时在Object
和Reflect
对象上部署,未来的新方法将只部署在Reflect
对象上。也就是说,从Reflect
对象上可以拿到语言内部的方法。
(2) 修改某些Object
方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)
则会返回false
。
// 老写法 try { Object.defineProperty(target, property, attributes); // success } catch (e) { // failure } // 新写法 if (Reflect.defineProperty(target, property, attributes)) { // success } else { // failure }
(3) 让Object
操作都变成函数行为。某些Object
操作是命令式,比如name in obj
和delete obj[name]
,而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为。
// 老写法 'assign' in Object // true // 新写法 Reflect.has(Object, 'assign') // true
(4)Reflect
对象的方法与Proxy
对象的方法一一对应,只要是Proxy
对象的方法,就能在Reflect
对象上找到对应的方法。这就让Proxy
对象可以方便地调用对应的Reflect
方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy
怎么修改默认行为,你总可以在Reflect
上获取默认行为。
Proxy(target, { set: function(target, name, value, receiver) { var success = Reflect.set(target, name, value, receiver); if (success) { console.log('property ' + name + ' on ' + target + ' set to ' + value); } return success; } });
所以我们在这里会使用到 Proxy 和 Reflect 对象的方与 Proxy 一一对应这一特性,来实现Vue3的响应式原理。
在Vue3中响应式的核心方法是
function reactive (target){ // 返回一个响应式对象 return createReactiveObject(target); }
根据我们前面所做的铺垫,所以我们会使用 Proxy
代理我们所需要的相应的对象,同时使用 Reflect
对象来映射。所以我们先初步实现一下,再慢慢优化,尽可能全面。
判断是否为对象(方法不唯一,有多种方法)
function isObject(val){ return typeof val === 'object' && val !== null }
尽可能采用函数式编程,让每一个函数只做一件事,逻辑更加清晰。
function createReactiveObject (target) { // 首先由于Proxy所代理的是对象,所以我们需要判断target,若是原始值直接返回 if(!isObject(target)) { return target; } let handler = { get(target, key, receiver) { let res = Reflect.get(target, key, receiver); // 使用Reflect对象做映射,不修改原对象 console.log('获取'); return res; }, set(target, key, value, receiver) { let res = Reflect.set(target, key, value, receiver); console.log('修改'); return res }, deleteProperty(target, key) { let res = Reflect.deleteProperty(target, key) console.log('删除'); return res; } } let ProxyObj = new Proxy(target,handler); // 被代理过的对象 return ProxyObj; }
但是这样会有一个问题,如果我需要代理的对象是深层嵌套的对象呢?我们先看看效果
当我们深层代理时,我们直接修改深层对象中的属性并不会触发 Proxy 对象中的 set 方法,那为什么我们可以修改呢?其实就是直接访问原对象中深层对象的值并修改了,那我们如何优化这个问题呢?
那也需要用到递归操作,判断深层对象是否被代理了,如果没有再执行reactive将内部未被代理的对象代理。
那么我们在 get 方法内部就不能直接将映射之后的 res 返回出去了
get(target, key, receiver) { let res = Reflect.get(target, key, receiver); // 使用Reflect对象做映射,不修改原对象 console.log('获取'); // 判断代理之后的对象是否内部含有对象,如果有的话就递归一次 return isObject(res) ? reactive(res) : res; }
这样我们就实现了对象的深层代理,并且只有当我们访问到内部嵌套的对象时我们才 会去递归调用reactive ,这样不仅可以实现深层代理,并且节约了性能,但是其实我们还没有彻底完善,我们来看看下面这段代码
let proxy = reactive({name: '寒月十九', message: { like: 'coding' }}); reactive(proxy); reactive(proxy); reactive(proxy);
这样是不是合法的,当然是合法的,但是没有必要也没有意义,所以为了避免被代理过的对象,再次被代理,太浪费性能,所以我们需要将被代理的对象打上标记,这样当带被代理过的对象访问到时,直接将被代理过的对象返回,不需要再次代理。
在 Vue3 中,使用了hash表做映射,来记录是否已经被代理了。
// WeakMap-弱引用对象,一旦弱引用对象未被使用,会被垃圾回收机制回收 let toProxy = new WeakMap(); // 存放形式 { 原对象(key): 代理过的对象(value)} let toRow = new WeakMap(); // 存放形式 { 代理过的对象(key): 原对象(value)}
let ProxyObj = new Proxy(target,handler); // 被代理过的对象 toProxy.set(target,ProxyObj); toRow.set(ProxyObj.target); return ProxyObj;
let ByProxy = toProxy.get(target); // 防止多次代理 if(ByProxy) { // 如果在WeakMap中可以取到值,则说明已经被代理过了,直接返回被代理过的对象 return ByProxy; } // 防止多层代理 if(toRow.get(target)) { return target } // 为了防止下面这种写法(多层代理) // let proxy2 = reactive(proxy); // let proxy3 = reactive(proxy2); // 其实本质上与下面这种写法没有区别(多次代理) // reactive(proxy); // reactive(proxy); // reactive(proxy);
let arr = [1 ,2 ,3 ,4]; let proxy = reactive(arr); proxy.push(5); // 在set内部其实会干两件事,首先会将5这个值添加到数组下标4的地方,并且会修改length的值
与 Vue2 的数据劫持相比,Vue3 中的 Proxy 可以直接修改数组的长度,但是这样我们需要在 set 方法中判断我们是要在代理对象身上添加属性还是修改属性。
因为更新视图的函数会在set函数中调用,我们向数组中进行操作会触发两次更新视图,所以我们需要做一些优化。
// 判断属性是否原本存在 function hasOwn(target,key) { return target.hasOwnProperty(key); } set(target, key, value, receiver) { let res = Reflect.set(target, key, value, receiver); // 判断是新增属性还是修改属性 let hadKey = hasOwn(target,key); let oldValue = target[key]; if(!hadKey) { // 新增属性 console.log('新增属性'); }else if(oldValue !== value){ console.log('修改属性'); } return res },
避免多次更新视图,比如修改的值与原来一致就不更新视图,在上面两个判断条件中添加更新视图的函数,就不会多次更新视图。
function isObject(val) { return typeof val === 'object' && val !== null } function reactive(target) { // 返回一个响应式对象 return createReactiveObject(target); } // 判断属性是否原本存在 function hasOwn(target, key) { return target.hasOwnProperty(key); } // WeakMap-弱引用对象,一旦弱引用对象未被使用,会被垃圾回收机制回收 let toProxy = new WeakMap(); // 存放形式 { 原对象(key): 代理过的对象(value)} let toRow = new WeakMap(); // 存放形式 { 代理过的对象(key): 原对象(value)} function createReactiveObject(target) { // 首先由于Proxy所代理的是对象,所以我们需要判断target,若是原始值直接返回 if (!isObject(target)) { return target; } let ByProxy = toProxy.get(target); // 防止多次代理 if (ByProxy) { // 如果在WeakMap中可以取到值,则说明已经被代理过了,直接返回被代理过的对象 return ByProxy; } // 防止多层代理 if (toRow.get(target)) { return target } let handler = { get(target, key, receiver) { let res = Reflect.get(target, key, receiver); // 使用Reflect对象做映射,不修改原对象 console.log('获取'); return isObject(res) ? reactive(res) : res; }, set(target, key, value, receiver) { let res = Reflect.set(target, key, value, receiver); // 判断是新增属性还是修改属性 let hadKey = hasOwn(target, key); let oldValue = target[key]; if (!hadKey) { // 新增属性 console.log('新增属性'); } else if (oldValue !== value) { console.log('修改属性'); } return res }, deleteProperty(target, key) { let res = Reflect.deleteProperty(target, key) console.log('删除'); return res; } } let ProxyObj = new Proxy(target, handler); // 被代理过的对象 return ProxyObj; } // let proxy = reactive({name: '寒月十九'}); // proxy.name = '十九'; // console.log(proxy.name); // delete proxy.name; // console.log(proxy.name); // let proxy = reactive({name: '寒月十九', message: { like: 'coding' }}); // proxy.message.like = 'writing'; // console.log('===================================='); // console.log(proxy.message.like); // console.log('===================================='); let arr = [1, 2, 3, 4]; let proxy = reactive(arr); proxy.push(5)
在IE11以下的浏览器都不兼容,所以如果使用 Vue3 开发一个单页应用的项目,需要考虑到兼容性问题,需要我们做额外的很多操作,才能使得IE11 以下的版本能够兼容。
위 내용은 Vue3의 반응 메커니즘에 대한 심층 탐구의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!