Vue.js设计与实现读书笔记三 第四章响应系统的作用与实现

4.8 计算属性computed与lazy

// 存储副作用函数的桶
const bucket = new WeakMap()

// 原始数据
const data = { foo: 1, bar: 2 }
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    trigger(target, key)
  }
})

function track(target, key) {
  if (!activeEffect) return
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
  activeEffect.deps.push(deps)
}

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)

  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })
  effectsToRun.forEach(effectFn => {
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      effectFn()
    }
  })
  // effects && effects.forEach(effectFn => effectFn())
}

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
// effect 栈
const effectStack = []

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
    activeEffect = effectFn
    // 在调用副作用函数之前将当前副作用函数压栈
    effectStack.push(effectFn)
    const res = fn()
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]

    return res
  }
  // 将 options 挂在到 effectFn 上
  effectFn.options = options
  // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  if (!options.lazy) {
    effectFn()
  }

  return effectFn
}

function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = 0
}

总结以上实现的响应式的代码
1.实现了将代理代码分离,track收集副作用函数,trigger执行副作用函数,
2.effect触发收集依赖
3.cleanup,处理了 xx.innerHTML=obj.ok?obj.text:"not"这样的分支问题,在trigger执行之前清除
4.effectStack处理了effect需要嵌套的问题
5. effectFn.options.scheduler(effectFn)处理的调度问题,让用户传递参数给effect来控制副作用函数的执行
下面看一用上面的代码实现computed方法

// =========================

function computed(getter) {
  let value // 缓存值
  let dirty = true // true表示getter中的多个数据,改变了,需要重新计算

  const effectFn = effect(getter, {
    lazy: true,//设置这个属性为true的时候 getter就不会立即执行,会返回到effectFn
    scheduler() {
      if (!dirty) {
        dirty = true
        trigger(obj, 'value')
      }
    }
  })

  const obj = {
    get value() {
      if (dirty) { // 当computed依赖的两个数据发生改变,才执行
        value = effectFn() // 执行getter,计算两个值
        dirty = false
      }
      track(obj, 'value')
      return value
    }
  }

  return obj
}

const sumRes = computed(() => obj.foo + obj.bar)

console.log(sumRes.value)
console.log(sumRes.value)

obj.foo++

console.log(sumRes.value)

effect(() => {
  console.log(sumRes.value)
})

obj.foo++

当第一次打印sumRes.value,value = effectFn()执行了,访问了obj.foo + obj.bar,他们用track将这个副作用函数存储起来,dirty = false 第二次打印的时候,直接返回value,
修改依赖数据obj.foo++,trigger执行要执行track收集的函数,但这里有scheduler这个参数,所以执行的是```
if (!dirty) {
dirty = true
trigger(obj, ‘value’)
}

将dirty = true,再打印sumRes.value,会重新执行value = effectFn() 更新了value的值

其中 trigger(obj, 'value')和 track(obj, 'value')连个语句用于

```javascript
effect(() => {
  console.log(sumRes.value)
})

obj.foo++

如果没有上面这种,写法,那么 trigger(obj, ‘value’)和 track(obj, ‘value’)他们收集的副作用为空,
让我们看看这种情况下如何执行的
effect执行,打印suRes.value, activeEffect有值,但是这个时候不会执行 obj的proxy,因为上面只是代理的obj(最上面的),所以打印suRes.value的时候是访问computed中的obj
执行

if (dirty) { // 当computed依赖的两个数据发生改变,才执行
        value = effectFn() // 执行getter,计算两个值
        dirty = false
      }
      track(obj, 'value')
      return value

这个时候dirty为false,但是由于 activeEffect有值,手动 track(obj, ‘value’)收集了这个obj的副作用函数
下面再修改obj.foo++

