基于Proxy原理理解reactive和ref的使用

回顾Object.defineProperty的hook原理

在es5时期,需要监控属性值变化时,是使用Object.defineProperty来实现gettersetter的拦截。


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。大部分的状态机,都是基于此种原理实现。

这种实现的问题,在于需要明确知道状态机结构,也就是所有的属性名;针对一个复杂的状态结构,就需要不断去定义gettersetter

像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]的读写拦截,有两个特点:

  1. 不必事先定义属性标签,其prop属性名为动态
  2. 读写过程可以和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进行累加;这里我们可以理解为是对refvalue标签做的hook。在jsx中绑定C时,推测是对ref做了类型判断,而后自动取了value标签。

ref对象的错误示范就不做了,跟上面reactive的错误示范一样的。

这里使用const定义也是为了避免出现类似使用错误。推荐多使用const。分享一个心得:在开发过程中,如果需要对一个变量进行多次值的变更,那就要注意一下,看看是不是模式有问题。我本人在开发过程中,用let非常之少。

总结

  • 不管是defineProperty还是Proxy,都是基于对property的hook拦截;
  • 替换掉整个reactiveref对象,是一个重新赋值的过程,状态机整个被覆盖了,会让hook失效;
  • 对状态机解构取值,会让值脱离状态机,后续更改不会再与状态机相关,也就无法触发hook。

我对Vue3众多的新特性、新模式给开发带来的便利,非常喜欢以及敬佩,但基于对JS本身的了解,我仍然不太愿意相信所谓的Vue3魔法,也不希望出现有悖JS语法原意的语法糖。

毕竟没有完美的编程语言,JS更是如此。

以上。

PS:本文所有代码都是手写,请当伪代码看待。

おすすめ

転載: juejin.im/post/7031575354443563021