美文网首页让前端飞Web前端之路前端派
vue3与vue2的区别之数据响应——手写vue3的reacti

vue3与vue2的区别之数据响应——手写vue3的reacti

作者: 超人s | 来源:发表于2021-02-10 01:08 被阅读0次

    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就差不多了
    后面依赖收集触发就是具体到响应后要做的事。

    相关文章

      网友评论

        本文标题:vue3与vue2的区别之数据响应——手写vue3的reacti

        本文链接:https://www.haomeiwen.com/subject/qkzcxltx.html