编程技术文章分享与教程

网站首页 > 技术文章 正文

尤雨溪教你写进阶版响应式

hmc789 2024-11-18 12:55:52 技术文章 1 ℃

前言

上一篇,我们解析了尤雨溪的初级版响应式代码,一定觉得初级版的响应式和我们平常用的vue3相差太远了,定义一个响应式对象,竟然还要手写get和set,而我们日常使用vue3来定义一个响应式对象只需要用ref或者reactive包起来。

那么,这一篇我们看看尤雨溪对初级版做了哪些升级。

封装reactive

上一版中,定义响应式变量需手写get和set:

const state = {
  get count() {
    dep.depend()
    return actualCount
  },
  set count(newCount) {
    actualCount = newCount
    dep.notify()
  }
}

那么这一版首先就将这一步封装起来。

function reactive(raw) {
  // use Object.defineProperty
  // 1. iterate over the existing keys
  Object.keys(raw).forEach(key => {
    // 2. for each key: create a corresponding dep
    const dep = new Dep()

    // 3. rewrite the property into getter/setter
    let realValue = raw[key]
    Object.defineProperty(raw, key, {
      get() {
        // 4. call dep methods inside getter/setter
        dep.depend()
        return realValue
      },
      set(newValue) {
        realValue = newValue
        dep.notify()
      }
    })
  })
  return raw
}

封装后,定义响应式变量只需要:

const state = reactive({
  count: 0
})

现在看来,是不是和vue3的写法一样了呢?

接下来我们解读下这个优化。

首先翻译几个名词,为什么想翻译一下呢?因为我觉得尤雨溪写的代码命名非常好,一段代码读下来,就像看小说,即使不懂编程,也会大概知道是做什么。

reactive: 反应的
raw: 未加工的、不成熟的

这两个变量名取的好呀,一眼看过去,函数reactive接受一个未加工的变量raw,然后返回了raw。咱们不看内容,就可以猜到reactive函数做了什么。

下面我们来看看reactive函数的内容(代码中的英文注释也是尤雨溪写的哦)。

传进来的raw对象,我们对key进行遍历,取得key对应的value值后,立即重写raw。

这里用到Object.defineProperty(obj, prop, descriptor),这个方法会直接在一个对象obj上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。其中obj是要定义属性的对象,prop是要定义或修改的属性的key,descriptor是更新或定义的属性的描述。

所以这里重写时,给key值对应的value绑定了get和set函数,实现了上一版手动绑定的过程。

然后优化watchEffect,执行effect后立刻将activeEffect置空,防止不必要的订阅行为。

function watchEffect(effect) {
  activeEffect = effect
  effect()
  activeEffect = null
}

再次进阶

上面的优化比较小,只是简化了响应式变量的定义,接下来我们看看尤雨溪还能做什么优化。

首先,重写了Dep类。
之前的Dep类比较简单,只有一个发布订阅模式。

let activeEffect
class Dep {
  subscribers = new Set()
  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect)
    }
  }
  notify() {
    this.subscribers.forEach(effect => effect())
  }
}

优化后:

let activeEffect
class Dep {
  // imeplement this
  subscribers = new Set()

  constructor(value) {
    this._value = value
  }

  get value() {
    this.depend()
    return this._value
  }

  set value(value) {
    this._value = value
    this.notify()
  }

  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect)
    }
  }

  notify() {
    this.subscribers.forEach((effect) => {
      effect()
    })
  }
}

多了一个叫_value的私有变量,并且这个_value是响应式的。然后写了一个reactiveHandlers函数:

// proxy version
const reactiveHandlers = {
  get(target, key) {
    // how do we get the dep for this key?
    const value = getDep(target, key).value
    if (value && typeof value === 'object') {
      return reactive(value)
    } else {
      return value
    }
  },
  set(target, key, value) {
    getDep(target, key).value = value
  }
}

接着是getDep函数:

const targetToHashMap = new WeakMap()

function getDep(target, key) {
  let depMap = targetToHashMap.get(target)
  if (!depMap) {
    depMap = new Map()
    targetToHashMap.set(target, depMap)
  }

  let dep = depMap.get(key)
  if (!dep) {
    dep = new Dep(target[key])
    depMap.set(key, dep)
  }

  return dep
}

最后就是用Proxy替代defineProperty重写reactive函数:

function reactive(obj) {
  return new Proxy(obj, reactiveHandlers)
}

这里补充下Proxy知识点:

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

语法是const p = new Proxy(target, handler)
target是要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
示例:

const handler = {
  get: function(obj, prop) {
    return prop in obj ? obj[prop] : 37;
  }
};

const p = new Proxy({ a: 1 }, handler);

console.log(p.a, p.b); // 1, 37

补充完Proxy知识点,我们再去看reactive。

我们用reactiveHandlers代理obj,读写obj时会进入reactiveHandlers,读的时候,通过getDep获取value值,如果value是object类型,就将value变成响应式,然后返回value;写的时候就把新值写到getDep获取的value上。

在getDep函数中,用到了Map和WeakMap。
Map对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值) 都可以作为一个键或一个值。
WeakMap对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

getDep中,首先去targetToHashMap中获取target,并赋值给depMap,没有获取到的话,就将target作为,new Map()生成的depMap作为值,存到targetToHashMap中。

然后在depMap中取key的值,取不到的话,就new Dep(target[key])作为value和key一起,组成键值对,加到depMap中。最后返回dep。

使用还是一样:

const state = reactive({
  count: 0
})

watchEffect(() => {
  console.log(state.count)
})

state.count++

在上面的代码中加上一些打印,使代码流程看得更加清楚:

可见getDep最后返回了一个Dep的实例。

还有什么不清楚的地方,自行断点调试。

结语

所以,看到这里,你明白Proxy比起defineProperty,在实现vue的响应式时,有什么好处吗?

后期后空,我们继续学习尤雨溪会怎么实现一个mini vue。

Tags:

标签列表
最新留言