vue2/vue3手写专题——实现双向绑定/响应式拦截/虚拟DOM/依赖收集

目录

vue双向绑定

请手动实现一个简单的双向绑定功能,要求实现以下功能:

1.使用原生javaScript

2.使用vue非v-model方式实现

 思考:vue为什么要做双向绑定?

  虚拟DOM/Render函数

将给定html片段写出Virtual Dom结构、并尝试挂载到页面上的root节点

 1.用javascript表示虚拟DOM

2.将虚拟DOM渲染为真实DOM

vue响应式

vue2:手动实现一个简化版的 reactive 函数,用来实现对象响应式的劫持

1.实现observer和defineReactive

 2.效果演示

3.回顾源码

follow up:在上一题的基础上,简单的实现vue2对数组的拦截方法

1.代码实现

 2.效果演示

3.回顾源码

vue3:手动实现一个简化版的vue3响应式

前置知识:vue3响应式和Proxy对象使用

 1.实现handler和reactive

 2.效果演示

 3.Proxy和defineProperty的区别

依赖收集

设计一个vue2的依赖收集系统

 Dep类

Watcher类

Observer方法和defineReactive方法

数据测试


vue双向绑定

请手动实现一个简单的双向绑定功能,要求实现以下功能:

  1. 有一个输入框和一个显示框,输入框用于输入内容,显示框用于展示输入框中的内容。
  2. 当输入框中的内容发生变化时,显示框中的内容应该实时更新。

思路:分析题目,一个输入框和显示框。输入的信息要显示在显示框内。所以要对输入框的input操作进行监听,并修改显示框的信息。下面从原生js和vue非v-model方式实现数据和视图的双向绑定。

1.使用原生javaScript

html部分

我们知道原生js对dom的操作都要先用getElementById或其他的方式拿到dom。给dom添加事件监听,从而操作dom。因此,html先定义一个输入框input和一个显示框div。并且给两个元素添加id信息

    <input type="text" id="input" placeholder="输入内容:input" />
    <div id="output">我将会input覆盖</div>

 js部分

  在js部分,先获取dom,然后通过addEventListener添加input的事件监听。之后修改dom属性,将output.textContent显示文本区域修改为input.value

<script>
  //拿到input和output的dom信息
  const input = document.getElementById("input");
  const output = document.getElementById("output");
  //给input添加事件监听
  //第一个参数是"input",表示输入时触发。如果监听失去焦点的可以使用"blur"
  input.addEventListener("input", () => {
    output.textContent = input.value; //将input输入框信息赋值给output
  });
</script>

效果如下

完整代码

<!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>
    <input type="text" id="input" placeholder="输入内容:input" />
    <div id="output">我将会input覆盖</div>
  </body>
</html>

<script>
  //拿到input和output的dom信息
  const input = document.getElementById("input");
  const output = document.getElementById("output");
  //给input添加事件监听
  //第一个参数是"input",表示输入时触发。如果监听失去焦点的可以使用"blur"
  input.addEventListener("input", () => {
    output.textContent = input.value; //将input输入框信息赋值给output
  });
</script>

2.使用vue非v-model方式实现

视图到数据

通过vue提供的@input事件,调用handleInput方法来更新inputValue数据,这样当输入框的值发生变化时,inputValue数据也会随之更新,实现了视图到数据的绑定。视图改变数据。

<template>
  <div>
    <input @input="handleInput" />
    <div>{{ inputValue }}</div>
  </div>
</template>
<script setup>
import { ref } from "vue";
let inputValue = ref("");
function handleInput(event) {
  inputValue.value = event.target.value;
}
</script>

数据到视图

通过vue提供的v-bind:value方法绑定数据,由于v-bind可以省略,这里直接使用:value

使用:value,当inputValue发生变化时,输入框的值也会随之改变

<template>
  <div>
    <input :value="inputValue" @input="handleInput" />
    <div>{{ inputValue }}</div>

  </div>
</template>
<script setup>
import { ref } from "vue";
let inputValue = ref("");
function handleInput(event) {
  inputValue.value = event.target.value;
}

</script>

 其实这里看不出来数据让视图改变。在增加一个输入框专门改变数据

可以看到第二个输入框改变了inputValue的数据,由于第一个input使用了:value=inputValue,所以第一个也跟着更新了。即数据变换,视图更新

 

完整代码

<template>
  <div>
    <input :value="inputValue" @input="handleInput" />
    <div>{{ inputValue }}</div>
    <input @input="alterInput" />
  </div>
</template>
<script setup>
import { ref } from "vue";
let inputValue = ref("");
function handleInput(event) {
  inputValue.value = event.target.value;
}
function alterInput(event) {
  inputValue.value = event.target.value;
}
</script>

 思考:vue为什么要做双向绑定?

