《WebGIS之Vue进阶教程》(10) 深入完善响应式系统

1 模块化

目前, 主流的浏览器如Chrome, FireFox都已经可以支持模块化导入导出了.

:::warning
特别说明

这里演示一下模块化的基本使用

由于工程化需要配置的环境对于大部分小伙伴而言, 增加了太多暂时不必要的学习成本和负担.

因此, 我在重置版录视频时, 砍掉了这部分. 希望优先保证大家对主线的理解和更自然更好理解的调试

:::

创建一个文件reactive.mjs作为响应式模块

导出

  • reactive函数: 实现普通对象的代理
  • effect函数. 实现注册副作用函数

示例

// 定义一个副作用函数桶, 存放所有的副作用函数. 每个元素都是一个副作用函数
// 修改 [state -> Map[name: Set(fn, fn), age: Set(fn, fn)], state1 -> Map]
const bucket = new WeakMap()

// 定义一个全局变量, 保存当前正在执行的副作用函数
let activeEffect = null

function isObject(value) {
  return typeof value === 'object' && value !== null
}

// 收集依赖
function track(target, key) {
  // 只有activeEffect有值时(保存的副作用函数), 才添加到桶中
  if (!activeEffect) return

  let depMap = bucket.get(target)
  if (!depMap) {
    depMap = new Map()
    bucket.set(target, depMap)
  }
  let depSet = depMap.get(key)
  if (!depSet) {
    depSet = new Set()
    depMap.set(key, depSet)
  }
  
  depSet.add(activeEffect)
}

function trigger(target, key) {
  let depMap = bucket.get(target)

  if (!depMap) return

  // 从副作用函数桶中依次取出每一个元素(副作用函数)执行
  let depSet = depMap.get(key)
  if (depSet) {
    depSet.forEach((fn) => fn())
  }
}
/**
 * 创建响应式数据
 *  @param [object]: 普通对象
 *  @return [Proxy]: 代理对象
 */
export function reactive(data) {
  if (!isObject(data)) return

  return new Proxy(data, {
    get(target, key) {
      // 在get操作时, 收集依赖
      track(target, key)

      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      // 在set操作时, 触发副作用重新执行
      trigger(target, key)
      return true
    },
  })
}

/**
 * 注册副作用函数
 *  @param [function]: 需要注册的 副作用函数
 */
export function effect(fn) {
  if (typeof fn !== 'function') return

  // 记录正在执行的副作用函数
  activeEffect = fn
  // 调用副作用函数
  fn()
  // 重置全局变量
  activeEffect = null
}

使用ES Module的方式

  • 使用export导出
  • 使用import导入

在测试文件中, 通过import导入, 但是需要在script元素上加上type属性, 值为module

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script type="module">
      // 使用import导入
      import { reactive, effect } from './reactive.mjs'
    </script>
  </body>
</html>

2 单例模式

:::color1 **🤔****思考**
  1. 对于同一个源对象每次调用reactive返回的代理对象应该是一样的
  2. 对于一个已经代理过的对象再次代理应该返回的也应该是一样的

:::

1) 实现单例

为了实现单例, 我们需要建立`源对象`->`代理对象`的映射关系
  • 如果存在映射, 说明已经代理过了, 直接返回
  • 如果不存在映射, 说明没有代理过, 创建一个新的代理对象返回

定义源对象->代理对象的映射表(使用WeakMap)

// 定义一个副作用函数桶, 存放所有的副作用函数. 每个元素都是一个副作用函数
// 修改 [state -> Map[name: Set(fn, fn), age: Set(fn, fn)], state1 -> Map]
const bucket = new WeakMap()

// 建立一个映射表 target -> proxy
const reactiveMap = new WeakMap() // 新增

// 定义一个全局变量, 保存当前正在执行的副作用函数
let activeEffect = null

function isObject(value) {
  return typeof value === 'object' && value !== null
}

// 收集依赖
function track(target, key) {
  // 只有activeEffect有值时(保存的副作用函数), 才添加到桶中
  if (!activeEffect) return

  let depMap = bucket.get(target)
  if (!depMap) {
    depMap = new Map()
    bucket.set(target, depMap)
  }
  let depSet = depMap.get(key)
  if (!depSet) {
    depSet = new Set()
    depMap.set(key, depSet)
  }

  depSet.add(activeEffect)
}

