目录
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双向绑定
请手动实现一个简单的双向绑定功能,要求实现以下功能:
- 有一个输入框和一个显示框,输入框用于输入内容,显示框用于展示输入框中的内容。
- 当输入框中的内容发生变化时,显示框中的内容应该实时更新。
思路:分析题目,一个输入框和显示框。输入的信息要显示在显示框内。所以要对输入框的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
定义对象属性时,可以通过get
和set
方法来定义属性的读取和赋值行为,这是对象本身提供的访问逻辑。具体实现的细节和内部机制可以不用过多考虑其次:我们在Object.defineProperty里定义的
get
和set
方法,实际上是在重写了对象属性的默认行为。通过定义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 通过以下步骤实现数组的响应式:
- 获取数组原型对象:Vue2 首先获取数组的原型对象
Array.prototype
。 - 创建新的数组原型对象:Vue2 通过
Object.create(Array.prototype)
创建一个新的数组原型对象arrayProto
,这样可以避免直接修改Array.prototype
,从而影响到所有数组实例。 - 重写数组的七个方法:Vue 2 对数组的七个操作方法(push、pop、shift、unshift、splice、sort、reverse)进行了重写,在重写的方法中,除了执行原始的数组操作外,还会通知依赖更新,即触发响应式更新。
- 替换原型对象: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 的
get
和set
方法来拦截对响应式对象的访问和修改。当访问响应式对象的属性时,会触发 Proxy 的get
方法,当修改响应式对象的属性时,会触发 Proxy 的set
方法。
Proxy对象使用
new Proxy()
构造函数接受两个参数,分别是target
和handler
。这两个参数的含义如下:
target
:表示要代理的目标对象,即被代理的对象。Proxy 对象会代理对目标对象的访问和操作。
handler
:表示一个对象,其属性是当执行一个操作时定义代理的行为的函数。handler
是一个包含了代理行为的方法的对象。
handler
对象中,可以定义多个方法来控制代理对象的行为。在本题中用的handler
方法及其作用如下:
get(target, property, receiver)
:拦截对象属性的读取操作,当访问代理对象的属性时会触发该方法。
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 中提供的方法,用于定义或修改对象的属性。在初始化时对对象的属性进行操作,可以为属性设置get
和set
方法,但是只对已存在的属性生效,对后续添加的属性不会自动追加get
和set
方法。通过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的时候就会通知组件更新