从原生js手写实现实时显示一个输入框的信息来看。用原生的js要操作多个dom,通过getElementById拿到dom,然后手动添加addEventListener,将输入的信息回显出来。处理一个数据的回显尚且如此,如果页面数据很多呢。岂不是要统统操作dom,手动绑定事件?

vue的双向绑定做了什么?

双向绑定的核心思想是数据和视图之间的自动同步,即当数据发生变化时,视图会自动更新;当视图发生变化时,数据也会自动更新。

 为什么要有双向绑定?

Vue之所以引入双向绑定的概念,是为了简化开发者在处理数据和视图之间的同步关系时的工作量,提高开发效率。一切技术的革命都是为了少写代码。这也是为什么常说vue只关心数据了。

  虚拟DOM/Render函数

 考察真实DOM到虚拟DOM的映射关系,虚拟DOM的渲染为真实DOM的原理

将给定html片段写出Virtual Dom结构、并尝试挂载到页面上的root节点

<ul>
	<li>1</li>
	2
	<li>
		<p @click=()=>alert("选中P标签啦")>3</p>
	</li>
</ul>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

 1.用javascript表示虚拟DOM

分析题目:从给定的html写出虚拟dom的js表示形式。html有什么,首先是标签,ul、li、p;然后是非标签节点1、2、3;还有事件,onClick。然后观察整个整体,是不是有嵌套关系,所以有children属性,存储内层的信息。由于元素可以同层吧,所以整体是一个数组,children也是数组。假设使用tag存储html标签名称,使用type表示是tag节点还是文本string节点,区分一下类型。对于标签上的事件,定义为props属性

const virtualDom = [
    {
      tag: "ul",
      type: "tag",
      children: [
        {
          tag: "li",
          type: "tag",
          children: [
            {
              tag: "",
              type: "string",
              content: "1",
            },
          ],
        },
        {
          tag: "",
          type: "string",
          content: "2",
        },
        {
          tag: "li",
          type: "tag",
          children: [
            {
              tag: "p",
              type: "tag",
              props: {
                onClick: () => alert("选中P标签啦"),
              },
              children: [
                {
                  tag: "",
                  type: "string",
                  content: "3",
                },
              ],
            },
          ],
        },
      ],
    },
  ];

2.将虚拟DOM渲染为真实DOM

这里要用到原生的html操作dom节点的一些方法了

document提供的createDocumentFragment()创建文档对象,在不执行appendChild前不会改变原文档流节点。

document对象提供的createElement创建节点,可以根据type是否为tag看是否新增标签,以及新增的是哪个标签,ul,li还是p。

通过createElement创建的节点,可以通过addEventListener手动添加事件;这里使用for in遍历props对象所有属性,找到on开头的事件,通过addEventListener添加事件

通过appendChild方法挂载

  function render(virtualDom) {
    const fragment = document.createDocumentFragment(); // 创建文档片段
    virtualDom?.forEach((element) => {
      if (element.type === "tag") {
        //是节点
        const tag = document.createElement(element.tag); // 创建元素节点

        //处理事件
        for (let key in element?.props) {
          if (/^on/.test(key)) {
            tag.addEventListener(
              // 添加事件监听器
              key.substr(2).toLocaleLowerCase(),
              element?.props[key]
            );
          }
        }
        //处理children
        if (element.children.length) {
          // 递归处理子节点
          const children = render(element.children);
          tag.appendChild(children);
        }
        fragment.appendChild(tag); // 将当前节点添加到文档片段中
      } else if (element.type === "string") {
        const textNode = document.createTextNode(element.content);
        fragment.appendChild(textNode);
      }
    });
    return fragment;
  }
  //   appendChild 用法,父节点.appendChild(子节点)
  document.getElementById("root").appendChild(render(virtualDom));

 完整代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