function trigger(target, key) {
  let depMap = bucket.get(target)

  if (!depMap) return

  // 从副作用函数桶中依次取出每一个元素(副作用函数)执行
  let depSet = depMap.get(key)
  if (depSet) {
    depSet.forEach((fn) => fn())
  }
}
/**
 * 创建响应式数据
 *  @param [object]: 普通对象
 *  @return [Proxy]: 代理对象
 */
function reactive(data) {
  if (!isObject(data)) return

  // 如果映射表中存在了对应关系
  if (reactiveMap.has(data)) {
    // 返回data对应的代理对象
    return reactiveMap.get(data)
  }

  const proxy = new Proxy(data, {
    get(target, key) {
      // 在get操作时, 收集依赖
      track(target, key)

      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      // 在set操作时, 触发副作用重新执行
      trigger(target, key)
      return true
    },
  })
  // 建立data(源对象)和proxy(代理对象)的映射关系
  reactiveMap.set(data, proxy)
  return proxy
}

/**
 * 注册副作用函数
 *  @param [function]: 需要注册的 副作用函数
 */
function effect(fn) {
  if (typeof fn !== 'function') return

  // 记录正在执行的副作用函数
  activeEffect = fn
  // 调用副作用函数
  fn()
  // 重置全局变量
  activeEffect = null
}

测试用例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./reactive.js"></script>
  </head>
  <body>
    <script>
      const source = { name: 'hello' }

      const p = reactive(source)
      const p1 = reactive(source)
      console.log(p === p1) // true
    </script>
  </body>
</html>

2) 实现重复代理

可以定义一个特殊的标识`__v_isReactive`
  • 如果存在该标识, 说明已经代理过, 直接返回
  • 如果不存在该标识, 说明没有被代理, 创建新的代理对象

示例

// 定义源对象->代理对象映射表
const reactiveMap = new WeakMap()

// 定义一个副作用桶bucket
const bucket = new WeakMap() 
// 定义一个全局变量, 作于保存 `当前副作用函数`
let activeEffect = null

// 收集依赖
function track(target, key) {
  // 根据不同的target, 获取对应的Map
  let depMap = bucket.get(target)
  if (!depMap) {
    depMap = new Map()
    bucket.set(target, depMap)
  }
  let depSet = depMap.get(key)
  if (!depSet) {
    depSet = new Set()
    depMap.set(key, depSet)
  }
  depSet.add(activeEffect)
}

// 触发执行
function trigger(target, key) {
  let depMap = bucket.get(target)

  if (!depMap) return

  let depSet = depMap.get(key)

  if (depSet) {
    // 如果对应的集合存在, 遍历集合中的每个函数
    depSet.forEach((fn) => fn())
  }
}

/**
 * 定义响应式
 *  @param [object] : 普通对象
 *  @return [Proxy] : 代理对象
 */
export function reactive(data) {
  // 如果传入的data不是一个普通对象, 不处理
  if (typeof data !== 'object' || data == null) return

  if (reactiveMap.has(data)) {
    // 返回data对应的代理对象
    return reactiveMap.get(data)
  }

  // 如果存在标识, 说明data被代理过了
  if (data['__v_isReactive']) {
    return data
  }

  const proxy = new Proxy(data, {
    get(target, key) {
      if (key == '__v_isReactive') return true // 新增

      // console.log(`自定义访问${key}`)
      if (activeEffect != null) {
        // 收集依赖
        track(target, key)
      }

      return target[key]
    },
    set(target, key, value) {
      // console.log(`自定义设置${key}=${value}`)
      target[key] = value // 先更新值
      // 触发更新
      trigger(target, key)
      return true
    },
  })

  reactiveMap.set(data, proxy)
  return proxy
}

/**
 * 注册副作用函数
 * @params [function]: 要注册的 副作用函数
 */
export function effect(fn) {
  if (typeof fn !== 'function') return

  // 将当前注册的副作用函数 保存 到全局变量中
  activeEffect = fn
  // 执行当前副作用函数, 收集依赖
  fn()
  // 重置全局变量
  activeEffect = null
}

测试用例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./reactive.js"></script>
  </head>
  <body>
    <script>
      const source = { name: 'hello' }

      const p = reactive(source)
      const p1 = reactive(p)
      console.log(p === p1) // true
    </script>
  </body>
</html>

3 支持分支切换

:::color1 **🤔****思考**

如果副作用函数中存在条件判断

在条件改变时, 理论上不该再收集旧分支的依赖

:::

