vue2vue3响应式

响应式基础

vue开篇提到了怎么在vue的选项式写法中声明组件状态,就是在对象中写一个data属性,这个属性要是一个函数,这个函数要返回一个对象,返回的对象会被vue在合适的时候调用赋予它响应的能力,然后vue会把这个对象上的属性都放到组件自身上, 我们再讨论接下来的问题之前c,先展示vue2以及vue3是怎么大致实现响应式的, 帮助理解

vue2响应式

vue2实现响应式的思路就是给对象加setter和getter,把这些属性全部挂载到组件实例对象上, 然后给每个属性添加上setter更新值的时候要触发的响应函数就可以实现响应式了,具体看下面这个js例子

javascript 复制代码
class Dep {
  constructor() {
    this.bukets = [];
  }
  addDep(fn) {
    this.bukets.push(fn);
  }

  notify() {
    this.bukets.forEach((fn) => {
      fn.update();
    });
  }
}


//观察者
class Watcher {
  constructor(obj, name, updateCb) {
    this.updateCb = updateCb;
    this.init(obj, name);
  }
  init(obj, name) {
    //把注册函数送出去,注册好响应式
    Dep.target = this;
    obj[name]; // 触发Dep响应,添加进这个watcher者
    this.update();
    Dep.target = null;
  }

  update() {
    this.updateCb();
  }
}


//定义给对象响应式属性
const defineReactive = (obj, key, val) => {
  //为这个属性实例化一个观察者
  const dep = new Dep();
  Object.defineProperty(obj, key, {
    get() {
      //当触发key时,说明要使用这个依赖
      if (Dep.target) {
        dep.addDep(Dep.target);
      }
      return val;
    },
    set(newVal) {
      val = newVal;
      //通知
      dep.notify();
      
    }
  });
};
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #app {
      display: inline-flex;
      column-gap: 10px;
      padding: 10px 12px;
      border-radius: 8px;
      margin: 100px 200px;
      background-color: #f5f5f5;
      cursor: pointer;
      user-select: none;
    }
    #app span {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 20px;
      height: 30px;
      background-color: #ececec;
    }
  </style>
</head>
<body>
  <div id="app">
    <span data-action="sub">-</span>
    <span class="count"></span>
    <span data-action="add">+</span>
  </div>

  <script src="./index.js"></script>
  <script>
    let obj = {};
    defineReactive(obj, "count", 0);
    const countEle = document.querySelector(".count");


    new Watcher(obj, "count", () => {
      countEle.innerText = obj.count;
    });

    document.querySelector("[data-action='sub']").addEventListener("click", () => {
      obj.count--;
    });
    document.querySelector("[data-action='add']").addEventListener("click", () => {
      obj.count++;
    });

  </script>
</body>
</html>

关注我们重点的最开头的四个函数,这就是vue2大致实现响应式的样子,我们可以看到,我们实际上是给data指定的数据使用Object.defineProperty定义了get和set函数, , 然后在初始的时候在get函数里添加上watcher,,在这个属性触发set的时候,我们通知这些watcher使用最新的值进行更新,这就是大致流程, 然后我们再来看看vue3对于响应式是怎么实现的

vue3响应式

javascript 复制代码
let activeFn;

const effect = (fn) => {
  activeFn = fn;
  fn();
  activeFn = null;
};

const buckets = new WeakMap();

const trigger = (target, property) => {
  const depsMap = buckets.get(target);
  if (!depsMap) {
    return ;
  }

  const fns = depsMap.get(property);

  console.log(fns, "fns");
  fns && fns.forEach(fn => fn());
};
const track = (target, property) => {
  let depsMap = buckets.get(target);
  if (!depsMap) {
    buckets.set(target, (depsMap = new Map()));
  }

  let deps = depsMap.get(property);

  if (!deps) {
    depsMap.set(property, (deps = new Set()));
  }

  deps.add(activeFn);
};

const reactive = (data) => {
   return new Proxy(data, {
    set(target, property, newVal, receiver) {
      trigger(target, property);
      return Reflect.set(target, property, newVal, receiver);
    },
    get(target, property, receiver) {
      if (activeFn) {
        console.log(target,property, "target-property");
        track(target, property);
      }
      console.log("触发set");
      return Reflect.get(target, property, receiver);
    }
  });
};
html 复制代码
  <style>
    #app {
      display: inline-flex;
      column-gap: 10px;
      padding: 10px 12px;
      border-radius: 8px;
      margin: 100px 200px;
      background-color: #f5f5f5;
      cursor: pointer;
      user-select: none;
    }
    #app span {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 20px;
      height: 30px;
      background-color: #ececec;
    }
  </style>
</head>
<body>
  <div id="app">
    <span data-action="sub">-</span>
    <span class="count"></span>
    <span data-action="add">+</span>
  </div>

  <script src="./index2.js"></script>
  <script>
    let obj = {count: 0};
    obj = reactive(obj);
    const countEle = document.querySelector(".count");

    
    effect(() => {
      countEle.innerText = obj.count;
    });


    document.querySelector("[data-action='sub']").addEventListener("click", () => {
      obj.count--;
    });
    document.querySelector("[data-action='add']").addEventListener("click", () => {
      obj.count++;
    });

  </script>
</body>

我们可以看到,我们基于Proxy实现的响应式系统是现有一个obj对象, 然后我们定义了一个代理对象,我们后续都是操作这个代理对象去实现响应式更新

总结

