浅析vue的响应性

简言

这次我们来浅析下vue3的响应性原理。

话不多说,不玩虚的,let’s go! ——ZSK666

观察者模式

在探究vue3的响应性原理之前,我们来了解一种设计模式:观察者模式。有人也会叫它发布-订阅者模式,反正设计原则都一样,是一对多的处理方式,当多个事物(观察者)依赖一个事物(被观察者)时,它们就建立了一对多的关系,那一个事物(被观察者)改变,依赖它的多个事物(观察者)也得发生改变,如果观察者很多的时候,会比较混乱,需要一个有人对它们的依赖关系进行管理调度(管理中心)。
下面是简单实现:

<template>
  <div>
    <h1>{{ a }}</h1>
    <div>{{ a }} + {{ b }} = {{ result }}</div>
  </div>
</template>
<script lang="ts">
interface watch {
  data: Array<Function>
  push: Function
  update: Function
}

class center implements watch {
  //  存储要触发的事件
  data = [] as Array<Function>
  constructor() {}
  push(fn: Function) {
    this.data.push(fn)
  }
  update() {
    this.data.forEach((fn) => fn())
  }
}

//  简单使用
let watchCenter = new center()
let a = 1
let b = 1
let result = a + b
//先改变a
watchCenter.push(() => {
  a = 2
})
//  然后将result重新算一遍
watchCenter.push(() => {
  result = a + b
})
//  更新
watchCenter.update()

vue2的响应性

上面的设计模式好用是好用,但是总是自己调用太麻烦了。有没有更改值后自己调用的?哎,正好有,那就是Object.defineProperty()。Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。这个方法支持修改对象的get和set方法,如果我们在get方法,推送到数组中,set时更新数组;就达到修改值,依赖它的值自动更新的效果,这就是数据劫持(Observe)。如果有多个地方依赖这个数据变化而变化,就需要将观察者模式细分优化一下,一分为二,一个只做监听和操作,有几个依赖数据变化的地方添加几个实例(watcher),另一个当作中转管理中心(Dep),管理依赖这个数据的所有实例,一旦数据改变统一调用所有实例。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>dom响应式</title>
</head>

<body>
  <div>
    <div id="a"></div>
    <div id="b"></div>
  </div>


  <script>
    // watcher
    class Watcher {
      constructor(vm, key, cb) {
        this.cb = cb
        this.vm = vm
        this.key = key
        //  创建的watcher实例存到Dep实例的subs数组中
        Dep.target = this
        console.log(this);
      }
      update() {
        //  我们定义一个cb函数,这个函数用来模拟视图更新,调用它即代表更新视图
        console.log('更新数据');
        this.cb()
      }
    }
    // dep 
    class Dep {
      constructor() {
        //  存放watcher
        this.subs = []
      }
      //  添加
      addSub(sub) {
        console.log('添加watcher');
        this.subs.push(sub)
      }
      //  更新
      notify() {
        this.subs.forEach(sub => {
          sub.update()
        })
      }
    }


    //  数据劫持
    function observe(obj) {
      console.log(obj);
      if (!obj || typeof obj !== 'object') {
        return
      }

      Object.keys(obj).forEach((key) => {
        let value = obj[key]
        observe(value)
        //  数据劫持
        //  实例化一个Dep对象
        const dep = new Dep()
        Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get() {
            console.log(key, '获取数据');
            if (Dep.target) {
              //  添加dep管理watcher
              dep.addSub(Dep.target)
              Dep.target = null
            }
            return value
          },
          set(newVal) {
            if (newVal === value) return null
            //  赋新值
            value = newVal
            observe(value)
            console.log('重新赋值', newVal, dep);
            //  触发更新
            dep.notify()
          }
        })

      })
    }

    const data = {
      a: 5,
      b: 0
    }
    observe(data)
    const Doma = document.querySelector('#a')
    const Domb = document.querySelector('#b')
    //  赋初始值
    Doma.innerHTML = data.a
    Domb.innerHTML = data.b
    //  new 观察者a
    new Watcher(Doma, 'a', () => {
      Doma.innerHTML = data.a

      console.log('DomA更新');
    })
    //  打印一下,添加监听
    console.log(data.a);
    console.log(data.b);
    //  更新a
    data.a = 10
    //  new 观察者b

    new Watcher(Domb, 'b', () => {
      Domb.innerHTML = data.a + data.b
      console.log('DomB更新');
    })

    //  改变值模拟改变
    setTimeout(() => {
      //  改变b
      data.b = data.a + 100

    }, 1000)
    setTimeout(() => {
      //  再次改变a,然后Domb也会改吧
      data.a = 100

    }, 2000)

  </script>
</body>

</html>

vue3响应性

为啥要vue2和vue3响应性要单独说呢?因为vue3的响应性原理依赖的方法变了。vue2使用的不是挺好的嘛,为啥变了呢?因为vue2使用的Object.defineProperty()有局限性:1.只能让对象做响应性,且对象半路如果添加新属性或者删除就属性,无法监听到变化(因为更新视图是根据属性变化来监听的)2。js数组的方法也不能监听到,只有部分方法可以重写方法才可监听,如果使用数组下标方式改变值或使用length修改数组长度也无法更新。
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。jsES6的Proxy正好可以弥补老方法的缺陷,而且方式还比较精简。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue3响应式</title>
</head>

