vue3与vue2的区别之数据响应——手写vue3的reactive,理解vue3数据响应式原理

1、 数据响应式

首先请大家认真的思考一个问题:什么是数据响应式

答:数据变化是可侦测的,并且和数据相关的内容可以更新。

️这里一定要明确一个概念,数据响应式和视图更新是没有关系的!数据响应式是一种机制,一种数据变化的侦测机制。而实现数据响应式这种机制的方法不唯一。
那么,vue是如何实现数据响应式的?vue2和vue3的数据响应式有什么区别?

2、vue如何实现数据响应式?

要知道,vue3.x实现数据响应的方案跟vue2.x是不一样的,所以在这里我将vue2.xvue3.x分别说说。这也是理解vue2.xvue3.x区别的时候,可以指出来的一个巨大的区别。

2.1 vue2.x的实现方案

我贴上一个vue2.x源码-Object的变化侦测解读的链接,方便大家理解和后续关于vue2.x的学习需要。
(特别是还没阅读过vue源码的同学,可以独自过一遍这个文档,能对vue有一个更深的认识)

在下面vue2的源码中可以看到,Observer类会通过递归的方式把一个对象的所有属性都转化成可观测对象,所以我们可以知道vue2需要遍历对象的所有的key。其实现数据响应式的核心思想就是通过defineProperty,去定义getset等方法。从而能够拦截到对象属性的访问和变更。

/**
 * Observer类会通过递归的方式把一个对象的所有属性都转化成可观测对象
 */
export class Observer {
  constructor (value) {
    this.value = value
    // 给value新增一个__ob__属性,值为该value的Observer实例
    // 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作
    def(value,'__ob__',this)
    if (Array.isArray(value)) {
      // 当value为数组时的逻辑
      // ...
    } else {
      this.walk(value)
    }
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}
/**
 * 使一个对象转化成可观测对象
 * @param { Object } obj 对象
 * @param { String } key 对象的key
 * @param { Any } val 对象的某个key的值
 */
function defineReactive (obj,key,val) {
  // 如果只传了obj和key,那么val = obj[key]
  if (arguments.length === 2) {
    val = obj[key]
  }
  if(typeof val === 'object'){
      new Observer(val)
  }
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get(){
      console.log(`${key}属性被读取了`);
      return val;
    },
    set(newVal){
      if(val === newVal){
          return
      }
      console.log(`${key}属性被修改了`);
      val = newVal;
    }
  })
}

在日常开发中,产品经理总是会跟我们说,我们做了xxxx就是为了解决客户的xxxx痛点。
那么,在继续往下阅读的时候,可以先思考一下vue2这样的实现方案的痛点有什么?或者说缺点有什么?
因为作为客户(使用vue开发的前端同学)的我们需要知道,vue3是否解决了我们的痛点?

vue2的缺点:(仅仅是关于数据响应造成的缺点哦!)

  • 1、影响初始化速度、数据过大时的资源问题
    (在源码的Observer方法上,对象的每一个属性都要被拦截。所有的key都要有一次循环和递归)
  • 2、数组的特殊处理,导致其修改数据不能使用索引
    (原因在于defineProperty不支持数组,参考vue源码-Array的变化侦测
  • 3、动态添加或删除对象属性无法被侦测
    defineProperty哭着对我说:臣妾的的setter函数办不到呀)

对于没阅读过vue源码的前端开发来说,应该也遇到过修改了数组,或者修改对象后发现,啥变化也没有,一头雾水,拍桌子直呼:vue真垃圾,有bug。
其实这些雾水大都是上面的2、3两点引发的,vue也都提供了解决方案:$set$delete,我都整理好了,需要理解的直接移步深入响应式原理
但是,这就体验极差

🤣小故事一则:去年还没阅读源码的时候,公司一个大版本的发布后,出现了一个不是很严重,却影响使用范围很广的一个bug,我们从凌晨2点修到4点,最后还是一个大牛搞了几轮实验发现了问题,说vue有bug,某某地方赋值需要用$set。没错,就是上面痛点里的第3点。原因还是我们太菜呀,没有阅读相关源码。

2.2 vue3.x的实现方案

文章开头我就强调了:数据响应式是一种机制,一种数据变化的侦测机制。而实现数据响应式这种机制的方法不唯一。于是乎,vue3.x来了,他带着vue2.x痛点的解决方案来了!

解决方案其实一点也不神秘,在ES6之后,出现了一个新的特性:ProxyVue3.x在使用了Proxy之后,痛点们一下子就全都解决了。Proxy是怎么解决的呢?请听下回...请继续往下看哈看完手写reactive之后,就全都明白啦。
顺便给个Proxy的MDN地址: Proxy MDN传松门

3、手写reactive

在vue3.x中,定义响应式对象的方法如下:

const obj = reactive({
  name: 'chenjing',
  age: 18
})

3.1 测试Proxy是否生效

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      console.log('target, key', target, key, target[key])
      return target[key]
    })
}

proxy-get.png

ok,生效。在简易版的reactive,我们要添加基本的属性getsetdeleteProperty。同时,在上面代码的get里直接return target[key],一来不太优雅、二来可能报错。我们先来看看vue3是怎么处理的:
vue3源码图1.png

