当 React Hooks 遇见 Vue3 Composition API

共 13925字,需浏览 28分钟

 ·

2021-04-29 09:13

1. 前言

前几天在知乎看到了一个问题,React 的 Hooks 是否可以改为用类似 Vue3 Composition API 的方式实现?

关于 React HooksVue3 Composition API 的热烈讨论一直都存在,虽然两者本质上都是实现状态逻辑复用,但在实现上却代表了两个社区的不同发展方向。

我想说,小孩子才分好坏,成年人表示我全都要。


2. 你不知道的 Object.defineProperty

那今天我们来讨论一下怎么用 React Hooks 来实现 Vue3 Composition 的效果。

先来看一下我们最终要实现的效果。

看到这个 API 的用法你会联想到什么?没错,很明显这里借用了 Proxy 或者 Object.defineProperty

在《你不知道的 Proxy:ES6 Proxy 能做哪些有意思的事情?》一文中,我们已经对比过两者的用法了。


其实这里还有一个不为人知的区别,那就是可以通过 Object.defineProperty 给对象添加一个新属性。

const person = {}
Object.defineProperty(person, "name", {
    enumerabletrue,
    get() {
        return "sh22n"
    }
})

打印出来的效果是这样的:

这就很有意思了,意味着我们可以把某个对象 A 上所有属性都挂载到对象 B 上,这样我们不必对 A 进行任何监听,即不会污染 A。

const state = { count0 }
Object.defineProperty({}, "count", {
    get() {
        return state.count
    }
})

3. React Hooks + Object.defineProperty = ?

如果将上面的代码结合 React Hooks,那会出现什么效果呢?没错,我们的 React 变得更加 reactive 了。

const [state, setState] = useState({ count0 })
const proxyState = Object.defineProperty({}, "count", {
    get() {
        return state.count
    },
    set(newVal) {
        setState({ ...state, count: newVal })
    }
})
return (
    <h1 onClick={() => proxyState.count++}>
        { proxyState.count }
    </h1>

)

将这段代码进一步封装,可以得到一个 Custom Hook,也就是我们今天要说的 Composition API

const ref = (value) => {
    const [state, setState] = useState(value)
    return Object.defineProperty({}, "count", {
        get() {
            return state.count
        },
        set(newVal) {
            setState({ ...state, count: newVal })
        }
    })
}
function Counter({
    const count = ref({ value0 })
    return (
        <h1 onClick={() => count.value++}>
            { count.value }
        </h1>

    )
}

当然,这段代码还存在很多问题,依赖了对象的结构、不支持更深层的 getter/setter 等等,我们接下来就一起来优化一下。

4. 实现 Composition

4.1 递归劫持属性

对于属性的对象来说,我们可以遍历,配合 Object.defineProperties 来劫持它的所有属性。

const descriptors = Object.keys(state).reduce((handles, key) => {
    return {
        ...handles,
        [key]: {
            get() {
                return state[key]
            },
            set(newVal) {
                setState({ ...state, [key]: newVal })
            }
        }
    }
}, {})
Object.defineProperty({}, descriptors)

而对于更深层的对象来说,不仅要做递归,还要考虑 setState 这里应该根据访问路径来设置。
首先,我们来对深层对象做一次递归。

const descriptors = (obj) => {
    return Object.keys(obj).reduce((handles, key) => {
        let value = obj[key];
        // 如果 value 是个对象,那就递归其属性进行 `setter/getter`
        if (Object.prototype.toString.call(obj) === "[object Object]") {
            value = Object.defineProperty({}, descriptors(value));
        }
        return {
            ...handles,
            [key]: {
                get() {
                    return value
                },
                set(newVal) {
                    setState({ ...state, [key]: newVal })
                }
            }
        }
    }, {})
}

如果你仔细观察了这段代码,会发现有个非常致命的问题。那就是在做递归的时候,set(newVal) 里面的代码并不对,state 是个深层对象,不能这么简单地对其外层进行赋值。


这意味着,我们需要将访问这个对象深层属性的一整条路径保存下来,以便于 set 到正确的值,可以用一个数组来收集路径上的 key 值。
这里用使用 lodash 的 set 和 get 来做一下演示。

const descriptors = (obj, path) => {
    return Object.keys(obj).reduce((handles, key) => {
        // 收集当前路径的 key 
        let newPath = [...path, key],
            value = _.get(state, newPath);

        // 如果 value 是个对象,那就递归其属性进行 `setter/getter`
        if (Object.prototype.toString.call(obj) === "[object Object]") {
            value = Object.defineProperty({}, descriptors(value, newPath));
        }
        return {
            ...handles,
            [key]: {
                get() {
                    return value
                },
                set(newVal) {
                    _.set(state, newPath, newVal)
                    setState({ ...state })
                }
            }
        }
    }, {})
}

但是,如果传入的是个数组,这里就会有问题了。因为我们只是对 Object 进行了拦截,没有对 Array 进行处理。

const isArray = arr => Object.prototype.toString.call(arr) === '[object Array]'
const isObject = obj => Object.prototype.toString.call(arr) === '[object Object]'

const descriptors = (obj, path) => {
    return Object.keys(obj).reduce((handles, key) => {
        // 收集当前路径的 key 
        let newPath = [...path, key],
            value = _.get(state, newPath);

        // 如果 value 是个对象,那就递归其属性进行 `setter/getter`
        if (isObject(value)) {
            value = Object.defineProperties({}, descriptors(value, newPath));
        }
        if (isArray(value)) {
            value = Object.defineProperties([], descriptors(value, newPath));
        }
        return {
            ...handles,
            [key]: {
                get() {
                    return value
                },
                set(newVal) {
                    _.set(state, newPath, newVal)
                    setState({ ...state })
                }
            }
        }
    }, {})
}

5. 完整版

这样,我们就实现了一个完整版的 ref,我将代码和示例都放到了 codesandbox 上面:Compostion API

const isArray = arr => Object.prototype.toString.call(arr) === '[object Array]'
const isObject = obj => Object.prototype.toString.call(arr) === '[object Object]'
const ref = (value) => {
  if (typeof value !== "object") {
    value = {
      value
    };
  }
  const [state, setState] = useState(value);
  const descriptors = (obj, path) => {
    return Object.keys(obj).reduce((result, key) => {
      let newPath = [...path, key];
      let v = _.get(state, newPath);
      if (isObject(v)) {
            v = Object.defineProperties({}, descriptors(state, newPath));
        } else if (isArray(v)) {
            v = Object.defineProperties([], descriptors(state, newPath));
        }

      return {
        ...result,
        [key]: {
          enumerabletrue,
          get() {
            return v;
          },
          set(newVal) {
            setState(
                _.set(state, newPath, newVal)
                setState({ ...state })
            );
          }
        }
      };
    }, {});
  };
  return Object.defineProperties(isArray(value) ? [] : {}, descriptors(state, []));
};


浏览 35
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报