<script>
  // <ul>
  // 	<li>1</li>
  // 	2
  // 	<li>
  // 		<p @click=()=>alert("选中P标签啦")>3</p>
  // 	</li>
  // </ul>
  const virtualDom = [
    {
      tag: "ul",
      type: "tag",
      children: [
        {
          tag: "li",
          type: "tag",
          children: [
            {
              tag: "",
              type: "string",
              content: "1",
            },
          ],
        },
        {
          tag: "",
          type: "string",
          content: "2",
        },
        {
          tag: "li",
          type: "tag",
          children: [
            {
              tag: "p",
              type: "tag",
              props: {
                onClick: () => alert("选中P标签啦"),
              },
              children: [
                {
                  tag: "",
                  type: "string",
                  content: "3",
                },
              ],
            },
          ],
        },
      ],
    },
  ];

  function render(virtualDom) {
    const fragment = document.createDocumentFragment(); // 创建文档片段
    virtualDom?.forEach((element) => {
      if (element.type === "tag") {
        //是节点
        const tag = document.createElement(element.tag); // 创建元素节点

        //处理事件
        for (let key in element?.props) {
          if (/^on/.test(key)) {
            tag.addEventListener(
              // 添加事件监听器
              key.substr(2).toLocaleLowerCase(),
              element?.props[key]
            );
          }
        }
        //处理children
        if (element.children.length) {
          // 递归处理子节点
          const children = render(element.children);
          tag.appendChild(children);
        }
        fragment.appendChild(tag); // 将当前节点添加到文档片段中
      } else if (element.type === "string") {
        const textNode = document.createTextNode(element.content);
        fragment.appendChild(textNode);
      }
    });
    return fragment;
  }
  //   appendChild 用法,父节点.appendChild(子节点)
  document.getElementById("root").appendChild(render(virtualDom));
</script>

效果演示

vue响应式

vue2:手动实现一个简化版的 reactive 函数,用来实现对象响应式的劫持

首先了解vue2的对象响应式原理:Vue.js 2.x 中的数据响应式原理主要是通过使用 Object.defineProperty 方法来劫持对象的属性,以实现数据的响应式更新。

  • 在 Vue 实例初始化阶段,会对 data 数据进行响应式处理。
  • Vue 会遍历 data 对象的属性,使用 Object.defineProperty 方法为每个属性定义 getter 和 setter 方法。
  • 在 getter 方法中,Vue 会收集依赖(比如 Watcher 对象),用于之后的更新通知。
  • 在 setter 方法中,当属性的值发生变化时,Vue 会通知相关依赖进行更新。

1.实现observer和defineReactive

unction observer(data) {
    // 如果数据不是对象或为null,则直接返回
    if (typeof data !== "object" || data === null) {
      return data;
    }
    // 遍历对象的每个属性,为每个属性设置响应式
    for (let key in data) {
      defineReactive(data, key, data[key]);
    }
  }

  function defineReactive(target, key, value) {
    // 递归遍历对象,为嵌套对象的属性设置响应式
    observer(value);
    // 使用Object.defineProperty为对象的属性定义getter和setter
    Object.defineProperty(target, key, {
      get() {
        return value;
      },
      set(newValue) {
        // 当属性值发生变化时,更新属性值并递归遍历新值
        if (value !== newValue) {
          value = newValue;
          observer(value);
        }
      },
    });
  }

 2.效果演示

获取对象值obj.a 或 obj.b.c——》触发Object.defineProperty的get方法

修改对象值obj.a=2 或者obj.b.c=3——》触发Object.defineProperty的set方法

 可以看到控制台打印obj对象,对象的所有属性,包括深层的属性都加了get和set方法。

并且访问的时候触发了get方法,修改属性值的时候触发了set方法。数据劫持有效。

 思考:为什么修改对象的属性会进set方法,我们没有手动调set?

首先:在使用Object.defineProperty定义对象属性时,可以通过getset方法来定义属性的读取和赋值行为,这是对象本身提供的访问逻辑。具体实现的细节和内部机制可以不用过多考虑

其次:我们在Object.defineProperty里定义的getset方法实际上是在重写了对象属性的默认行为。通过定义get方法,自定义属性的读取行为;通过定义set方法,自定义属性的赋值行为。

 思考:为什么set在新旧value比对的时候,可以拿到旧的value?

看下value是在哪定义的?是不是在set外部?想到了什么?是不是闭包!

闭包是指函数和函数内部能访问到的外部变量形成的组合,形成了一个封闭的作用域。在这段代码中,set方法中的value变量是在defineReactive函数作用域内定义的,而set方法是一个闭包,可以访问到value变量。因此,当调用set方法更新属性值时,value变量是在闭包中被记住的,即使defineReactive函数执行完毕,set方法仍然可以访问和修改value变量的值。

