文章目录
v-model
v-model就实现了双向数据绑定,实际上它就是通过Vue提供的事件机制。即在子组件通过$emit()触发一个事件,在父组件使用v-on来监听对应的事件并修改相应的数据。
input的v-model就是通过<input :value="value" @input="input"/>
封装一个input组件,v-model实现
两个注意点:
假设不是使用的value属性和input事件,而是使用string属性和strChange事件。通过mode属性设置
爷父孙组件,父组件通过computed实现
// 父组件
<my-input v-model="value"></my-input>
// my-input子组件组件
<template>
<!-- 2. 监听 input 事件的出发 -->
<div class="input" contenteditable @input="input"></div>
</template>
<script>
export default {
// 1. 接受父级传递的值
props: {
value: {
type: String,
default: ''
}
},
methods: {
// 3. 编写 input 事件触发执行的事件处理函数
input (event) {
// 4. $emit input 事件,并将 event.target.innerText 作为参数
this.$emit('input', event.target.innerText)
}
}
}
</script>
原生实现数据双向绑定
<body>
<span id="text"></span>
<input id="value" />
</body>
<script>
window.onload = function() {
let input = document.getElementById('value')
let text = document.getElementById('text')
let data = { value: '' }
input.value = data.value
text.innerHTML = data.value
// 数据劫持
Object.defineProperty(data, 'value', {
set: function (val) {
input.value = val
text.innerHTML = val
},
get: function () {
return input.value
}
})
input.addEventListener('keyup', function (){
data.value = input.value
})
}
</script>
响应式原理
- 对象类型:通过Object.definePrototype()劫持getter和setter
- 数组类型:通过重写数组上的方法
vue是一个 MVVM 结构的框架;也就是 数据层、视图层、数据-视图层;响应式的原理就是要实现当数据更新时,视图层也要相应的更新
vue的实现原理:
- 数据劫持 :在Vue中,当你把一个普通js对象传给Vue实例作为data选项时,Vue将遍历此对象所有的属性,并使用 Object.defineProperty() 把这些属性全部转为 getter/setter。这样,Vue 能够追踪到属性的变化,并在属性被访问和修改时执行相应的操作
- 依赖追踪 :Vue内部维护了一个依赖收集的系统,每个响应式对象都有一个对应的依赖集合,当数据被访问时,会把当前的Watcher(观察者)记录下来。这样,当数据发生变化时,依赖于这个数据的所有Watcher都会被通知,进而更新相应的视图
- 派发更新 :当响应式数据发生变化时,Vue会遍历依赖集合,通知相关的Watcher更新视图
vue双向绑定流程
- new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe中
- 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中
- 同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
- 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher
- 将来data中数据⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数
数据影响视图,视图影响数据
- 侦测数据的变化(数据劫持/数据代理)
- 收集视图依赖了哪些数据(依赖收集)
- 数据变化时,自动通知更新视图(发布订阅模式)
实现一个简单的响应式(vue2和vue3版本)
实现响应式:
- 发布-订阅者模式:Vue使用发布-订阅者模式来实现数据变动时的通知和更新
- 数据劫持:Vue通过Object.defineProperty对数据进行劫持
vue2:
1、实现一个监听器 Observer,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者(Watcher);
2、实现一个订阅器 Dep,用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理;
3、实现一个订阅者 Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图;
4、实现一个解析器 Compile,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化。
/**
* @name Vue数据双向绑定(响应式系统)的实现原理 Vue2
*/
// observe方法遍历并包装对象属性
function observe(target) {
// 若target是一个对象,则遍历它
if (target && typeof target === "Object") {
Object.keys(target).forEach((key) => {
// defineReactive方法会给目标属性装上“监听器”
defineReactive(target, key, target[key]);
});
}
}
// 定义defineReactive方法
function defineReactive(target, key, val) {
const dep = new Dep();
// 属性值也可能是object类型,这种情况下需要调用observe进行递归遍历
observe(val);
// 为当前属性安装监听器
Object.defineProperty(target, key, {
// 可枚举
enumerable: true,
// 不可配置
configurable: false,
get: function () {
return val;
},
// 监听器函数
set: function (value) {
dep.notify();
},
});
}
class Dep {
constructor() {
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
vue3:
1、通过state = reactive(target)来定义响应式数据(这里基于Proxy实现)
2、通过 effect声明依赖响应式数据的函数cb ( 例如视图渲染函数render函数),并执行cb函数,执行过程中,会触发响应式数据 getter
3、在响应式数据 getter中进行 track依赖收集:存储响应式数据与更新函数 cb 的映射关系,存储于targetMap
4、当变更响应式数据时,触发trigger,根据targetMap找到关联的cb并执行
// 建立依赖收集、通知的类
class Dep {
constructor() {
this.subscribers = new Set();
}
depend() {
if (Dep.activeEffect) {
this.subscribers.add(Dep.activeEffect);
}
}
notify() {
this.subscribers.forEach(effect => effect());
}
}
// 存储当前正在运行的effect
Dep.activeEffect = null;
function effect(fn) {
// 设置当前激活的effect
Dep.activeEffect = fn;
fn();
Dep.activeEffect = null;
}
function reactive(target) {
const depMap = new Map();
const handler = {
get(target, key, receiver) {
let dep = depMap.get(key);
if (!dep) {
dep = new Dep();
depMap.set(key, dep);
}
dep.depend(); // 依赖收集
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
let dep = depMap.get(key);
if (dep) {
dep.notify(); // 触发更新
}
return result;
}
};
return new Proxy(target, handler);
}
// 使用示例
const state = reactive({ count: 0 });
effect(() => {
console.log(`The count is: ${state.count}`);
});
setTimeout(() => {
state.count = 3
}, 3000)
vue对于数组的变异方法
Vue2 中对于对象的响应式是通过Object.defineProperty()
方法来实现的,这个方法可以将对象属性转换成 getter 和 setter 的形式,从而实现对属性的“劫持”。
但是对于数组来说,Array.prototype 中的方法并不会触发这样的 getter 和 setter,因此 Vue 无法监听到这些变化,也就不能及时地更新视图。 当数组的元素发生变化时,Vue.js无法触发视图的更新,因为数组的属性(例如长度)是只读的。
为了解决这个问题,Vue2 对于数组提供了一些变异方法。当使用这些方法操作数组时,Vue2 会检测到数组的变化并及时更新视图,从而保证视图和状态的同步。
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
而直接通过索引来修改元素,则无法触发这种变化,因此也就无法实现响应式更新。arr[0] = 12无法触发更新
重写Vue数组中的push方法,每次push时输出push的值
const arrayPush = Array.prototype.push;
// 重写Array的原型上的push方法
Array.prototype.push = function(...args) {
args.forEach(item => {
console.log('Pushed value:', item);
});
// 调用原始的push方法来实现正常的数组操作
return arrayPush.apply(this, args);
};
// 示例
const arr = [1, 2, 3];
arr.push(4);