文章目录
- 前言
- proxy和defineProperty的本质区别
- proxy的基本使用
- 总结
- 响应式原理的简单实现
- 简单实现下reactive和effect
- Ref的原理
前言
vue2中的响应式核心是es5的,缺点有:
- 深度监听需要递归到底,性能层面考虑不太好
- 无法监听对象新增属性和删除属性,需要vue特殊处理
- 无法监听原生数组,需要在数组原型上做拦截
所以vue3采用了es6之后的proxy去重构了响应式原理,proxy能够很好的解决的缺点。
proxy和defineProperty的本质区别
defineProperty是对象的基本操作之一,而proxy是对整个对象所有基本操作的拦截器。也就是proxy包含了defineProperty等等基本操作,而且在调用基本操作的时候,都会被proxy拦截到。
defineProperty里的set和get只是属性描述符。
而proxy里的set和get是真正的基本操作。
换句话来说,defineProperty监听的是属性,而proxy监听的是整个对象。
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; // 是否删除成功,为一个布尔值
},
});
咱们是将原变量做了层代理,以后的操作都只针对代理出来的对象,例如,当我们执行('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的作用:
- 和Proxy能力是一一对应的,配合着实现响应式
- 让代码书写更加规范标准化,例如原来的
'a' in obj
、delete
- 慢慢替代掉Object上的一些功能方法,让它专心的成为一个类型的构造函数
总结
通过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="./">
</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)) {
// ("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)) {
// ("已有的 key", key);
} else {
// ("新增的 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就可以看到效果了
简单实现下reactive和effect
代码来自慕课网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(() => {
// ('city', )
// })
// = '上海'
// setTimeout(() => {
// = '杭州'
// }, 1000)
Ref的原理
前面讲的都是引用类型的响应式机制,那基本类型的呢,其实就是把值弄成{value: '1'}
的形式,然后让reactive去处理。