赞
踩
vue2中的响应式核心是es5的Object.defineProperty
,缺点有:
所以vue3采用了es6之后的proxy去重构了响应式原理,proxy能够很好的解决Object.defineProperty
的缺点。
defineProperty是对象的基本操作之一,而proxy是对整个对象所有基本操作的拦截器。也就是proxy包含了defineProperty等等基本操作,而且在调用基本操作的时候,都会被proxy拦截到。
defineProperty里的set和get只是属性描述符。
而proxy里的set和get是真正的基本操作。
换句话来说,defineProperty监听的是属性,而proxy监听的是整个对象。
这里简单记录一下proxy的基本使用,想学习具体的使用细节还需自行查阅资料(下面的Reflect函数如果不理解没关系,不影响我们认识proxy的作用)。
const data = ["a", "b", "c"]; const proxyData = new Proxy(data, { // data对象被Proxy处理后就成了一个Proxy对象 get(target, key, receiver) { console.log("Reflect get", Reflect.ownKeys(target)); console.log("get", key); // 监听 const result = Reflect.get(target, key, receiver); return result; // 返回执行的方法 }, set(target, key, val, receiver) { const result = Reflect.set(target, key, val, receiver); console.log("set", key, val); return result; // 是否设置成功,为一个布尔值 }, deleteProperty(target, key) { const result = Reflect.deleteProperty(target, key); console.log("delete property", key); return result; // 是否删除成功,为一个布尔值 }, });
咱们是将原变量做了层代理,以后的操作都只针对代理出来的对象,例如,当我们执行proxyData.push('e')
时,就会打印出:
Reflect get (4) ['0', '1', '2', 'length']
get push
Reflect get (4) ['0', '1', '2', 'length']
get length
set 3 e
set length 4
可以发现proxy触发了一些我们不需要的操作,get中不需要读取push,set不需要重新设置length(因为已经自动改变了),所以可以这样改写:
const proxyData = new Proxy(data, { get(target, key, receiver) { // 只处理本身(非原型的)属性 const ownKeys = Reflect.ownKeys(target); if (ownKeys.includes(key)) { console.log("get", key); // 监听 } const result = Reflect.get(target, key, receiver); return result; // 返回结果 }, set(target, key, val, receiver) { // 重复的数据,不处理 if (val === target[key]) { return true; } const result = Reflect.set(target, key, val, receiver); console.log("set", key, val); return result; }, deleteProperty(target, key) { const result = Reflect.deleteProperty(target, key); console.log("delete property", key); return result; }, });
这样再次执行就是:
get length
set 3 e
注意:proxy要触发get才能被处理成proxy对象
对于Reflect函数,具体的还需自行查看,不影响理解响应式原理。这里还可以了解一下Reflect的作用:
'a' in obj
、delete obj.a
通过Proxy代理的形式,能够完美的监听到对象和数组,并且采用get触发深度监听的方式大大减少性能上的损耗。
唯一的缺点就是不能兼容所有的浏览器,也无法polyfill。
咱们写一个简单的响应式原理,能够更好的加深印象:
// 创建响应式 function reactive(target = {}) { if (typeof target !== "object" || target == null) { // 不是对象或数组,则返回 return target; } // proxy的代理配置,单独拿出来写 const proxyConf = { get(target, key, receiver) { // 只处理本身(非原型的)属性 const ownKeys = Reflect.ownKeys(target); if (ownKeys.includes(key)) { console.log("get", key); // 监听 } const result = Reflect.get(target, key, receiver); // 深度监听,因为是触发了get,才会进行递归处理,所以性能会更好些 return reactive(result); }, set(target, key, val, receiver) { // 重复的数据,不处理 if (val === target[key]) { return true; } const ownKeys = Reflect.ownKeys(target); if (ownKeys.includes(key)) { console.log("已有的 key", key); } else { console.log("新增的 key", key); } const result = Reflect.set(target, key, val, receiver); console.log("set", key, val); return result; // 是否设置成功 }, deleteProperty(target, key) { const result = Reflect.deleteProperty(target, key); console.log("delete property", key); return result; // 是否删除成功 }, }; // 生成代理对象 const observed = new Proxy(target, proxyConf); return observed; } // 测试数据 const data = { name: "xiaoming", age: { young: 18, old: 26, }, }; const proxyData = reactive(data);
加入视图更新逻辑,首先写好第一步操作:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <ul> <li id="name"></li> <li id="age"></li> </ul> <script src="./vue.js"> </script> <script> let dataObj = { name: '小米', age: 1 } // 监听数据 let proxyObj = reactive(dataObj); function renderName() { document.querySelector('#name').textContent = proxyObj.name } function renderAge() { document.querySelector('#age').textContent = proxyObj.age } // 自动第一次获取数据更新视图 addWatcher(renderName) addWatcher(renderAge) </script> </body> </html>
然后改造下原来的核心逻辑:
// 触发更新视图(新增代码) let watchers = new Set(); // 要触发更新具体位置的函数集合 function updateView() { for (let watcher of watchers) watcher(); // 遍历执行 } // 传入一个读取函数,通过defineProperty把函数添加到watchers里(新增代码) function addWatcher(fn) { window.__watcher = fn; // 暂时挂在全局上,在get中可以读取到 fn(); // 第一次读取 window.__watcher = null; } // 创建响应式 function reactive(target = {}) { if (typeof target !== "object" || target == null) { // 不是对象或数组,则返回 return target; } // proxy的代理配置,单独拿出来写 const proxyConf = { get(target, key, receiver) { // 只处理本身(非原型的)属性 const ownKeys = Reflect.ownKeys(target); if (ownKeys.includes(key)) { // console.log("get", key); // 监听 } const result = Reflect.get(target, key, receiver); if (window.__watcher) watchers.add(window.__watcher); // 第一次读取就埋入依赖 !!!!!新增代码 // 深度监听,因为是触发了get,才会进行递归处理,所以性能会更好些 return reactive(result); }, set(target, key, val, receiver) { // 重复的数据,不处理 if (val === target[key]) { return true; } const ownKeys = Reflect.ownKeys(target); if (ownKeys.includes(key)) { // console.log("已有的 key", key); } else { // console.log("新增的 key", key); } const result = Reflect.set(target, key, val, receiver); updateView(); // 更新视图 return result; // 是否设置成功 }, deleteProperty(target, key) { const result = Reflect.deleteProperty(target, key); return result; // 是否删除成功 }, }; // 生成代理对象 const observed = new Proxy(target, proxyConf); return observed; }
控制台里直接修改proxyObj就可以看到效果了
代码来自慕课网3-13:
const fns = new Set() function reactive(obj) { return new Proxy(obj, { get(target, key) { if (activeFn) fns.add(activeFn) return target[key] // // obj 嵌套属性 传入的是个多维对象 // const val = target[key] // if (typeof val === 'object' && val != null) { // 只考虑 object ,其他未考虑 // return reactive(val) // 直接返回一个 Proxy 对象即可 // } else { // return val // } }, set(target, key, newVal) { target[key] = newVal fns.forEach(fn => fn()) return true } }) } let activeFn function effect(fn) { activeFn = fn fn() // 执行一次,触发 proxy get } const user = reactive({ name: '双越' }) effect(() => { console.log('name', user.name) }) user.name = '张三' setTimeout(() => { user.name = '李四' }, 1000) // // obj 嵌套属性 // const user = reactive({ name: '双越', info: { city: '北京' } }) // effect(() => { // console.log('city', user.info.city) // }) // user.info.city = '上海' // setTimeout(() => { // user.info.city = '杭州' // }, 1000)
前面讲的都是引用类型的响应式机制,那基本类型的呢,其实就是把值弄成{value: '1'}
的形式,然后让reactive去处理。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。