再来一个传送门:Reflect - MDN

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。
与大多数全局对象不同Reflect并非一个构造函数,所以不能通过new运算符对其进行调用,或者将Reflect对象作为一个函数来调用。Reflect的所有属性和方法都是静态的(就像Math对象)。
Reflect 对象提供了以下静态方法,这些方法与proxy handler methods的命名相同.
其中的一些方法与 Object相同, 尽管二者之间存在 某些细微上的差别 .

3.2 reactive基本形态

让我们来学习一下vue3的写法后,加上了Reflect后,于是我们最基本的reactive就是下面这样的:

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      const res = Reflect.get(target, key) // 可以直接return target[key],避免报错和代码的优雅性,模仿源码采用Reflect
      console.log('get', key)
      return (typeof res === 'object') ? reactive(res) : res // 子属性若是对象 需要再次代理
    },
    set(target, key, val) {
      const res = Reflect.set(target, key, val)
      console.log('set', key)
      return res
    },
    deleteProperty() {
      const res = Reflect.deleteProperty(target, key)
      console.log('deleteProperty', key)
      return res
    }
  })
}

reactive基本形态.png

通过跑脚本后的控制台,可以看到访问属性成功的触发了get。同时新增属性也触发了set
到这里为止,vue2中的数据响应式在vue3里其实已经完全实现了。回过头来想想,是不是没那么难理解了吧。没有vue2的循环遍历递归,只是上了Proxy的车
当然了在Vue3内真正的实现,肯定不是这么几行代码就搞定的。只是响应式的原理就是利用了Proxy

既然要手写实现一个简易的reactive函数,让我们继续往下阅读。
目前只是想简单理解vue3数据响应式原理,了解vue3数据响应和vue2数据响应的区别的同学可以直接点赞了哈哈,鼓励一下互相学习进步😁

3.3 依赖的收集、触发

既然要手写实现一个简易的reactive函数,我们就继续。
要实现reactive函数,我们就要在get内进行依赖收集,在set中进行触发。即便是vue2也是通过类似的发布订阅模式体现。在这里,我们也是通过发布订阅模式去完成。

首先是依赖收集:在get内,我们需要对依赖进行收集。在依赖收集的时候,将其按照依赖关系放入map中映射。
然后就是依赖触发:在set中,需要触发响应式函数。即完成了发布订阅。

下面代码 有需要的可以直接复制粘贴,直接跑。可以自行断点看看,有疑问的欢迎交流。

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      const res = Reflect.get(target, key)
      console.log('get', key)
      // 依赖收集
      track(target, key)
      return (typeof res === 'object') ? reactive(res) : res
    },
    set(target, key, val) {
      const res = Reflect.set(target, key, val)
      console.log('set', key)
      // 触发
      trigger(target, key)
      return res
    },
    deleteProperty() {
      const res = Reflect.deleteProperty(target, key)
      console.log('deleteProperty', key)
      return res
    }
  })
}

// 保存副作用函数
const effectStack = []
// 添加副作用函数
function effect (fn) {
  const e = createReactiveEffect(fn)

  // 立即执行
  e()
  return e
}

function createReactiveEffect(fn) {
  // 封装fn,处理其错误,执行之,存放到stack
  const effect = () => {
    try {
      // 0入栈
      effectStack.push(effect)
      // 1 执行fn
      return fn()
    } finally {
      // 2 出栈
      effectStack.pop
    }
  }
  return effect
}

// 保存映射关系的数据结构
const targetMap = new WeakMap()

// 当副作用函数触发响应式数据之后,执行track,进项依赖收集工作
// 目标是将target, key和前面effectStack中的副作用函数之间建立映射关系
function track (target, key) {
  // 1.先拿出响应函数
  const effect = effectStack[effectStack.length - 1]
  if (effect) {
    // 获取target对应的map
    let depMap = targetMap.get(target)
    if (!depMap) {
      // 初始化的时候 depMap不存在 初始化一次
      depMap = new Map()
      targetMap.set(target, depMap)
    }

    // 从depMap中 获取对应的set
    let deps = depMap.get(key)
    if (!deps) {
      // 初始化需要创建一个Set
      deps = new Set()
      depMap.set(key, deps)
    }

    // 将副作用函数放到集合中
    deps.add(effect)
  }
}

// 触发响应式函数
function trigger (target, key) {
  // 从targetMap中获取对应副作用函数集合
  // 1. 获取target对应的map
  const depMap = targetMap.get(target)
  if (!depMap) return

  // 根据key获取对应的deps
  const deps = depMap.get(key)
  if (deps) {
    // 遍历执行他们
    deps.forEach(dep => dep())
  }
}
const obj = reactive({
  name: 'chenjing',
  age: 18,
  look: {
    height: '180cm'
  }
})
effect(() => {
  console.log('effect1', obj.name)
})
effect(() => {
  console.log('effect2', obj.name, obj.look.height)
})

setTimeout(() => {
  console.log('----  分割线   -----')
  obj.name = 'jay'
  obj.look.height = '178cm'
}, 1000)
执行结果.png

4. 结尾

好了,到此手写简易版vue3的reactive函数完成,希望可以帮助到打击爱理解vue3数据响应原理。

单纯的理解数据响应原理可以理解到Proxy就差不多了
后面依赖收集触发就是具体到响应后要做的事。

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

推荐阅读更多精彩内容