回顾Object.defineProperty的hook原理
在es5时期,需要监控属性值变化时,是使用Object.defineProperty
来实现getter
和setter
的拦截。
const target = {}
let _name = ''
Object.defineProperty(target, 'name', {
get: () => {
// getter hook
console.log('getter', _name)
return _name
},
set: v => {
_name = v
// setter hook
console.log('setter', v)
return _name
}
})
复制代码
此时,在对target.name
进行访问和写入时,都会触发hook
。大部分的状态机,都是基于此种原理实现。
这种实现的问题,在于需要明确知道状态机结构,也就是所有的属性名;针对一个复杂的状态结构,就需要不断去定义getter
和setter
。
像React的state,Vue的data/methods结构,无一例外都是如此。其中,React15(?)之后,在dev阶段的MouseEvent对象,就已经开始使用Proxy来包装,但build之后依然保守处理。
因为Proxy无法进行polyfill,出于对IE8等早期浏览器保守治疗的考虑,大部分框架都没有使用Proxy对象作为状态机的底层实现机制。
Proxy的hook方式
Vue3明确宣布使用Proxy进行开发,实质上就是放弃了对IE等非标浏览器的治疗,从开发侧倒逼前端业务向esnext
推进,摆脱老旧浏览器造成的技术债和行业内卷。当然,这与移动端的发展、PC端微软的Edge浏览器、终止Windows7支持,等等大环境因素也是分不开的。
以下是一个简单的Proxy拦截例子:
const target = { name: '' }
const proxy = new Proxy(target, {
get: (tar, prop) => {
console.log('getter', prop)
return tar[prop]
},
set: (tar, prop, value) => {
tar[prop] = value
console.log('setter', prop, value)
}
})
复制代码
从代码中可以看到,对于obj[prop]
的读写拦截,有两个特点:
- 不必事先定义属性标签,其
prop
属性名为动态 - 读写过程可以和
target
完全解耦
reactive的正确打开方式
Vue3的reactive
,本质上是一个预定义结构的状态机,虽然底层使用Proxy实现,但仍然是基于属性拦截的方式来运行。
export default defineComponent({
//...
setup(props) {
const { name = '', count = 0 } = props
const state = reactive({ count })
const handleClick = () => state.count += 1
return () => (
<h1 onClick={handleClick}>
Click {name} {state.count} times
</h1>
)
}
})
复制代码
请注意这里的reactive
监控对象是一个对象{ count }
,而不是直接reactive(count)
。
// 错误示范
let C = reactive(count)
const handleClick = () => C += 1
return () => (
<h1 onClick={handleClick}>
Click {name} {C} times
</h1>
)
复制代码
试想,当C += 1
发生时,C
还是原来的那个C
吗?
这种情况,很多开发者会引入watch
进行救治。对此我不做评价,但我一定不会这么做。
ref的正确打开方式
还是使用上面的例子,用ref
来实现:
export default defineComponent({
//...
setup(props) {
const { name = '', count = 0 } = props
const C = ref(count)
const handleClick = () => C.value += 1
return () => (
<h1 onClick={handleClick}>
Click {name} {C} times
</h1>
)
}
})
复制代码
请注意在变更count
值时,使用的是C.value
,而不是直接对变量C
进行累加;这里我们可以理解为是对ref
的value
标签做的hook
。在jsx
中绑定C
时,推测是对ref
做了类型判断,而后自动取了value
标签。
对ref
对象的错误示范就不做了,跟上面reactive
的错误示范一样的。
这里使用
const
定义也是为了避免出现类似使用错误。推荐多使用const
。分享一个心得:在开发过程中,如果需要对一个变量进行多次值的变更,那就要注意一下,看看是不是模式有问题。我本人在开发过程中,用let
非常之少。
总结
- 不管是
defineProperty
还是Proxy
,都是基于对property
的hook拦截; - 替换掉整个
reactive
或ref
对象,是一个重新赋值的过程,状态机整个被覆盖了,会让hook失效; - 对状态机解构取值,会让值脱离状态机,后续更改不会再与状态机相关,也就无法触发hook。
我对Vue3众多的新特性、新模式给开发带来的便利,非常喜欢以及敬佩,但基于对JS本身的了解,我仍然不太愿意相信所谓的Vue3魔法,也不希望出现有悖JS语法原意的语法糖。
毕竟没有完美的编程语言,JS更是如此。
以上。
PS:本文所有代码都是手写,请当伪代码看待。