完整代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body></body>
</html>
<script>
  // observer函数用于递归遍历对象,为对象的每个属性设置getter和setter
  function observer(data) {
    // 如果数据不是对象或为null,则直接返回
    if (typeof data !== "object" || data === null) {
      return data;
    }
    // 遍历对象的每个属性,为每个属性设置响应式
    for (let key in data) {
      defineReactive(data, key, data[key]);
    }
  }

  function defineReactive(target, key, value) {
    // 递归遍历对象,为嵌套对象的属性设置响应式
    observer(value);
    // 使用Object.defineProperty为对象的属性定义getter和setter
    Object.defineProperty(target, key, {
      get() {
        return value;
      },
      set(newValue) {
        // 当属性值发生变化时,更新属性值并递归遍历新值
        if (value !== newValue) {
          value = newValue;
          observer(value);
        }
      },
    });
  }

  // 测试对象的数据的查看,修改
  const obj = {
    a: 1,
    b: {
      c: 2,
    },
  };
  // 对对象进行数据劫持,实现属性的监听和响应
  observer(obj);
  console.log(obj); //打印observer后的对象

  //获取对象属性和修改属性
  obj.a;
  obj.a = 2;
  obj.b.c;
  obj.b.c = 3;
</script>

3.回顾源码

follow up:在上一题的基础上,简单的实现vue2对数组的拦截方法

 我们都知道vue2对数组的响应式是通过改写数组操作的七个方法的,那究竟怎么实现的?

Vue 2 通过以下步骤实现数组的响应式: 

  1. 获取数组原型对象:Vue2 首先获取数组的原型对象 Array.prototype
  2. 创建新的数组原型对象:Vue2 通过 Object.create(Array.prototype) 创建一个新的数组原型对象 arrayProto,这样可以避免直接修改 Array.prototype,从而影响到所有数组实例。
  3. 重写数组的七个方法:Vue 2 对数组的七个操作方法(push、pop、shift、unshift、splice、sort、reverse)进行了重写,在重写的方法中,除了执行原始的数组操作外,还会通知依赖更新,即触发响应式更新。
  4. 替换原型对象:Vue 2 将新的数组原型对象 arrayProto 替换掉数组的原型对象 Array.prototype,从而实现了对数组的响应式处理。

1.代码实现

关键点:Object.create方法创建一个新的对象,并且将新对象的原型指向create的参数。

数组的原型是Array.prototype。对数组对象调用.push等方法是从数组的原型上去找的。而我们要做的是对要进行响应式拦截的数组进行原型指向修改。

这里通过Object.create(Array.prototype)创建了一个新的对象newArrayProto,扩展newArrayProto里数组的方法,让其保留原始的数组方法,同时具备拦截数据的能力。

在对数组进行遍历时,通过data.__proto__修改对象的原型指向,让其指向新的newArrayProto,从而实现了对数组的响应式拦截。

<script>
  //新增一个newArrayProto对象,指向Array的原型
  let newArrayProto = Object.create(Array.prototype);
  let oldArrayProto = Array.prototype;
  const arrayMethods = [
    "push",
    "pop",
    "shift",
    "unshifit",
    "sort",
    "reverse",
    "splice",
  ]; //需要改写的七个方法
  arrayMethods.forEach((method) => {
    newArrayProto[method] = function (...args) {
      console.log("用户调用了:", method);
      oldArrayProto[method].call(this, ...args);
    };
  });
  // observer函数用于递归遍历对象,为对象的每个属性设置getter和setter
  function observer(data) {
    // 如果数据不是对象或为null,则直接返回
    if (typeof data !== "object" || data === null) {
      return data;
    }
    if (Array.isArray(data)) {
      data.__proto__ = newArrayProto; //改写proto的原型指向
    } else {
      // 遍历对象的每个属性,为每个属性设置响应式
      for (let key in data) {
        defineReactive(data, key, data[key]);
      }
    }
  }

  function defineReactive(target, key, value) {
    // 递归遍历对象,为嵌套对象的属性设置响应式
    observer(value);
    // 使用Object.defineProperty为对象的属性定义getter和setter
    Object.defineProperty(target, key, {
      get() {
        return value;
      },
      set(newValue) {
        // 当属性值发生变化时,更新属性值并递归遍历新值
        if (value !== newValue) {
          value = newValue;
          observer(value);
        }
      },
    });
  }

  // 测试对象的数据的查看,修改
  const obj = {
    a: 1,
    b: {
      c: 2,
    },
  };
  const obj2 = [1, 4, 5, 3];
  // 对对象进行数据劫持,实现属性的监听和响应
  observer(obj2);
  obj2.push(90);
  obj2.sort();
  console.log(obj2); //打印observer后的对象
</script>

 2.效果演示

完整代码

在代码中,使用了call方法访问数组原有的方法,为什么用call?

