前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >vue3与vue2的区别之数据响应

vue3与vue2的区别之数据响应

原创
作者头像
conanma
修改2021-11-03 13:14:02
5120
修改2021-11-03 13:14:02
举报
文章被收录于专栏:正则正则

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等方法。从而能够拦截到对象属性的访问和变更。

代码语言:javascript
复制
/**
 * 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中,定义响应式对象的方法如下:

代码语言:javascript
复制
const obj = reactive({
  name: 'chenjing',
  age: 18
})

3.1 测试Proxy是否生效

代码语言:javascript
复制
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就是下面这样的:

代码语言:javascript
复制
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中,需要触发响应式函数。即完成了发布订阅。

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

代码语言:javascript
复制
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数据响应原理。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、 数据响应式
  • 2、vue如何实现数据响应式?
    • 2.1 vue2.x的实现方案
      • 2.2 vue3.x的实现方案
      • 3、手写reactive
        • 3.1 测试Proxy是否生效
          • 3.2 reactive基本形态
            • 3.3 依赖的收集、触发
            • 4. 结尾
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档