vue3响应式系统简易实现(proxy、computed、watch)

201 阅读6分钟

vue3响应式系统简易实现

从宏观的角度来实现vue3的响应式系统的机制,从副作用函数开始逐步实现一个响应式系统,本文还讲述了计算属性以及侦听器的实现原理。

响应式数据和副作用函数

副作用函数指的是会产生副作用的函数 函数影响了其他函数的执行,列如对外界的数据进行了set操作,从而在一定程度上影响了外部函数的运行。

纯函数:入参和出参,函数内部只是单纯的根据入参进行相应的操作,然后把函数的操作结果返回出来就是纯函数,不影响入参也不影响外部数据。

如下列代码所示:

function effect() {
    document.body.innerText = 'hello world'; // 函数副作用 影响了body的文本节点
}

function effct(a) {
    return a * 2 // 纯函数
}

响应式数据

const obj = { text: 'hello world'}
function effect() {
    document.body.innerText = obj.text; // 函数副作用 影响了body的文本节点
}

obj.text = 'hello';
// 如果是响应式数据的话document.body.innerText的值会随着obj.text的变化发送响应式变化

响应式数据的实现

  1. 当副作用effct函数执行的时候,会触发obj.text(getter)的读取操作。
  2. 当修改obj.text的值时,会触发obj.text的设置(setter)操作。
 const data = {
     text: 'hello world'
 }
 // 储存当前活动的副作用函数
 let activeEffct
 const bucket = new Set()
 // 对原始数据的代理
 const obj = new Proxy(data, { // 数据源, handler一些函数属性的对象
     get(target, key) {
         bucket.add(activeEffct)
         
         return target[key]
     },
     set(target, key, newValue) {
         target[key] = newValue
         bucket.forEach(fn => fn())
         
         return true 
     }
 })
 
 function effct(fn) {
     activeEffct = fn
     fn()
 }
 
 effct(() => {
     document.body.innerText = obj.text
 })

setTimeoust(() => {
    obj.text = 'hello'
}, 2000) // hello world -> hello

这就实现了一个简单的响应式系统的响应式系统, 当改变obj.text就会触发setter函数,执行副作用函数。

数据响应式的优化

  1. 优化做多个响应式数据触发全部副作用函数
const data = {
  text: 'hello world',
  isOk: true
}
// 储存当前活动的副作用函数
let activeEffect
// 使用weakMap和对象建立弱连接,在不会影响垃圾回收机制
const bucket = new WeakMap()
// 对原始数据的代理
const obj = new Proxy(data, { // 数据源, handler一些函数属性的对象
  get(target, key) {
      // 如果没有副作用函数就是普通对象
      if (!activeEffect) return target[key];
      // 看weakMap中有没有改对象的唯一键
      let depsMap = bucket.get(target);
      if (!depsMap) bucket.set(target, (depsMap = new Map()));
      
      // 看对象中是否有属性被保存
      let deps = depsMap.get(key);
      // 使用set作为储存器好处是可以去重
      if (!deps) depsMap.set(key, (deps = new Set()));
      // 收集副作用函数
      deps.add(activeEffect);

      activeEffect.deps.push(deps)
      return target[key]
  },
  set(target, key, newValue) {
      target[key] = newValue;
      
      let depsMap = bucket.get(target);
      if (!depsMap) return
      
      // 由于不断的触发收集依赖和触发依赖 要使用一个新的set来包裹 不然会触发无限循环
      let deps = new Set(depsMap.get(key));
      
      // 执行副作用函数
      deps && deps.forEach(fn => fn())
      
      return true 
  }
})

function effect(fn) {
  const effectFn = () => {
     // 清除多余的副作用函数
     cleanup(effectFn);
     // 在触发副作用函数时 保证收集的函数是改副作用函数
     activeEffect = effectFn;
     fn();
   }
   // 收集包含副作用函数的set集合
   effectFn.deps = [];
   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
}

effect(() => {
  document.body.innerText = obj.isOk ? obj.text : '' 
})
// 可以运行
// setTimeout(() => {
//  obj.text = 'hello'
// }, 2000) // hello world -> hello

可以把get中的逻辑封装成一个track函数,set中的逻辑封装成trigger函数

const obj = new Proxy(data, { // 数据源, handler一些函数属性的对象
  get(target, key) {
      // 如果没有副作用函数就是普通对象
      if (!activeEffect) return target[key];
      track(target, key)
      return target[key]
  },
  set(target, key, newValue) {
      target[key] = newValue;
      trigger(target, key)
      return true 
  }
})

function track(target, key) {
  if (!activeEffect) return  
  // 看weakMap中有没有改对象的唯一键
  let depsMap = bucket.get(target);
  if (!depsMap) bucket.set(target, (depsMap = new Map()));
  
  // 看对象中是否有属性被保存
  let deps = depsMap.get(key);
  // 使用set作为储存器好处是可以去重
  if (!deps) depsMap.set(key, (deps = new Set()));
  // 收集副作用函数
  deps.add(activeEffect);

  activeEffect.deps.push(deps)
}

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

  let deps = new Set(depsMap.get(key));
  // 执行副作用函数
  deps && deps.forEach(fn => fn())
}

实现effct嵌套

改变了一个effct函数的代码以及添加了一个effct栈