这里oldArrayProto[method]的执行是依赖具体的对象的,方法找对象?是不是可以用call改变this指向,让这个方法在当前作用域中执行。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body></body>
</html>
<script>
  //新增一个arrayProto对象,指向Array的原型
  let arrayProto = Object.create(Array.prototype);
  let oldArrayProto = Array.prototype;
  const arrayMethods = [
    "push",
    "pop",
    "shift",
    "unshifit",
    "sort",
    "reverse",
    "splice",
  ]; //需要改写的七个方法
  arrayMethods.forEach((method) => {
    arrayProto[method] = function (...args) {
      console.log("用户调用了:", method);
      //使用call方法显示告诉oldArrayProto调用对象
      oldArrayProto[method].call(this, ...args);
    };
  });
  // observer函数用于递归遍历对象,为对象的每个属性设置getter和setter
  function observer(data) {
    // 如果数据不是对象或为null,则直接返回
    if (typeof data !== "object" || data === null) {
      return data;
    }
    if (Array.isArray(data)) {
      data.__proto__ = arrayProto; //改写proto的原型指向
    } else {
      // 遍历对象的每个属性,为每个属性设置响应式
      for (let key in data) {
        defineReactive(data, key, data[key]);
      }
    }
  }

  function defineReactive(target, key, value) {
    // 递归遍历对象,为嵌套对象的属性设置响应式
    observer(value);
    // 使用Object.defineProperty为对象的属性定义getter和setter
    Object.defineProperty(target, key, {
      get() {
        return value;
      },
      set(newValue) {
        // 当属性值发生变化时,更新属性值并递归遍历新值
        if (value !== newValue) {
          value = newValue;
          observer(value);
        }
      },
    });
  }

  // 测试对象的数据的查看,修改
  const obj = {
    a: 1,
    b: {
      c: 2,
    },
  };
  const obj2 = [1, 4, 5, 3];
  // 对对象进行数据劫持,实现属性的监听和响应
  observer(obj2);
  obj2.push(90);
  obj2.sort();
  console.log(obj2); //打印observer后的对象
</script>

3.回顾源码

vue3:手动实现一个简化版的vue3响应式

前置知识:vue3响应式和Proxy对象使用

vue3响应式系统使用了 ES6 的 Proxy 对象来实现数据的监听和触发更新。Proxy发挥了两个作用:代理拦截

  • 什么是代理:我不直接访问对象Obj,我访问被proxy转换后的proxyObj。使用 Proxy 对象对原始数据对象进行代理,实现对数据的监听。当访问响应式对象的属性时,实际上是访问了被 Proxy 转换后的对象,从而触发了代理的作用。
  • 什么是拦截:proxy提供了访问proxy对象的get和set方法,通过自定义 Proxy 的 getset 方法来拦截对响应式对象的访问和修改。当访问响应式对象的属性时,会触发 Proxy 的 get 方法,当修改响应式对象的属性时,会触发 Proxy 的 set 方法。

Proxy对象使用

new Proxy() 构造函数接受两个参数,分别是 targethandler。这两个参数的含义如下:

  1. target:表示要代理的目标对象,即被代理的对象。Proxy 对象会代理对目标对象的访问和操作。

  2. handler表示一个对象,其属性是当执行一个操作时定义代理的行为的函数。handler 是一个包含了代理行为的方法的对象。

handler 对象中,可以定义多个方法来控制代理对象的行为。在本题中用的handler 方法及其作用如下:

  1. get(target, property, receiver):拦截对象属性的读取操作,当访问代理对象的属性时会触发该方法。

  2. set(target, property, value, receiver):拦截对象属性的设置操作,当给代理对象的属性赋值时会触发该方法。

 1.实现handler和reactive

根据proxy的代理特性和对对象的劫持功能,得到如下vue3的数据劫持方法

reactive函数的作用就是返回一个proxy代理的对象。在handler的get方法里,对value是对象时进行了递归处理

  let handler = {
    //拦截整个对象,访问对象的属性时get拦截器触发
    get(target, key) {
      let value = target[key];
      if (typeof value === "object") {
        //如果访问的对象属性还是对象,进行递归
        return new Proxy(value, handler);
      }
      return value;
    },
    //拦截整个对象,当修改对象的属性的时候set拦截器会触发
    set(target, key, value) {
      target[key] = value;
    },
  };
  function reactive(target) {
    return new Proxy(target, handler);
  }

  let obj = { name: "jw", age: 30, n: [1, 2, 3, 4, 5] };
  //拿到obj的代理对象proxyObj
  const proxyObj = reactive(obj);
  //不访问obj,访问代理对象proxyObj
  console.log(proxyObj.name); //触发get拦截器
  proxyObj.age = 31; //触发set拦截器
  proxyObj.name = 100; //设置一个不存在的属性

 vue3在使用对象的属性的时候其实是懒代理,访问对象的属性的时候才进行get逻辑处理

 2.效果演示

 3.Proxy和defineProperty的区别

