一 proxy
1. 什么是proxy
proxy是es6的新特性,主要是通过handler对象中的拦截方法拦截目标对象target的某些行为(属性查找,赋值,枚举,函数调用等)
const proxy = new Proxy(target, handler)
/* target: 使用Proxy包装的目标对象(可以是任何类型的对象,如原生数组,函数,另外一个代理) */
/* handler: 通常以函数作为属性的对象,定义了在执行各种操作时代理proxy的行为 */
2. 为什么改用proxy
vue2使用object.defineProperty()来实现响应式,会有它的局限性,比如无法监听对属性的添加删除操作,无法监听数组基于下标的修改操作,不支持Map,Set,WeakMap,WeakSet等缺陷,改用proxy来实现响应式可以解决这些问题,并且更好的支持了typescript,但同时也意味着vue3将放弃对低版本浏览器的兼容(兼容版本ie11以上)
3. 几个proxy中hander对象的捕获器
1. handler.has(target, propKey):target是目标对象,propKey为拦截属性名,其作用是拦截判断target对象是否含有属性propKey的操作,对应Reflect.has(target, propKey)
const handler = {
has(target, propKey){
/* 操作 */
return propKey in target
}
}
const proxy = new Proxy(target, handler)
2. handler.get(target, propKey, receiver):target是目标对象,propKey为拦截属性名,receiver是proxy实例,返回读取的属性,作用是拦截对象属性的读取,对应Reflect.get(target, propertyKey, receiver)
const handler = {
get: function(obj, prop) {
return prop in obj ? obj[prop] : '没有此属性'
}
}
const obj = new Proxy({}, handler)
obj.propA = 'A'
obj.propB = 'B'
console.log(obj.propA, obj.propB) /* A B */
console.log(obj,propC) /* 没有此属性 */
3. handler.set(target, propKey, value, receiver):target是目标对象,propKey为拦截属性名,value是新设置的属性值,receiver:是proxy实例,返回true表示操作成功,否则操作失败,作用是拦截对象的属性赋值操作,对应Reflect.set(obj, prop, value, receiver),当对象的属性writable为false时,该属性不能在拦截器中被修改
const handler = {
set: function(obj, prop, value) {
if (prop === 'propA') {
/* 操作 可在这里抛出异常 */
if (!Number.isInteger(value)) {
/* 如果propA不是整数 */
throw new TypeError('The propA is not an integer')
}
if (value > 100) {
/* propA超过100 */
throw new RangeError('The propA More than 100')
}
}
obj[prop] = value // 表示成功
return true
}
}
const obj = new Proxy({}, handler)
obj.propA = 27
console.log(obj.propA) // 27
obj.propA = 27.7 // 抛出异常: Uncaught TypeError: The propA is not an integer
obj.propA = 200 // 抛出异常: Uncaught TypeError: The propA More than 100
4. handler.deleteProperty(target, propKey):target是目标对象,propKey为拦截属性名,返回true表示成功,否则失败,作用是拦截删除target对象的propKey属性的操作,对应Reflect.delete(obj, prop),当属性是不可配置属性时,不能进行删除
const obj = { propA: 'A', propB:'B' }
const proxy = new Proxy(obj, {
deleteProperty(target, prop) {
console.log('删除属性:', target[prop])
return delete target[prop]
}
})
delete proxy.propA
console.log(obj)
/*
删除属性: propA
{ propB:'B' }
*/
二 vue3建立响应式
vue3建立响应式的方法有两种:第一种运用composition-api中的reactive直接构建响应式,composition-api的出现让我们可以直接用setup()函数来处理之前的大部分逻辑,同时也避免了this的使用,像data,watch,computed,生命周期函数都声明在setup函数中,这样就像react-hooks一样提升代码的复用率,逻辑性更强。第二种就是使用传统的 data(){ return{} } 的形式,vue3并没有放弃对vue2写法的支持,而是对vue2的写法完全兼容
1. composition-api
const { reactive , onMounted } = Vue
setup(){
const data = reactive({
propA: 0,
propB: []
})
/* 生命周期mounted */
onMounted(() => {
console.log('mounted')
})
function addPropA() {
data.propA ++
}
const addPropB = () => {
data.propB.push('vue3')
}
return {
data,
addPropA,
addPropB
}
}
2. options-api
options-api的形式和vue2没有什么区别
export default {
data() {
return {
propA: 0,
propB: []
}
},
mounted() {
console.log('mounted')
},
methods: {
addPropA(){
this.propA ++
},
addPropB(){
this.propB.push('vue3')
}
}
}
三 响应式原理
reactive()的作用主要是将目标转化为响应式的proxy实例,如果是嵌套的对象,会继续递归将子对象转为响应式对象,reactive()是向用户暴露的 API,它真正执行的是createReactiveObject()函数
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
if (!isObject(target)) {
{
console.warn(`value cannot be made reactive: ${String(target)}`);
}
return target;
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (target["__v_raw" /* RAW */] &&
!(isReadonly && target["__v_isReactive" /* IS_REACTIVE */])) {
return target;
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
// only a whitelist of value types can be observed.
const targetType = getTargetType(target);
if (targetType === 0 /* INVALID */) {
return target;
}
const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
proxyMap.set(target, proxy);
return proxy;
}
这个函数的处理逻辑:
1. 如果 target 不是一个对象,返回 target
2. 如果 target 已经是 proxy 实例,返回 target
3. 如果 target 不是一个可观察的对象,返回 target
4. 生成 proxy 实例,在原始对象 target 上添加一个属性,属性只读为__v_isReadonly,否则为__v_isReactive,指向这个 proxy 实例,最后返回这个实例,添加这个属性就是为了在第 2 步做判断,防止对同一对象重复监听
vue3响应式的实现:
1. 通过effect声明依赖响应式数据的函数cb(如视图渲染render函数),并执行cb函数,执行过程中,会触发响应式数据getter
2. 在响应式数据getter中进行track依赖收集,建立数据和cb的映射关系存储于targetMap
3. 当变更响应式数据时触发triggter,然后根据targetMap找到关联的cb执行
手写vue3响应式
/* 建立响应式数据 */
function reactive(obj){}
/* 声明响应函数cb */
function effect(cb){}
/* 依赖收集:建立数据和cb映射关系 */
function track(target, key){}
/* 触发更新:根据映射关系执行cb */
function trigger(target, key){}
reactive
function reactive(obj) {
if(!isObject(obj)) return obj
const proxy = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 依赖收集
track(target, key)
return reactive(res)
},
set(target, key, val, receiver) {
const res = Reflect.set(target, key, val, receiver)
// 触发更新
trigger(target, key)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
// 触发更新
trigger(target, key)
return res
},
})
return proxy
}
effect
const effectStack = []
function effect(cb) {
// 对函数进行封装
const rxEffect = function() {
try {
effectStack.push(rxEffect)
return cb()
} finally {
effectStack.pop()
}
}
// 最初要执行一次,进行最初的依赖收集
rxEffect()
return rxEffect
}
track
const targetMap = new WeakMap()
function track(target, key) {
// 存入映射关系
const effectFn = effectStack[effectStack.length - 1]
if(effectFn) {
let depsMap = targetMap.get(target)
if(!depsMap){
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if(!deps){
deps = new Set()
depsMap.set(key, deps)
}
deps.add(effectFn)
}
}
trigger
function trigger(target, key) {
const depsMap = targetMap.get(target)
if(depsMap) {
const deps = depsMap.get(key)
if(deps){
deps.forEach(effect => effect())
}
}
四 总结
vue3的建立响应式大致可以分为三个阶段
1. 初始化阶段:通过组件初始化方法形成对应的proxy对象,然后形成一个负责进行渲染的effect
2. get依赖收集阶段:通过解析template,替换真实的data属性来触发get,调用track方法,通过proxy对象和key形成对应的deps,将负责渲染的effect存入到deps
3. set派发更新阶段:当我们使用obj[key] = value改变对象属性的时候,首先调用triggter方法,通过proxy对象和key找到对应的deps,然后执行依次effect()