Vue响应式原理

首先,Vue3 与 Vue2 采用了不同的实现方式,option api 将所有的属性与方法全部挂在在 this 中,造成了类型推断困难,而 composition api 重新用 TypeScript 以及 ES6 中的 Proxy 和 Reflect 编写改进,

Effect

响应性的本质实际上是发布订阅设计模式,我们需要在变量发生改变时执行一次 effect,也就是变量发生改变时所造成的影响。

let a = 0
let b = 1

let sum

function effect() {
  sum = a + b
}
effect()
console.log(sum)    // 1
a = 3
effect()
console.log(sum)    // 4
复制代码

此时我们手动执行了 effect 函数,但是我们希望 effect 函数是自动执行的,每当我们更改了 effect 中的某个变量 effect 就重新执行,为了实现这个目标,我们引入了 Proxy。

Proxy

我们新建一个 reactive 函数,这个函数接受一个 target 变量,返回一个被 Proxy 封装后的对象,每次更改对象中的值时,都会调用一次 effect 函数重新计算结果

function reactive(target) {
  let handler = {
    set(target, key, value) {
      target[key] = value
      effect()
    },
  }
  return new Proxy(target, handler)
}

function effect() {
  sum = obj.a + obj.b
}

let obj = reactive({
  a: 0,
  b: 1,
})
let sum

effect()
console.log(sum)  // 1
obj.a = 3
console.log(sum)  // 4
复制代码

到此为止本质上来说我们已经实现了最基本的响应式,但是我们当前的响应式只能执行同一个函数,当我们需要使用别的函数时只能修改 reactive 内部 set 函数执行的内容。为了使我们的响应式可以自定义需要执行的函数,且可以针对不同的变量有不同的执行函数,我们开始维护一些全局变量。

deps和depsMap

我们会在 reactive handler 的 get 中保存 effect,在 set 中执行所有的 effect。

const depsMap = new Map()
let activeEffect = null

function reactive(target) {
  let handler = {
    get(target, key, receiver) {
      if (activeEffect) {
        let dep = depsMap.get(key)
        if (!dep) {
          depsMap.set(key, (dep = new Set()))
        }
        dep.add(activeEffect)
      }
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      let result = Reflect.set(target, key, value, receiver)
      let dep = depsMap.get(key)
      dep.forEach((effect) => effect())
      return result
    },
  }
  return new Proxy(target, handler)
}

function effect(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}

let obj = reactive({
  a: 5,
  b: 1,
})
let sum, product

effect(() => {
  sum = obj.a + obj.b
})
effect(() => {
  product = obj.a * 3
})

console.log('sum', sum)   // sum 6
console.log('product', product)   // product 15

obj.a = 3
console.log('sum', sum)   // sum 4
console.log('product', product)   // product 9
复制代码

在上面的代码中,我们做了如下修改

  1. 新增了depsMap集合,depsMap中保存了所有属性的对应的要执行的effect Set集合;
  2. 新增了一个activeEffect全局变量,这个变量中保存着现在运行的effect;
  3. 修改effect函数,下面我们给出effect函数的执行过程

当我们修改reactive包裹的对象的值时,会调用Proxy.handler.set方法,我们先将target[key]修改,接着运行所有的effect(这里有优化空间,我们将在下面讲述)

我们在这里使用了ES6中的Reflect.get()Reflect.set()方法,目的是在存在继承时可以有正确的this指向,详情参考 在es6 Proxy中,推荐使用Reflect.get而不是target[key]的原因

到此为止,我们已经完成了大部分的响应式的内容,但是我们可以注意到我们现在仅仅有一个reactive对象,当我们拥有多个reactive对象,且这些reactive对象中拥有相同的属性时,会出现一些错误,为了解决这个问题,我们在这里加入targetMap(weakMap),用来保存不同对象的depsMap

targetMap,track和trigger

我们首先封装tracktrigger函数

function track(target, key) {
  if (activeEffect) {
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    dep.add(activeEffect)
  }
}

function trigger(target, key) {
  let dep = depsMap.get(key)
  dep.forEach((effect) => effect())
}
复制代码

接下来我们将depsMap移入targetMap

const targetMap = new WeakMap()
let activeEffect = null

function reactive(target) {
  let handler = {
    get(target, key, receiver) {
      track(target, key)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      let result = Reflect.set(target, key, value, receiver)
      trigger(target, key)
      return result
    },
  }
  return new Proxy(target, handler)
}

function track(target, key) {
  if (activeEffect) {
    // 将depsMap移入targetMap中,我们不在将depsMap作为一个全局变量
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }

    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    dep.add(activeEffect)
  }
}

function trigger(target, key) {
  let depsMap = targetMap.get(target)
  if (depsMap) {
    let dep = depsMap.get(key)
    if (dep) dep.forEach((effect) => effect())
  }
}

function effect(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}
复制代码

到这里基本就要大功告成啦!我们将depsMap移入了targetMap中,如果不存在就新建并且将其放入targetMap。

最后我们再进行一点点优化

const targetMap = new WeakMap()
let activeEffect = null

function reactive(target) {
  let handler = {
    get(target, key, receiver) {
      track(target, key)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      // 保存oldValue
      let oldValue = target[value]
      let result = Reflect.set(target, key, value, receiver)
      // 当oldValue和新的值不同时,执行所有的effect
      if (oldValue !== value) trigger(target, key)
      return result
    },
  }
  return new Proxy(target, handler)
}

function track(target, key) {
  if (activeEffect) {
    // 将depsMap移入targetMap中,我们不在将depsMap作为一个全局变量
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }

    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    dep.add(activeEffect)
  }
}

function trigger(target, key) {
  let depsMap = targetMap.get(target)
  if (depsMap) {
    let dep = depsMap.get(key)
    if (dep) dep.forEach((effect) => effect())
  }
}

function effect(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}
复制代码

在Proxy.handler.set中,我们加入了对过去值和当前值的判断,当且仅当过去的值和当前修改的值不同时才执行dep中的effect函数

最后的最后,我们再来实现一个ref

ref

function ref(raw) {
  const r = {
    get value() {
      track(r, 'value')
      return raw
    },
    set value(newValue) {
      let oldValue = this.value
      raw = newValue
      if (oldValue !== newValue) trigger(r, 'value')
    },
  }
  return r
}
复制代码

有了前面的铺垫,ref的实现就变得简单了起来,由于在Vue中ref包裹的变量都需要通过.value访问,我们采用了对象的属性访问器gettersetter来实现对于value的访问。属性访问器的功能类似于前面reactive中使用的Proxy,每当获取值的时候执行track保存当前的activeEffect,设置值的时候判断是否和之前的值相等,如果不相等的话执行trigger

好了,以上所有就是Vue响应式的基本原理了,本文章主要来源于学习Vue Master的一些笔记和总结,希望对大家有所帮助。

本文使用 文章同步助手 同步

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

推荐阅读更多精彩内容