乐趣区

笔记Vue6-实现set和数组响应式

Vue.set()

vue2.0 中不能间接监听对象中新增属性的变动,如果须要监听,须要通过 Vue.set(target, propertyName/index, value)办法增加

set 函数通过 Object.defineProperty 将传入的对象上的属性变为响应式属性,简易版实现如下:

  const set = (target, prop, initValue) => {
    let value = initValue
    let dep = new Dep()
    return Object.defineProperty(target, prop, {get() {dep.depend()
        return value
      },
      set(newValue) {
        value = newValue
        dep.notify()}
    })
  }

这段代码中的逻辑与 ref 函数中的逻辑反复,将代码提取放到 createReactive 函数中。

数组响应式

Vue 源码中对于 push pop shift unshift splice sort reverse 这些办法进行了解决,使得通过这些办法操作数组时能感知到数据的变动。

解决数组原型上的 push 办法

  1. 通过 set 生成一个响应式数组,在执行 set 函数时,曾经增加了依赖
  2. 革新数组原型上的 push 办法。首先将原型上的 push 办法存储起来,再从新定义 Array.prototype.push。
  3. 在新办法中首先执行原本的 push 操作,而后须要调用 notify 办法,触发依赖的执行。notify 办法挂载在 createReactive 函数内的 dep 实例上,这里的 this 即 createReactive 函数中的 target 对象,所以能够革新 createReactive 函数,将 dep 实例挂载到 target 的_dep 属性上。这样就能够拿到并触发 notify 了。
  let createReactive = (target, prop, value) => {// let dep = new Dep()
    target._dep = new Dep()
    if (Array.isArray(target)) {target.__proto__ = arrayMethods}
    return Object.defineProperty(target, prop, {get() {target._dep.depend()
        return value
      },
      set(newValue) {
        value = newValue
        target._dep.notify()}
    })
  }
  
  let push = Array.prototype.push
  let arrayMethods = Object.create(Array.prototype)
  arrayMethods.push = function (...args) {push.apply(this, [...args])
    // 这里须要调用 notify 办法
    // notify 办法挂载在 createReactive 函数内的 dep 实例上,批改为挂载到 target 上
    // 这里通过 this 就能够拿到 notify 办法
    this._dep && this._dep.notify()}

残缺带示例代码:

<!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>
  <button id="add">add</button>
  <div id="app"></div>
  <hr>
  <button id="addArr">addArr</button>
  <div id="appArr"></div>
</body>
<script>
  let active

  let effect = (fn, options = {}) => {
    // 为什么要减少一个_effect 函数
    // 因为须要给_effect 减少属性
    // 也能够间接给 fn 减少,然而因为援用类型的起因,会对 fn 函数造成净化
    let _effect = (...args) => {
      try {
        active = _effect
        return fn(...args)
      } finally {active = null}
    }

    _effect.options = options
    _effect.deps = [] // effect 和 dep 的关系 -1
    return _effect
  }

  let cleanUpEffect = (effect) => {
    // 革除依赖
    // 须要反向查找 effect 被哪些 dep 依赖了
    // 在 effect 上增加[] 建设双向索引
    const {deps} = effect
    console.log(deps)
    console.log(effect)
    if (deps.length) {for (let i = 0; i < deps.length; i++) {deps[i].delete(effect)
      }
    }
  }

  let watchEffect = (cb) => {
    /* active = cb
    active()
    active = null */
    let runner = effect(cb)
    runner()

    return () => {cleanUpEffect(runner)
    }
  }

  let nextTick = (cb) => Promise.resolve().then(cb)

  // 队列
  let queue = []

  // 增加队列
  let queueJob = (job) => {if (!queue.includes(job)) {queue.push(job)
      // 增加之后,将执行放到异步工作中
      nextTick(flushJob)
    }
  }

  // 执行队列
  let flushJob = () => {while (queue.length > 0) {let job = queue.shift()
      job && job()}
  }


  let Dep = class {constructor() {
      // 寄存收集的 active
      this.deps = new Set()}
    // 依赖收集
    depend() {if (active) {this.deps.add(active)
        active.deps.push(this.deps) // effect 和 dep 的关系 -2
      }
    }
    // 触发
    notify() {this.deps.forEach(dep => queueJob(dep))
      this.deps.forEach(dep => {dep.options && dep.options.schedular && dep.options.schedular()
      })
    }
  }

  let createReactive = (target, prop, value) => {// let dep = new Dep()
    target._dep = new Dep()
    if (Array.isArray(target)) {target.__proto__ = arrayMethods}
    return Object.defineProperty(target, prop, {get() {target._dep.depend()
        return value
      },
      set(newValue) {
        value = newValue
        target._dep.notify()}
    })
  }

  let ref = (initValue) => createReactive({}, 'value', initValue)

  const set = (target, prop, initValue) => createReactive(target, prop, initValue)

  let computed = (fn) => {
    let value
    let dirty = true // 为 true 表明依赖的变量产生了变动,此时须要从新计算
    let runner = effect(fn, {schedular() {if (!dirty) {dirty = true}
      }
    })
    return {get value() {if (dirty) {
          // 何时将 dirty 重置为 true,当执行 fn 后
          // 因而须要通过配置回调函数,在执行 fn 后将 dirty 重置为 true
          // value = fn() 
          value = runner()
          dirty = false
        }
        return value
      }
    }
  }

  let watch = (source, cb, options = {}) => {const { immediate} = options
    const getter = () => {return source()
    }
    // 将函数增加到 count 的依赖下来,当 count 变动时
    let oldValue
    const runner = effect(getter, {schedular: () => applyCb()})

    const applyCb = () => {let newValue = runner()
      if (newValue !== oldValue) {cb(newValue, oldValue)
        oldValue = newValue
      }
    }

    if (immediate) {applyCb()
    } else {oldValue = runner()
    }
  }

  let push = Array.prototype.push
  let arrayMethods = Object.create(Array.prototype)

  arrayMethods.push = function (...args) {console.log(this)
    push.apply(this, [...args])
    // 这里须要调用 notify 办法
    // notify 办法挂载在 createReactive 函数内的 dep 实例上,批改为挂载到 target 上
    // 这里通过 this 就能够拿到 notify 办法
    this._dep && this._dep.notify()}

  // set 示例:let count = ref(0)
  /* // count.v 新增属性,不会有响应式变动
  document.getElementById('add').addEventListener('click', function () {if (!count.v) {count.v = 0}
    count.v++
  })

  let str
  let stop = watchEffect(() => {str = `hello ${count.v}`
    document.getElementById('app').innerText = str
  }) */

  document.getElementById('add').addEventListener('click', function () {if (!count.v) {set(count, 'v', 0)
      watchEffect(() => {str = `hello ${count.v}`
        document.getElementById('app').innerText = str
      })
    }
    count.v++
  })

  // 数组 push 示例:let arrValue = 0
  // set 函数中曾经对依赖进行了一次增加
  let countArr = set([], 1, 0)
  document.getElementById('addArr').addEventListener('click', function () {
    arrValue++
    countArr.push(arrValue)
  })
  watchEffect(() => {str = `hello ${countArr.join(',')}`
    document.getElementById('appArr').innerText = str
  })

</script>

</html>
退出移动版