scheduler() {
      if (!dirty) {
        dirty = true
        trigger(obj, 'value')
      }

这个dirty为false,trigger执行刚才存储的副作用函数,执行 console.log(sumRes.value)

4.9 watch的实现原理

实现watch监听代理对象数据变化,回调函数中获取新旧值的功能

代码就补贴了,traverse就是变量对象,把所有对象访问一遍,做可以一次让所有对象属性都绑定自己的副作用函数,
watch(getter,cb)getter可以是一个已经代理的对象,也可以是一个有返回值的函数,对象会被 traverse给变量然后返回,注意一定要返回需要监听的值,
然后watch内部的effect 使用了lazy,不立刻执行副作用函数, 最后执行effectFn(),获得第一次的旧值,同时触发track收集副作用函数,当监听的值改变,scheduler中执行effectFn()获得新值,执行cb()然后将新值赋值给旧值,这样,下一次改变监听的数据,旧值就是上一次的新值

4.10 立即执行的watch与回调执行时机

这个主要是让watch中立即执行一次回调函数由,watch(obj,cb,options)中options.immediate为true控制,回调执行时机是用options.flush来控制回调函数执行是否添加到微任务中

4.11 过期的副作用

这个主要是watch监听的数据改变,回调函数如果是异步函数,那么每次数据改变,就会触发异步函数,但是如果确定那个异步函数的结果是我们想要的,这里让最后一个异步函数的结果为最终结果。让前面的异步函数过期,
主要是由异步函数的第三个参数onInvalidate 来控制,不过需要手动设置 let valid = true onInvalidate(() => { valid = false })

valid用于判断当前函数是否过期true表示未过期,false表示过期,onInvalidate是将参数的函数传递给,下一次修改监听数据,
由于是异步的当前await是没执行玩,如果下一次修改,还没执行玩。onInvalidate的函数就执行,valid变为false,这样上一次的函数就过期啦,就不采纳他的值。


  let valid = true
  onInvalidate(() => {
    valid = false}// 存储副作用函数的桶
const bucket = new WeakMap()

// 原始数据
const data = { foo: 1, bar: 2 }
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    trigger(target, key)
  }
})

function track(target, key) {
  if (!activeEffect) return
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
  activeEffect.deps.push(deps)
}

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)

  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })
  effectsToRun.forEach(effectFn => {
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      effectFn()
    }
  })
  // effects && effects.forEach(effectFn => effectFn())
}

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
// effect 栈
const effectStack = []

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
    activeEffect = effectFn
    // 在调用副作用函数之前将当前副作用函数压栈
    effectStack.push(effectFn)
    const res = fn()
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]

    return res
  }
  // 将 options 挂在到 effectFn 上
  effectFn.options = options
  // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  if (!options.lazy) {
    effectFn()
  }

  return effectFn
}

function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = 0
}




// =========================
// 变量代理对象所有的属性
function traverse(value, seen = new Set()) {
  if (typeof value !== 'object' || value === null || seen.has(value)) return
  seen.add(value)
  for (const k in value) {
    traverse(value[k], seen)
  }

  return value
}

function watch(source, cb, options = {}) {
  let getter
 // 和computed一样最终都会将第一个参数变成函数
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }
 
  let oldValue, newValue

  let cleanup  //存储上一次,onInvalidate传入的函数
  function onInvalidate(fn) { // 上一次执行,将参数,赋值给 cleanup
    cleanup = fn
  }

  const job = () => {
    newValue = effectFn()
    if (cleanup) { 
      cleanup() // 执行上一个异步函数的方法,改变他的valid 值
    }
    cb(oldValue, newValue, onInvalidate)
    oldValue = newValue
  }

  const effectFn = effect(
    // 执行 getter
    () => getter(),
    {
      lazy: true,
      scheduler: () => {
        if (options.flush === 'post') {  // 控制回调执行时机
          const p = Promise.resolve()
          p.then(job)
        } else {
          job()
        }
      }
    }
  )
  
  if (options.immediate) {
    job()
  } else {
    oldValue = effectFn()
  }
}

let count = 0
function fetch() {
  count++
  const res = count === 1 ? 'A' : 'B'
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(res)
    }, count === 1 ? 1000 : 100);
  })
}