Object.defineProperty:

  Object.defineProperty 是 ES5 中提供的方法,用于定义或修改对象的属性。在初始化时对对象的属性进行操作,可以为属性设置 getset 方法,但是只对已存在的属性生效,对后续添加的属性不会自动追加 getset 方法。通过 Object.defineProperty 添加的属性,无法像 Proxy 那样对整个对象进行拦截,只能对单个属性进行操作。

Proxy:

  Proxy 是 ES6 中新增的特性,提供了一种用于定义基本操作的通用方法。Proxy 可以代理整个对象,而不仅仅是单个属性,可以拦截对象的多种操作,如读取属性、写入属性、删除属性等。通过 Proxy 创建的代理对象可以对整个对象进行拦截,包括后续添加的属性,因此具有更强大的灵活性。对于嵌套对象,通过递归遍历来为每个属性添加 Proxy 对象,从而实现对整个嵌套对象的拦截。并且proxy是访问属性的时候才进行递归。

依赖收集

设计一个vue2的依赖收集系统

实现一个 Dep 类,用于管理依赖,包括添加依赖、移除依赖、通知依赖等方法。

实现一个 Watcher 类,用于管理 watches,包括添加 watch、移除 watch、更新 watch 等方法。

实现一个Observe方法,遍历对象key。并实现一个defineReactive方法定义响应式数据

思路: vue每个组件实例vm都有一个渲染watcher。每个响应式对象的属性key都有一个dep对象。所谓的依赖收集,就是让每个属性记住它依赖的watcher。但是属性可能用在多个模板里,所以,一个属性可能对应多个watcher。因此,在vue2中,属性要通过dep对象管理属性依赖的watcher。在初始化时编译器生成render函数,此时触发属性的依赖收集dep.depend。组件挂载完成后,操作页面,当数据变化后,对应的响应时对象会调用dep.notify方法通知自己对应的watcher更新。在watcher实例中有updateComponent方法,可以进行对应组件的更新。

详细依赖收集可以看我的另一篇博客介绍的:

vue2源码解析——vue中如何进行依赖收集、响应式原理-CSDN博客

 Dep类

Dep类,subs数组存储watcher实例

depend方法,收集依赖,调用该方法,触发sub.addDep方法

addSub方法,添加watcher,通过watcher类的方法回调

remove移除watcher

notify方法,通知subs数组每个watcher实例更新组件

class Dep {
  target = null;
  constructor() {
    this.subs = []; //watcher实例数组
  }
  addSub(sub) {
    console.log("dep收集依赖watcher");
    this.subs.push(sub);
  }
  depend(sub) {
    if (sub) {
      sub.addDep(this);
    }
  }
  remove(sub) {
    const index = this.subs.indexOf(sub);
    if (index > -1) {
      this.subs.splice(index, 1);
    }
  }
  notify() {
    console.log("notify:通知watcher更新");
    this.subs.forEach((sub) => sub.update());
  }
}

Watcher类

deps数组,存储dep对象

addDep方法,在Dep类中被depend方法调用,将dep存放deps数组中,并触发dep的addSub方法。

update方法,更新组件方法

get方法,更新Dep.target

class Watcher {
  constructor(vm, cb) {
    this.vm = vm;
    this.cb = cb;
    this.deps = [];
    this.get();
  }
  addDep(dep) {
    this.deps.push(dep);
    dep.addSub(this);
  }
  update() {
    this.get();
    this.cb.call(this.vm, this.vm);
  }
  get() {
    Dep.target = this;
  }
}

Observer方法和defineReactive方法

响应式核心,在get和set方法基础上,对每个key增加dep对象。用dep对象对属性进行依赖收集,依赖通知。

//observer观察对象
function observer(data) {
  if (typeof data != "object" || data == null) {
    return;
  }
  //先不考虑数组,考虑对象的响应式
  Object.keys(data).forEach((key) => {
    defineReactive(data, key, data[key]);
  });
}
//对象的属性key定义响应式
function defineReactive(obj, key, val) {
  observer(val); //递归调用val,防止对象的val也是对象
  const dep = new Dep(); //对每个key生成一个dep实例
  Object.defineProperty(obj, key, {
    //使用defineProperty重写get和set方法
    get: function reactiveGetter() {
      console.log("访问属性" + key);
      dep.depend(Dep.target); //将Dep.target当前模板wacher实例
      console.log(key + "的dep对象收集watcher");

      return val;
    },
    set: function reactiveSetter(newVal) {
      if (newVal === val) return;
      observer(newVal);
      console.log("dep:notify调用; 属性" + key + "被修改");

      dep.notify();
    },
  });
}