问题示例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./reactive.js"></script>
  </head>
  <body>
    <div id="app"></div>
    
    <script>
      const state = reactive({ flag: true, name: 'xiaoming', age: 20 })
      
      effect(() => {
        app.innerHTML = state.flag ? state.name : state.age
      })
      
      setTimeout(() => {
        // 我们期望:
        //   - 当flag为true时, 收集name
        //   - 当flag为false时, 收集age
        state.flag = false
      }, 1000)
    </script>
  </body>
</html>

为了实现上述功能. 当分支切换时, 我们需要做两件事

  1. 清理之前的依赖
  2. 重新收集新的依赖

1) 包装副作用函数

由于我们需要在副作用函数上扩展一些属性, 重新收集新的依赖

可以考虑封装一个类ReactiveEffect来包装副作用函数, 方便后期扩展属性

class RectiveEffect {
  constructor(fn) {
    this.fn = fn
  }
  run() {
    activeEffect = this
    this.fn()
    activeEffect = null
  }
}

在全局变量activeEffect上保存的是reactiveEffect的实例

2) 建立对应关系

如果我们想清理依赖关系, 就需要知道当前副作用函数被哪些属性依赖.

可以考虑扩展一个deps属性来记录

class RectiveEffect {
  constructor(fn) {
    this.fn = fn
    this.deps = [] // 新增
  }
  run() {
    activeEffect = this
    this.fn()
    activeEffect = null
  }
}

在track收集属性依赖的副作用函数的同时, 记录当前副作用函数依赖的属性

// 收集依赖
function track(target, key) {
  // 根据不同的target, 获取对应的Map
  let depMap = bucket.get(target)
  if (!depMap) {
    depMap = new Map()
    bucket.set(target, depMap) // 建立target -> Map的对应关系
  }
  // 根据不同的key, 获取对应的集合
  let depSet = depMap.get(key)
  if (!depSet) {
    // 如果不存在, 创建一个新的集合
    depSet = new Set()
    depMap.set(key, depSet) // 建立 key -> Set的对应关系
  }
  depSet.add(activeEffect) // 将当前副作用函数添加到对应的集合

  activeEffect.deps.push(depSet) 
}

:::color1
🤔****思考

Q: 为什么将集合放入数组, 而不是将key值放入数组里?

A: 如果将key放入数组里, 后面还是需要通过key找到集合, 再清除集合中所有的元素

:::

3) 清理旧依赖

新增一个清理函数`cleanup`
function cleanup(target) {
  let deps = target.deps

  if (deps) {
    deps.forEach((depSet) => {
      // 清空Set
      depSet.delete(target)
    })
    deps.length = 0
  }
}

在重新收集依赖前, 先清理旧的依赖关系

class RectiveEffect {
  constructor(fn) {
    this.fn = fn
    this.deps = []
  }
  run() {
    activeEffect = this
    cleanup(this) // 新增
    this.fn()
    activeEffect = null
  }
}

此时, 会出现Set假死的问题

上述问题可以简化为

const set = new Set([1])
set.forEach(item => {
  set.delete(1)
  set.add(1)
  console.log('死循环...')
})

在trigger时, 构造一个新的Set来遍历

// 触发执行
function trigger(target, key) {
  let depMap = bucket.get(target)

  if (!depMap) return

  let depSet = depMap.get(key)

  if (depSet) {
    let effects = [...depSet]
    effects.forEach((effect) => effect.run())
  }
}

4 避免死循环

1) 如何产生

> 示例 >
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./reactive.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      const state = reactive({ name: 'xiaoming', age: 20 })

      effect(() => {
        // 在副作用函数中, 设置依赖的值, 会出现死循环
        state.age = state.age + 1
      })
    </script>
  </body>
</html>

  1. 先获取state.age的值, 收集依赖, 建立了age-> 副作用函数的关系
  2. 再更新state.age的值, 触发更新, 调用trigger
  3. trigger中调用age对应的副作用函数, 再次执行

2) 如何避免

我们重写`trigger`函数, 多加一个判断: 只有当遍历执行的函数跟当前副作用函数不同时, 才会调用`run`去执行
function trigger(target, key) {
  let depMap = bucket.get(target)

  if (!depMap) return

  let depSet = depMap.get(key)

   if (depSet) {
    let effects = [...depSet]
    // 如果对应的集合存在, 遍历集合中的每个函数
    effects.forEach((effect) => {
      if (effect !== activeEffect) {
        effect.run()
      }
    })
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值