effct(() => {
    console.log(obj.text)
    effct(() => {
        console.log(obj.isOk)
    })
})
// 这种嵌套上述代码的activeEffect只会是最后一层的 所以要把加一个effctFn栈
const effectBucket = []
function effect(fn) {
  const effectFn = () => {
     // 清除多余的副作用函数
     cleanup(effectFn);
     // 在触发副作用函数时 保证收集的函数是改副作用函数
     activeEffect = effectFn;
     // 把当前状态保存起来
     effectBucket.push(activeEffect)
     fn();
     // 把当前的副作用函数弹出去
     effectBucket.pop()
     // 如果有嵌套就会把activeEffect还原成之前的值 activeEffect就不会错乱
     activeEffect = effectBucket[effectBucket.length - 1]
   }
   // 收集包含副作用函数的set集合
   effectFn.deps = [];
   effectFn();
}

调度执行

effect(() => {
  console.log(obj.foo)
})

obj.foo++

console.log('结束了')

想要输出的结果是 1 结束了 2
 // 让副作用函数异步执行

function effect(fn, option) {
  const effectFn = () => {
    // ...
   }
   // 收集包含副作用函数的set集合
   effectFn.deps = [];
   // 额外属性
   effectFn.option = option // 新增
   effectFn();
}

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

  let deps = new Set(depsMap.get(key));
  // 执行副作用函数
  deps && deps.forEach(fn => {
    // 如果需要调度执行就执行调度函数
    if (fn.option.schedule) { // 新增
      fn.option.schedule(fn)
    } else {
      fn()
    }
  })
}

effect(() => {
  console.log(obj.foo)
}, {
  schedule(fn) {
    console.log(obj.foo)
    setTimeout(fn, 0) // 相当于异步调度
  }
})

栈溢出

如果出现副作用函数中触发了setter函数的情况会出现栈溢出

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

  let deps = depsMap.get(key);
  const depsEffect = new Set()
  // 防止栈溢出 如果当前函数正在执行就不需要继续触发了
  deps.forEach(fn => {
    if (activeEffect == fn) return
    depsEffect.add(fn)
  })
  // 执行副作用函数
  depsEffect && depsEffect.forEach(fn => {
    if (fn.option.schedule) {
      fn.option.schedule(fn)
    } else {
      fn()
    }
  })
}

异步一个周期执行触发一个作用函数

const data = { foo : 1 }
cosnt obj = new Proxy(...)

effect(() => {
    console.log(obj.foo) // 想要输出 1 3 而不是 1 2 3, 之前写的异步调度只能保证调度是异步的
})
obj.foo++
obj.foo++


< ------------ >
// 执行函数去重队列
const jonQueue = new Set()
// 异步
const p = Promise.resolve()
let isFlusing = false

function flushJob (fn) {
  // jonQueue.add(fn)
  if (!isFlusing) {
    isFlusing = true
    p.then(() => {
      jonQueue.forEach(fn => fn())
    }).finally(() => {
      isFlusing = false
    })
  }
}

effect(() => {
  console.log(obj.foo)
}, {
  schedule(fn) {
    // setTimeout(fn, 0)
    jonQueue.add(fn)
    flushJob()
  }
})

计算属性computed

实现计算属性首先得了解计算属性的特点: 计算属性会跟踪函数中所有的响应式数据,如果有数据发生变化就会重新计算。

const data = {foo: 1, bar: 2}
const obj = new Proxy(...)
function computed(getter) {
    let value // 作为值保存
    let dirty = true // 节流
    
    const effectFn = effect(getter, {
        lazy: true,
        schedule() {
            dirty = true 
            
            trigger(obj, 'value') // 用于触发依赖
        }
    })
    
    const obj = {
        get value() {
            if (dirty) {
                dirty = false
                value = effectFn() // 获取getter函数返回结果
            }
            track(obj, 'value') // 用于收集依赖
        }
    }
    
    return obj
} 

const compu = computed(() => obj.foo + obj.bar)
effect(() => console.log(compu.value)) // 更改foo、bar的时候会触发计算属性的副作用函数

watch实现

监听数据变化来执行相应的函数

function watch(getter, Fn, option = {}) {
  // 是ref等类型的响应式
  // getter是函数
  let getterFn = getter
  if (typeof getter !== 'function') {
    getterFn = () => traverse(getter)
  }
  effect(getterFn, {
    schedule() {
      const p = Promise.resolve()
      if (option.flush === 'post') {
        return p.then(() => fn())
      }
      Fn()
    },
  })

  if (option.immediate) {
    Fn()
  }
}

// option 有deep深度监听、immediate初始化的时候执行一次、flush dom前后更新 'post'后 'pre'前 'sync'同步
watch(obj, () => {
  console.log('watch监听到了')
}, {
  immediate: true,
  deep: true,
  flush: 'post'
})

function traverse(value, active = new Set(), deep = false) { // 遍历对象中的每一个属性
  if (typeof value === 'object' && value !== null) {
    active.add(value)
    for(key in value) {
      if (value[key] && active.has(value[key]) && deep) {
        traverse(value)
      }
    }
  }
  return value
}

总结

vue3的响应式系统就大致的实现了,这是我根据vue.js设计与实现中学习的,想着看自己能不能把响应式系统写出来。这只是响应式系统的大致实现,思路应该是类似的。希望能对大家有帮助。