数据测试

创建一个简单的Vue构造函数。传入data数据。在Vue中调用observer定义响应式。同时new一个watcher实例。

测试1:通过data.name访问属性name;然后修改name属性。

测试2:直接修改age属性

const data = {
  name: "John",
  age: 20,
};
function Vue(obj) {
  observer(obj.data);
}
const vm = new Vue({
  data,
});

const watcher = new Watcher(vm, function () {
  console.log("component updated");
});
console.log("打印属性name:" + data.name); //访问属性name
data.name = "change"; //修改属性name
data.age = 22; //修改数据age

 可以看到结果:访问属性name的时候,触发了dep收集watcher,并且在name别修改的时候,dep的notify通知watcher进行修改,watcher的更新方法打印了。

直接修改age属性,dep的notify通知了,但watcher不用更新,因为age没有被读取属性。所以不需要通知更新。

 如果age也被访问了,那么在修改age的时候就会通知组件更新

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/527050.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【面试】运算器-⑪搜索旋转排序数组

先存一下后面要用的字符⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳ 33. 搜索旋转排序数组 感谢力扣&#xff01; 整数数组 nums 按升序排列&#xff0c;数组中的值 互不相同 。 在传递给函数之前&#xff0c;nums 在预先未知的某个下标 k&#xff08;0 < k < nums.length&#xff09;上进行了…

《UE5_C++多人TPS完整教程》学习笔记31 ——《P32 角色移动(Character Movement)》

本文为B站系列教学视频 《UE5_C多人TPS完整教程》 —— 《P32 角色移动&#xff08;Character Movement&#xff09;》 的学习笔记&#xff0c;该系列教学视频为 Udemy 课程 《Unreal Engine 5 C Multiplayer Shooter》 的中文字幕翻译版&#xff0c;UP主&#xff08;也是译者&…

S32K324 数据初始化Rom到Ram Copy的方式

文章目录 前言基础知识ld文件中的段定义ld文件中的符号定义 ld定义copy地址范围启动文件中的定义Copy的使用总结 前言 之前一直不理解在ld文件中加__xxx_ram_start,__xxx_rom_start,__xxx_rom_end这些的作用&#xff0c;也不清楚原理。前几天遇到一个内存copy的问题&#xff0…

云计算(五)—— OpenStack基础环境配置与API使用

OpenStack基础环境配置与API使用 项目实训一 【实训题目】 使用cURL命令获取实例列表 【实训目的】 理解OpenStack的身份认证和API请求流程。 【实训准备】 &#xff08;1&#xff09;复习OpenStack的认证与API请求流程的相关内容。 &#xff08;2&#xff09;熟悉cURL…

软件设计师——数据库

数据库 三级模式两级映像关系模型基本术语关系模型中的关系完整性约束 三级模式两级映像 概念模式&#xff08;也称模式&#xff09;对应基本表 外模式&#xff08;也称用户模式或子模式&#xff09;对应视图 内模式&#xff08;也称存储模式&#xff09;对应存储文件 两级映像…

SL1581耐压30V芯片 24V转5V/2.4A

SL1581是一款专为24V转5V/2.4A应用设计的耐压30V芯片。这款芯片采用了先进的电源管理技术和高效能的转换电路&#xff0c;为电子设备提供了稳定、可靠的电源输出。 首先&#xff0c;SL1581芯片具有出色的耐压性能&#xff0c;能够在高达30V的电压下稳定工作。这使其非常适合在需…

RFID涉密载体柜 RFID智能文件柜系统

涉密载体管控RFID智能柜&#xff08;载体柜DW-G101R&#xff09;通过对涉密物资、设备进行RFID唯一标识并放置于RFID设备涉密物资柜柜体&#xff0c;通过定位每台设备每件涉密物资的位置&#xff0c;实现涉密物资审批、自助借还、防盗等出入库全流程自动化管理。主要管理对象移…

Vulnhub:MHZ_CXF: C1F

目录 信息收集 arp-scan nmap nikto WEB web信息收集 dirmap gobuster ssh登录 提权 获得初始立足点 系统信息收集 横向渗透 提权 信息收集 arp-scan ┌──(root㉿ru)-[~/桌面] └─# arp-scan -l Interface: eth0, type: EN10MB, MAC: 00:50:56:…

产品经理和项目经理的区别

1. 前言 本文深入探讨了产品经理与项目经理在职责、关注点以及所需技能方面的显著区别。产品经理主要负责产品的规划、设计和市场定位,强调对用户需求的深刻理解和产品创新的推动;而项目经理则侧重于项目的执行、进度控制和资源管理,确保项目按时、按质、按预算完成。两者在…

