简陋的实现vue3中reactive, effect

为啥要有响应式

比如我变量a = 2;
然后要让变量b = a +10;
a变化的时候 b 也要变化。如果没有响应式那么要自己调用更新

let a = 2;
let b;

function update(){
  b = a + 10
  console.log('b', b);
}

update()
a=20
update()

这样就很不友好了。vue3提供了reactivity API。我们可以直接下 npm i @vue/reactivity

然后就可以这么写了

const { reactive, effect } = require('@vue/reactivity')

let a = reactive({ value: 2 })
let b;

effect(() => {
  b = a.value + 10
  console.log('b', b);
})

a.value = 20

现在就实现了响应式 ,a的value值变化, b也会自动变化了。

实现简陋的reactivity

  1. 我们需要一个Dep类,这个类用来收集和触发依赖
  2. 然后我们需要一个effectWatch函数, 用来配置Dep类似收集,effectWatch函数就类似于vue3的effect

实现原理
effectWatch 一上来就是执行自己内部函数, 就会触发 变量 a的get访问,而再 Dep中实现,get返回会触发收集依赖(当前的内部函数)。

然后修改变量a ,触发set。又会触发依赖(收集的effectWatch的内部函数执行)

为了让effectWatch与Dep类收集到正确的依赖,会有一个全局变量currentEffect来作为中转收集(vue2中是直接放到Dep类的target属性上)

// 设置一个环境变量 
let currentEffect;

// 依赖 一个类
class Dep {
  constructor(val) {
    this.effects = new Set() // 依赖不能重复收集(Set实现,  集合数据结构 集合是由一种没有重复元素且没有顺序的数组)

    this._val = val
  }
  get value () {
    //*********************** */ 收集依赖
    this.depend()
    return this._val
  }
  set value (newVal) {
    this._val = newVal
    //*********************** 值更新完成之后再去 触发依赖
    this.notice()
  }

  // 1. 收集依赖
  depend () {

    // 判断是否存在环境变量
    if (currentEffect) {
      this.effects.add(currentEffect)
    }

  }
  // 2. 触发依赖
  notice () {
    this.effects.forEach(effect => effect())
  }
}



// 配合收集 依赖
function effectWatch (effect) {

  // 把effect 依赖 存到 环境变量中
  currentEffect = effect
  // 一上来就先调用一次
  effect()
  //*********************** dep.depend()
  // 最后 环境变量置空
  currentEffect = null

}

//  类似于 a = 2
const dep = new Dep(2)

let b;

effectWatch(() => {
  b = dep.value + 10
  console.log('b', b);
})

// 值变更
dep.value = 20

实现vue3的proxy

Dep目前只能监听string,number类型,现在实现对对象的监听

我们传入一个对象object, 在访问object.a的时候会触发get方法, 给object.a = 2 赋值的时候会触发 set方法

vue2 Object.defineProperty与 vue3 proxy 对比

  1. Object.defineProperty的第一个缺陷,无法监听数组变化(vue2改写了数组的七个方法 push,pop等)

  2. bject.defineProperty的第二个缺陷,只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历(深度遍历)

  3. Proxy可以直接监听对象而非属性,并返回一个新对象

  4. Proxy可以直接监听数组的变化

ok,下面我们用 reactive 方法实现
关于 Proxy 与 Reflect 网上有很多了,这里就不概述了

reactive 传入一个对象,在get、set的时候都要调用getDep获取当前的dep。(如果不存在就会调用 Dep类然后保存起来)

function reactive (raw) {
  return new Proxy(raw, {
    get (target, key) {
      console.log('触发get钩子', key);
      // key 对应一个 dep
      // dep  存储在 哪里  

      const dep = getDep(target, key)

      // dep 收集依赖
      dep.depend()

      // return target[key]  Object 的一些明显属于语言内部的方法移植到了 Reflect 对象上
      return Reflect.get(target, key)
    },
    set (target, key, value) {
      console.log('触发set钩子');
      // 触发依赖
      const dep = getDep(target, key)

      const result = Reflect.set(target, key, value)

      dep.notice();

      // 为什么要return 数组是需要返回值的
      return result
    }
  })
}

实现getDep方法

// 一个全局的Map保存 dep
const targetMap = new Map()

function getDep (target, key) {
  let depsMap = targetMap.get(target)

  // 如果不存在 target对象的Map那么保存起来
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  let dep = depsMap.get(key)

  // 如果不存在 key对应的dep, 也保存起来
  if (!dep) {
    dep = new Dep()
    depsMap.set(key, dep)
  }
  // 方法dep
  return dep
}

我理解的是proxy 中 Reflect.get,Reflect.set在实现原来对象的方法的同时,dep.depend,dep.notice 也同时完成了自己想要的动态响应需求。
只要用proxy代理的对象之后,对象的值的方法和触发都会进入get或者set方法中,那么就会触发我们的依赖收集和触发

下面是完整代码。


// 响应式库

// 设置一个环境变量 
let currentEffect;

// 依赖 一个类
class Dep {
  constructor(val) {
    this.effects = new Set() // 依赖不能重复收集(Set实现,  集合数据结构 集合是由一种没有重复元素且没有顺序的数组)

    this._val = val
  }
  get value () {
    //*********************** */ 收集依赖
    this.depend()
    return this._val
  }
  set value (newVal) {
    this._val = newVal
    //*********************** 值更新完成之后再去 触发依赖
    this.notice()
  }

  // 1. 收集依赖
  depend () {

    // 判断是否存在环境变量
    if (currentEffect) {
      this.effects.add(currentEffect)
    }

  }
  // 2. 触发依赖
  notice () {
    this.effects.forEach(effect => effect())
  }
}

