手写Ref 最强响应式处理方式!

631 阅读5分钟

假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!

书接上篇,我们正式手搓完了reactive,那么ref还会远吗?回到第一篇文章就说过的话:我们不去纠结是使用reactive还是ref。没错!当我们搞懂两者的底层逻辑,ref和reactive的真面目都逃不过你的一双火眼金睛。

image.png

开始手写

还不清楚Reactive实现原理的掘友可以去看看我的上篇文章手写reactive2.0(依赖收集),得益于Proxy对象代理使得我们的reactive可以收集依赖和触发依赖,但是Proxy有个缺点就是只接受对象作为参数,而ref还得考虑将原始类型数据变成响应式,所以我们手写前需要考虑的问题是:

1. 判断ref身上的参数是对象还是原始值,并以不同的方式代理。

2. 当属性被读取值、设置值、判断值这些行为当中,为属性添加副作用函数。

3. 在属性被修改值或者删除值的时候,去触发这些属性身上绑定的副作用函数,来实现响应效果。

ref函数的实现

首先,我们定义了一个 ref 函数,其目的是将原始类型数据或引用类型数据转化为响应式对象。

export function ref(val) { 
  return createRef(val)
}

createRef函数的实现

然后,为了避免二次代理响应式对象的情况,我们用createRef 函数判断传入的值是否已经是响应式对象,如果是则直接返回,如果不是则调用 RefImpl 构造函数,将值转化为响应式对象。

function createRef(val) {
  if (val.__v_isRef) {
    return val;
  }
  return new RefImpl(val);
}

副作用函数的收集与副作用函数的触发

当代理对象的属性值被读取、设置、判断等这些行为时,我们还需要为属性收集副作用函数;在属性被修改值或者删除值的时候,去触发这些属性身上绑定的副作用函数,来实现响应效果。 在上篇文章中,我们对此有详细的解释,不理解的小伙伴可以去翻翻上一篇文章手写reactive2.0(依赖收集)

RefImpl类的实现

紧接着,我们需要知道哪些属性值已经是响应式的,就得有一个RefImpl类,它是实现响应式对象的关键。在构造函数RefImpl中,我们给每一个被ref操作过的属性值都添加了标记__v_isRef,并通过convert函数将属性值转换为响应式对象。

class RefImpl {
  constructor(val) {
    this.__v_isRef = true;
    this._value = convert(val);
  }

  get value() {
    track(this, 'value');
    return this._value;
  }

  set value(newVal) {
    if (newVal !== this._value) {
      this._value = convert(newVal);
      trigger(this, 'value');
    }
  }
}

当代理对象的属性值被读取、设置、判断等这些行为时,我们还需要为属性收集副作用函数

get value() {
    track(this, 'value');
    return this._value;
  }

由于传入的参数是原始值,所以第二个参数我们人为地约定俗成用'value'替代。

在属性被修改值或者删除值的时候,去触发这些属性身上绑定的副作用函数,来实现响应效果

set value(newVal) {
    if (newVal !== this._value) {
      this._value = convert(newVal)
      trigger(this, 'value') // 触发掉 'value' 上的所有副作用函数
    }
  }
}

判断更新的值与之前的值是否相等,如果不相同则调用convert再判断一次更新的值是不是引用类型,然后最后再触发掉 'value' 上的所有副作用函数。

convert函数的实现

还有一个convert函数用于判断传入的值是否为对象,如果是,则调用reactive函数将其转换为响应式对象,若不是则返回。

function convert(val) {
  if (typeof val !== 'object' || val === null) {
    return val;
  } else {
    return reactive(val);
  }
}

效果

<!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>
  <script type="module">
    import { ref } from './ref.js'
    import { effect } from './effect.js'

    const age = ref({n:18})


    effect(() => {
      console.log(age.value.n);
    })
    
    
    setInterval(() => {
      age.value.n++
    }, 2000)


  </script>
</body>
</html>

动画.gif

用ref还是reactive?

这个取决于自己,没有谁好谁差,但是我们知道ref其实才是功能最强大的,当ref接受的参数为引用类型时其实也是通过reactive实现响应式化。ref缺点就是写法上比reactive多了一个.value。所以当一个组件里面响应式变量多的时候用reactive;不多的时候用ref,减轻代码量。

总结

reactive 使用proxy代理了对象的各种操作行为,当属性被读取值、设置值、判断值这些行为当中,为属性添加副作用函数,在属性被修改值或者删除值的时候,去触发这些属性身上绑定的副作用函数,来实现响应效果。

当ref 身上参数是对象的时候,依然是借助reactive代理对象实现响应式的;当参数式原始值的时候,给原始值添加value函数并采用了原生JS内置的setter和getter效果来实现为属性添加副作用函数和触发函数进而达到响应式效果的。

假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!

开源Git仓库: gitee.com/cheng-bingw…

更多内容:手写reactive2.0(依赖收集)