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的时候就会通知组件更新

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

三月的一天

你的鼓励将是我前进的动力。

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

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

打赏作者

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

抵扣说明:

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

余额充值