C++11可变模板参数:海纳百川的Args

目录 一、可变模板参数的概念及功能 1.1Args的概念与使用 1.2获取args中的参数 二、emplace可变模板参数的实际应用 三、逗号表达式展开参数包 一、可变模板参数的概念及功能 1.1Args的概念与使用 C11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板…

python中的split()用法

在Python中&#xff0c;split() 是一个字符串方法&#xff0c;用于将字符串按照指定的分隔符分割成一个列表。如果没有提供分隔符&#xff0c;那么它会默认按照任何空白字符&#xff08;如空格、换行符、制表符等&#xff09;进行分割。 这里是 split() 方法的一些基本用法&am…

德兰梅尔:耐高温热销的膜元件亮相2024上海国际生物发酵展

德兰梅尔&#xff1a;耐高温热销的膜元件盛装亮相2024上海国际生物发酵展&#xff0c;8月7-9号上海新国际博览中心与您不见不散&#xff01; 据了解&#xff0c;从成立至今&#xff0c;德兰梅尔一直专注膜技术、膜产品的开发生产。在中国市场上&#xff0c;德兰梅尔刚步入中国…

代码随想录算法训练营33期 第三十一天(补29) | 491. 非递减子序列、46. 全排列、47. 全排列 II

491. 非递减子序列 class Solution { public:vector<int> path;vector<vector<int>> result;void BackTracking(vector<int>& nums, int index){if(path.size()>2){result.push_back(path);}unordered_set<int> usedSet;for (int iindex…

nandgame中的asm编程 Escape Labyrinth(逃离迷宫)

先翻译题目&#xff1a; 逃离迷宫计算机被困在火星上的迷宫中。编写一个程序&#xff0c;让它逃离迷宫。计算机配备了连接的轮子和前方障碍物探测器。与轮子和探测器的输入/输出是内存映射在地址7FFF上&#xff1a;对外设的输出信号&#xff1a; 位 设置为1代表&#xff1a; 2…

高精度原边控制离线式PWM功率开关芯片D3820的特征和详细的工作原理介绍

D3820是一款高精度原边控制离线式PWM功率开关。本文主要介绍D3820的特征和详细的工作原理&#xff0c;对反激式隔离AC-DC开关电源提供较为详细的测试过程。 特 点 1、全电压范围CC/CV精度保持在5%以内 2、用原边控制&#xff0c;无需TL431和光耦 3、欠压锁定&#xff08…

2024mathorcup妈妈杯数学建模A题思路模型

2024mathorcup妈妈杯数学建模A题思路模型&#xff1a;比赛开始后第一时间更新&#xff0c;更新见文末名片&#xff0c;下面对2022年B题进行介绍&#xff1a; 2022Mathorcup B题题目介绍 ​ B题无人仓的搬运机器人调度问题本题考在无人仓内的仓库管理问题之一&#xff0c;搬运机…

mos管开关出现尖峰的原理? mos管开关的时候cs会出现尖峰,请问这是什么原因?

MOS管在开关过程中出现尖峰现象&#xff0c;通常是由于电路中的寄生参数和快速电压变化引起的。以下是一些导致尖峰出现的主要原因和原理&#xff1a; 寄生电容 在MOS管的源极&#xff08;S&#xff09;和漏极&#xff08;D&#xff09;之间存在寄生电容&#xff0c;这个电容在…

Vue3组件基础示例

组件是vue中最推崇的&#xff0c;也是最强大的功能之一&#xff0c;就是为了提高重用性&#xff0c;减少重复性的开发。 如何使用原生HTML方法实现组件化 在使用原生HTML开发时&#xff0c;我们也会遇到一些常见的功能、模块&#xff0c;那么如何在原生HTML中使用组件化呢&am…

IoT数采平台4:测试

IoT数采平台1&#xff1a;开篇IoT数采平台2&#xff1a;文档IoT数采平台3&#xff1a;功能IoT数采平台4&#xff1a;测试 Modbus RTU串口测试 OPC测试 HTTP测试 MQTT透传测试 MQTT网关测试及数据上报 TCP / UDP 监听&#xff0c;客户端连上后发送信息&#xff0c;客户端上报数据…

P4117 [Ynoi2018] 五彩斑斓的世界

分析第一个操作 朴素的做法&#xff0c;遍历一遍大于x就-x&#xff0c;极限复杂度O(mn) 分块做法 整块:我们维护一个最大值&#xff0c;从mx到x遍历一遍&#xff08;减去x)用并查集操作merge(i,i-x),考虑mx100001,x1,极限复杂度O(mV) 我们可以分析 1.x>(mx/2),从mx到x遍…