数据双向绑定v-modal和响应式原理

20 篇文章 0 订阅

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>

响应式原理

vue双向绑定流程

  1. new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe中
  2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中
  3. 同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
  4. 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher
  5. 将来data中数据⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

数据影响视图,视图影响数据

  1. 侦测数据的变化(数据劫持/数据代理)
  2. 收集视图依赖了哪些数据(依赖收集)
  3. 数据变化时,自动通知更新视图(发布订阅模式)

实现一个简单的响应式(vue2和vue3版本)

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);
  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值