// 配合收集 依赖
function effectWatch (effect) {

  // 把effect 依赖 存到 环境变量中
  currentEffect = effect
  // 一上来就先调用一次
  effect()
  //*********************** dep.depend()
  // 最后 环境变量置空
  currentEffect = null

}

// 一个全局的Map保存 dep
const targetMap = new Map()

function getDep (target, key) {
  let depsMap = targetMap.get(target)

  // 如果不存在 target对象的Map那么保存起来
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  let dep = depsMap.get(key)

  // 如果不存在 key对应的dep, 也保存起来
  if (!dep) {
    dep = new Dep()
    depsMap.set(key, dep)
  }
  // 方法dep
  return dep
}

function reactive (raw) {
  return new Proxy(raw, {
    get (target, key) {
      console.log('触发get钩子', key);
      // key 对应一个 dep
      // dep  存储在 哪里  

      const dep = getDep(target, key)

      // dep 收集依赖
      dep.depend()

      // return target[key]  Object 的一些明显属于语言内部的方法移植到了 Reflect 对象上
      return Reflect.get(target, key)
    },
    set (target, key, value) {
      console.log('触发set钩子');
      // 触发依赖
      const dep = getDep(target, key)

      const result = Reflect.set(target, key, value)

      dep.notice();

      // 为什么要return 数组是需要返回值的
      return result
    }
  })
}

module.exports = {effect: effectWatch , reactive}

然后我们再indx.js中引入我们自己写的方法

const {effect , reactive} = require('./core/reactivity3/index.js')

let a = reactive({ value: 2 })
let b;

effect(() => {
  b = a.value + 10
  console.log('b', b);
})

a.value = 20

新增 上面还是用到了 vue2 中的Dep,下面我们精简vue3实现

let targetMap = new WeakMap()
let effectStack = [] //存储 effect 副作用

// 拦截的 get set
const baseHandler = {
  get (target, key) {
    const ret = target[key]// Reflect.get(target, key)

    // 收集依赖 到全局map targetMap
    track(target, key)

    return ret // 如果有递归  typeof ret === 'object' ? reactive(ret): ret;
  },
  set (target, key, value) {
    // 获取 新老值
    const info = { oldValue: target[key], newValue: value }

    target[key] = value // Reflect.set(target, key, value)

    // 拿到收集的 effect ,并执行
    trigger(target, key, info)
  }
}

function reactive (target) {
  const observed = new Proxy(target, baseHandler)

  return observed
}



// 收集依赖
function track (target, key) {
  //初始化
  const effect = effectStack[effectStack.length - 1]

  if (effect) {
    // 初始化
    let depsMap = targetMap.get(target)
    if (depsMap === undefined) {
      depsMap = new Map()
      targetMap.set(target, depsMap)
    }

    let dep = depsMap.get(key)
    if (dep === undefined) {
      dep = new Set() // 防止 重复
      depsMap.set(key, dep)
    }

    // 收集
    if (!dep.has(effect)) {
      dep.add(effect) // 把effect 放到dep 里面 封存
      effect.deps.push(dep) // 双向缓存
    }
  }
}

// 触发依赖
function trigger (target, key, info) {
  let depsMap = targetMap.get(target)
  // 如果没有副作用
  if (depsMap === undefined) {
    return
  }

  const effects = new Set()

  const computeds = new Set() // 一个特殊的effect  懒执行

  // 存储
  if (key) {
    let deps = depsMap.get(key)
    // 可能有多个副作用 effect
    deps.forEach(effect => {
      // 如果有计算 属性
      if (effect.computed) {
        computeds.add(effect)
      } else {
        effects.add(effect)
      }
    })
  }

  //  执行
  effects.forEach(effect => effect())
  computeds.forEach(computed => computed())
}


function computed (fn) {
  const runner = effect(fn, { computed: true, lazy: true }) // 懒执行,开始的时候不用初始执行
  
  return {
    effect: runner,
    get value () {
      return runner()
    }
  }
}

function effect (fn, options = {}) {
  let e = createReactiveEffect(fn, options)
  // 不是懒执行 那么初始化就执行一次
  if (!options.lazy) {
    e()
  }
  return e
}

function createReactiveEffect (fn, options) {
  const effect = function effect(...args){
    return run(effect, fn, args)
  }

  // 函数上挂载 属性
  effect.deps = []
  effect.computed = options.computed
  effect.lazy = options.lazy

  return effect
}

// 真正执行  调度
function run(effect, fn, args){
  //  不存在
  if(effectStack.indexOf(effect) === -1){
    try{
      //  把副作用 存储到 effectStack 中
      effectStack.push(effect)
      return fn(...args)
    }finally{
      //  最后 把执行后的副作用  pop掉
      effectStack.pop()
    }    
  }
}


module.exports = {effect , reactive, computed}

computed 不会一上来就执行 副作用 ,要等到 调用获取返回值的时候才会执行

const {effect , reactive, computed} = require('./core/reactivity4/index.js')

let a = reactive({ value: 2 })
let b;

// 不会执行副作用  只有等 访问的时候 执行track 和trigger
const comp = computed(() => a.value * 100)
// const c = a.value *2
// const comp = { value: a.value * 100 }

// 执行副作用  就会触发 track 访问 收集依赖, 改变的时候 就会触发依赖
effect(() => {
  b = a.value + 10
  console.log('b', b);
  console.log('comp', comp.value);
})


// console.log('comp1', comp.value); // 200
// console.log('c', c); // 4
a.value = 20
// console.log('comp1', comp.value); // 2000
// console.log('c', c); // 4
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容