let finallyData

watch(() => obj.foo, async (newVal, oldVal, onInvalidate) => {
  let valid = true
  onInvalidate(() => {
    valid = false
  }) // 将方法传递给下一次job函数执行
    
  const res = await fetch()

  if (!valid) return

  finallyData = res
  console.log(finallyData)
})

obj.foo++
setTimeout(() => {
  obj.foo++
}, 200);

问题总结:

4.8 计算属性computed与lazy

(十一)调度器的options中的lazy的含义?computed的实现原理?scheduler中的trigger和obj中的track有啥作用?
(1)lazy为true,就表示我们effect函数中不直接执行effeteFn,而是返回effectFn,在effectFn中也返回执行fun()副作用函数的结果,所以如果lazy为true,那么执行effect函数的结果会得到fun()执行的结果。这和computed的实现原理就很接近了
(2)computed就是由effect(getter,{lazy:true})改造而来,getters是一个有代理对象属性返回的函数,他执行会被收集,computed中用一个obj.value对象来接受getters返回的值,当getters中的数据改变的时候,访问computed的结果的时候,他会执行track(obj, ‘value’) 将obj变成响应式数据,每次访问computed的结果,并且getters中的数据有变化的时候都会重新执行effectFn,重新计算
(3)这两个手动收集和执行副作用函数,是为了实现effetet中嵌套computed,因为computed的结果并不是data中的代理,所以effect中执行,不会自动触发track和trigger,
track写在obj的get value中,用effect执行,可以触发track添加obj,更新getters依赖的的时候自动执行obj的effectFn函数,

4.9 watch的实现原理
(十二)watch的参数有哪一些?新旧值如何实现的?它的实现原理是?watch和computed相同和区别?
(1)watch(obj,cb,{ immediate,deep,flush })
其中第一个参数是obj,可以是一个被代理的对象,或者是一个返回代理对象属性的函数,都会在effect中被读取,被代理,这个和deep参数有关,deep为true,则会用traverse遍历全部,实现全部追踪,
cb是当监听的数据发生变化的时候的回调函数,是放在effect中scheduler函数中执行的,
deep:表示是不是需要深监听,
immediate:这个表示是不是watch执行的时候就执行一次回调函数(没有旧value)
flush:表示回调函数执行的时机,是在渲染前还是渲染后执行回调函数,这个的实现是scheduler中调用回调函数,可以使用队列,来异步调用,这样结果就会在最后

(2)这个先要设置effect的lazy为true,新值就是执行effectFn得到,旧值就是cb执行完后的新值
(3)从源码上来看,computed会根据getters中的依赖,生成一个新的结果,每次读取这个结果,他会先查看依赖是否有变化,如果有变化才会更新,也就是有缓存,computed的返回值本身也可以成为代理数据。
而watch其实本质是在监听代理数据的改变,也就是当代理数据触发trigger的时候,执行watch的回调函数,
他们都使用effect中的lazy和scheduler这两个参数选项,
computed用lazy获取更新后的依赖的值,scheduler用来设置是否更新缓存,
watch用lazy获取到回调函数的旧值,scheduler来执行回调函数

4.11 过期的副作用
(十三)竞态问题是什么?watch中如何解决竞态问题?
(1)竞态问题是指如果watch的回调函数是一个异步函数,例如是一个请求,那么每次watch监听的对象变化,会导致异步函数发送请求,我们该如何确定请求返回的结果是我们想要的结果,
(2)watch中遇到这种情况,默认是使用最后一次的结果,他使用的是回调函数cb的第三个参数,他是一个函数,函数的参数也是一个函数,用来修改expire,expire表示是否过期,每一次执行都会注册一个全局函数,作用是设置当前回调函数为过期,这样如果连续执行异步函数,下一次就会执行上一次的全局函数,将上一次回调函数expire设置为过期,如果上一个还没有结果,它的结果就会作废掉

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值