<body>
  <div id="a"></div>
  <div id="b"></div>
  <button class="btn">a+1</button>
  <script>
    function reactive(obj) {
      const proxy = new Proxy(obj, {
        get(target, prototype, receiver) {
          console.log('获取', target[prototype]);
          return target[prototype]
        },
        set(target, prototype, value, receiver) {
          console.log('设置', value);
          target[prototype] = value
          update()

          return true
        }
      })
      return proxy
    }
    const obj = reactive(
      {
        a: 1
      }
    )
    //设置触发更新
    obj.a = 10
    //  更新函数
    function update() {
      const Doma = document.getElementById('a')
      const Domb = document.getElementById('b')
      Doma.innerHTML = obj.a
      Domb.innerHTML = obj.a + 'hello World!'
    }
    const btn = document.querySelector('.btn')
    btn.addEventListener('click', function () {
      //  加一触发更新
      obj.a += 1
    }) 
  </script>
</body>

</html>

proxy也有缺陷:1.比较新,有一定的兼容问题。2.代理返回的proxy对象和原对象不全等。

vue3响应性 + 观察者模式

上面的代码当然傻眼,,,我特啊油弄啥嘞?你搞笑呢?上面的代码还是自己调用更新啊,那个update函数都跟reactive函数强耦合啥样了,赶紧给我改,改个,一调用reative函数就可以的!
哎嘿,别急,知道你很着急,但是别先急,上面的代码确实没法看,但是有了响应性雏形,如果加上观察模式,直接原地起飞。

<!--
 * @Date: 2023-10-27 14:51:55
 * @LastEditors: zhangsk
 * @LastEditTime: 2023-10-27 17:56:31
 * @FilePath: \js\aaa.html
 * @Label: Do not edit
-->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue3响应式</title>
</head>

<body>
  <div id="a"></div>
  <div id="b"></div>
  <button class="btn">a+1</button>
  <script>
    /*
      观察者模式
    */
    let activeEffect;
    const reactiveMap = new WeakMap()
    function reactiveEffect(fn) {
      return {
        deps: [],
        run: function () {
          // 
          if (!Object.is(this, activeEffect)) {
            activeEffect = this
          } else {
            //  清掉
            activeEffect = undefined
          }

          return fn()
        }
      }
    }
    //  存储相关依赖函数
    const targetMap = new WeakMap()
    //  收集依赖函数
    function track(target, key) {
      let depsMap = targetMap.get(target)
      if (!depsMap) {
        depsMap = new Map();
        targetMap.set(target, depsMap)
      }
      let dep = depsMap.get(key)
      if (!dep) {
        dep = new Set()
        depsMap.set(key, dep)
      }
      if (activeEffect && !dep.has(activeEffect)) {

        //  添加
        dep.add(activeEffect)
        //  新建好加到里面
        activeEffect.deps.push(dep)
      }

    }
    //  触发依赖函数
    function trigger(target, type, key, newValue, oldValue) {
      const depsMap = targetMap.get(target)
      if (!depsMap) return
      //  set
      //  获取收集到得依赖
      let effects = depsMap.get(key)
      //遍历set
      for (let effect of effects) {
        //  触发运行函数

        effect.run()

      }


    }
    //  更新函数在这
    function effectFn(fn) {
      const _effect = reactiveEffect(fn)
      _effect.run()

    }
    /**
     *  响应式函数
     * */
    function reactive(obj) {
      //  ...
      //  啥边界都不管(假装obj是普通对象),直接开造
      const proxy = new Proxy(obj, {
        //  获取值
        get(target, prototype, receiver) {

          //  Reflect.get() 方法与从 对象 (target[propertyKey]) 中读取属性类似,但它是通过一个函数执行来操作的。
          const res = Reflect.get(target, prototype, receiver)
          //  收集
          track(target, prototype)
          //  添加好后及时清理初始化,不然有bug
          if (activeEffect && activeEffect.deps[activeEffect.deps.length - 1].has(activeEffect)) {
            activeEffect = undefined
          }
          return res
        },
        //  设置值
        set(target, prototype, value, receiver) {
          //  旧值
          const oldValue = target[prototype]
          const result = Reflect.set(target, prototype, value, receiver)

          //  这个不行啊,不能直接调,
          // update()
          //  间接调用,从 target为key得依赖map值上调用update函数
          if (value !== oldValue)
            trigger(target, 'set', prototype, value, oldValue)

          return result
        }
      })
      //  加一个
      reactiveMap.set(obj, proxy)
      return proxy
    }
    const obj = reactive(
      {
        a: 1
      }
    )
    //设置触发更新
    obj.a = 10
    //  更新函数
    function update() {
      const Doma = document.getElementById('a')
      const Domb = document.getElementById('b')
      Doma.innerHTML = obj.a
      Domb.innerHTML = obj.a + 'hello World!'
    }
    effectFn(update)
    const btn = document.querySelector('.btn')
    btn.addEventListener('click', function () {
      //  加一触发更新
      obj.a += 1

    })
    effectFn(() => {
      console.log('a更新了', obj.a);
    })
    const obj2 = reactive({
      b: 3
    })
    effectFn(() => {
      console.log('obj2有更新', obj2.b);
    })
    setTimeout(() => {
      obj2.b = 5
    }, 1000)

    effectFn(() => {
      console.log('没有使用响应性数据,假装什么都没发生1');
    })
    effectFn(() => {
      console.log('没有使用响应性数据,假装什么都没发生2');
    })
  </script>
</body>

</html>

结果
在这里插入图片描述
芜湖起飞,简单数值响应性属性运行良好

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZSK6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值