基于上述描述,我们可以知道,vue2的响应式的确是在原始对象上定义了一个新的属性然后设置get和set,我们在这个对象属性上触发了set的时候,也会触发响应函数更新, 在vue3的时候,是现有原始的对象,我们给这个对象设置了一个代理对象,后续的响应式都是通过触发代理对象的set和get实现的,在代理对象上触发了set的时候,会触发响应函数更新, 完全与原始对象解耦了。同时也可以注意到,我们在vue2的实现中,并没有return 一个函数或者是包含函数的对象,但是我们的属性val,却因为defineProperty的实现而被留存了下来,通过这种形式也实现了一个闭包,所以我们可以说,没有return一个使用了内部变量的函数就不是闭包的说法是错误的,只要实现了将内部变量外泄到外部代码,并且外部代码只能受控的间接访问这个内部变量的这么个现象,我们就可以认为是一个闭包,return一个使用了内部变量的函数只是实现的一个具体方法。

回到Vue文档

查看下面一个vue文档给出的例子

javascript 复制代码
export default {
  data() {
    return {
      someObject: {}
    }
  },
  mounted() {
    const newObject = {}
    this.someObject = newObject

    console.log(newObject === this.someObject) // false
  }
}

当你在复制后再访问this.someObject, 这个时候因为触发了this的set函数,属性是someObject, 所以在vue3中会创建一个新的响应式对象,然后复制给this.someObject,这个对象是代理后的对象,它的原始对象是newObject, 而对于vue2,它会接受这个对象,然后在这个对象上设置getter和setter,把这个对象转换成响应式 由于转换是在同一个对象上进行的 ,所以文档说当你在赋值后再访问this.someObject, 此值已经是原来的newOject的一个响应式代理,与vue2 不同的是,这里的原始的newObject不会变为响应式,请确保始终通过this来访问响应式状态

声明方法

先看下面一个例子

javascript 复制代码
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    // 在其他方法或是生命周期中也可以调用方法
    this.increment()
  }
}

vue文档在这里说不应该使用箭头函数,因为箭头函数的this值是跟着作用域走了,而在对象中使用 ...() {}, 的形式相当于function () {} ,其中的this是由调用方觉定的,所以这里的methods中的方法使用箭头函数后如果是顶层的箭头函数的this就是window,不会改变

响应式状态新增属性

当我们在vue2的响应式状态上新增一个属性的时候,vue2没有办法检测到变化,查看下面一个例子

javascript 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>数组列表渲染重点</title>
</head>

<body>
  <div id="app">
    {{obj.nested.count}}
    {{JSON.stringify(obj.nested)}}
    <button @click="mutateDeeply">增加</button>
  </div>
  <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.js"></script>
  <script>
    const app = new Vue({
      el: "#app",
      data() {
        return {
          obj: {
            nested: { count: 0 },
          }
        }
      },
      methods: {
        mutateDeeply() {
          // 以下都会按照期望工作
          this.obj.nested.count++
        }
      }
    })
  </script>
</body>

</html>

如果我们在控制台输入app.obj.nested.count2 = 2;可以发现,这个时候我们的页面并没有发生变化,如果我们换成vue3的写法,会怎么样,请查看下面一个例子

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>数组列表渲染重点</title>
</head>

<body>
  <div id="app">
    {{obj.nested.count}}
    {{JSON.stringify(obj.nested)}}
    <button @click="mutateDeeply">增加</button>
  </div>
  <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.22/vue.global.min.js"></script>
  <script>
  const app = Vue.createApp({
  data() {
    return {
      obj: {
        nested: { count: 0 },
      }
    }
  },
  methods: {
    mutateDeeply() {
      // 以下都会按照期望工作
      this.obj.nested.count++
    }
  }
}).mount("#app");
  </script>
</body>

</html>

如果我们在上面的这个例子控制台中实时的添加app.obj.nested.count2 = 2;可以看到,页面发生了变化! 这是为什么呢,其实,vue2的响应是基于definePRoperty,这就意味着vue2在实现响应式的时候在统一注册响应式的阶段在对象的属性上定义setter/getter,这个时候新增一个属性,压根就没有给这个对象赋予一个setter/getter,所以也就不会触发setter/getter了,如果是在vue3中,我们使用代理对象,响应是基于整个对象的,如果你新增了一个属性,这个时候就会触发整个对象的getter/setter,然后更新整个页面,所以最后的区别也还是因为vue2的响应式是基于对象属性的,而vue3的响应式是基于整个对象的,这是我们在响应式系统上讨论的vue3和vue2的第二个区别

相关推荐
Hhang1 小时前
Pageindex -- 新一代的文档智能检索
前端·人工智能
恋猫de小郭2 小时前
Claude Code 已经 100% 自己写代码,为什么 Anthropic 还有上百个工程职位空缺?
前端·人工智能·ai编程
liann1192 小时前
4.3.2_WEB——WEB后端语言——PHP
开发语言·前端·网络·安全·web安全·网络安全·php
是欢欢啊2 小时前
前端纯原生canvas图片裁剪工具,不依赖任何插件
前端
zheshiyangyang2 小时前
前端面试基础知识整理【Day-4】
前端·面试·职场和发展
FunW1n2 小时前
tmf.js Hook Shark框架相关疑问归纳总结报告
java·前端·javascript
武帝为此2 小时前
【Shell 变量作用域详解】
前端·chrome
henry1010103 小时前
Deepseek辅助生成的HTML5网页版抄经典《弟子规》
前端·javascript·css·html·html5
少云清3 小时前
【UI自动化测试】2_web自动化测试 _Selenium环境搭建(重点)
前端·selenium·测试工具·web自动化测试