Vue动态绑定原理简单入门

December 23, 2023 作者: yijianhao 分类: 前端 浏览: 86 评论: 0

Vue动态绑定原理简单入门

在vue2中,整个响应式系统构建在js特性 Object.defineProperty,可以说它是vue2的MVVM架构的核心。在vue3中,Object.definePropertyProxy替代。

Object.defineProperty

Object.defineProperty是一个在ECMAScript 5(ES5)标准中引入的方法。主要的作用是定义属性是否可写、可枚举、可配置,以及定义属性的getter和setter。

Object.defineProperty() - JavaScript | MDN (mozilla.org)

一个简单的例子:

const obj = {
  internalValue: 0
};

Object.defineProperty(obj, 'number', {
  get() {
    console.log('getter: 获取值');
    return this.internalValue;
  },
  set(value) {
    console.log('setter: 设置值为 ' + value);
    this.internalValue = value;
  },
  enumerable: true,
  configurable: true
});

obj.number = 37; // setter: 设置值为 37
console.log(obj.number); // getter: 获取值; 输出 37

Object.defineProperty 为obj定义了一个number属性,以及定义了这个属性的get方法和set方法。当我们调用obj.number的时候,js就会执行get方法,get方法返回值就是获取到的值;同样的执行obj.number = 123;的时候, set方法就会被调用。

这就是vue2中双向绑定数据的基础。一个简单的例子,用了观察者模式:

class Dep {
  constructor() {
    this.subscribers = [];
  }

  depend() {
    if (Dep.target && !this.subscribers.includes(Dep.target)) {
      this.subscribers.push(Dep.target);
    }
  }

  notify() {
    this.subscribers.forEach(sub => sub.update());
  }
}

Dep.target = null;

class Watcher {
  constructor(obj, key, cb) {
    Dep.target = this;
    this.cb = cb;
    this.obj = obj;
    this.key = key;
    this.value = this.get();  // 这将触发依赖收集
    Dep.target = null;  // 收集完毕后清除
  }

  update() {
    const oldValue = this.value;
    this.value = this.get();
    this.cb.call(this.obj, this.value, oldValue);
  }

  get() {
    return this.obj[this.key];  // 访问属性,触发 getter
  }
}

function defineReactive(obj, key, val) {
  const dep = new Dep();

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      dep.depend();
      return val;
    },
    set: function reactiveSetter(newVal) {
      if (newVal === val) return;
      val = newVal;
      dep.notify();
    }
  });
}

function observe(value) {
  if (value && typeof value === 'object') {
    Object.keys(value).forEach(key => {
      defineReactive(value, key, value[key]);
    });
  }
}

// 假设的组件的 data
let data = { message: 'Hello Vue' };

// 响应式化
observe(data);

// 创建一个 Watcher 来观察 data.message
// 在这个例子中,我们简化了回调函数,只是简单地打印新旧值
new Watcher(data, 'message', (newVal, oldVal) => {
  console.log(`data.message数据变化:${oldVal} -> ${newVal}`);
});

data.message = 'Hello Vue.js1'; 
const a = data.message
console.log(a)

上面的例子需要格外注意Dep.target,它是一个静态属性,目的是将Watcher添加到通知列表

执行流程:

创建Dep类
  ↓
创建Watcher类
  ↓
定义defineReactive函数
  ↓
定义observe函数
  ↓
初始化数据对象data
  ↓
调用observe(data) -> 使data成为响应式对象
  |    ↓
  |  遍历data的属性,对每个属性调用defineReactive
  |    ↓
  |  对每个属性创建一个Dep实例
  |    ↓
  |  使用Object.defineProperty设置getter和setter
  ↓
创建Watcher实例 (依赖收集开始)
  ↓
Watcher构造函数执行
  |    ↓
  |  设置Dep.target为当前Watcher实例
  |    ↓
  |  调用Watcher的get方法 -> 读取data.message
  |    |    ↓
  |    |  触发data.message的getter
  |    |    ↓
  |    |  getter内调用dep.depend() -> 收集依赖
  |    ↓
  |  还原Dep.target为null (依赖收集结束)
  ↓
data.message值发生改变
  ↓
触发data.message的setter
  |    ↓
  |  setter内调用dep.notify() -> 通知变化
  |    ↓
  |  dep.notify遍历所有订阅者,调用它们的update方法
  |    ↓
  |  Watcher的update方法被调用
  |    ↓
  |  update内调用Watcher的get方法 -> 重新读取data.message
  |    ↓
  |  调用回调cb -> 打印新旧值
  ↓
程序继续执行...

当执行了`data.message = 123` 的时候,马上触发get方法,get方法通知一个包含渲染方法的`Watcher`观察者,然后通过这个执行渲染方法更新Html页面上的数据。

当v-mode双向绑定数据的时候(v-model是语法糖,实际是绑定了一个事件,官网有解释),触发事件执行set方法,然后也是执行上面一样的流程

Proxy

在Vue3中,Object.definProperty被替换成`Proxy`, `Proxy`是 ES6 中引入的一个新特性,它允许你创建一个对象的代理,从而可以拦截并自定义对象的基本操作,例如属性访问、赋值、枚举、函数调用等。因为 Proxy 可以拦截整个对象的操作,所以 Vue 3 不需在初始化时遍历对象的每个属性。这样可以减少初始化响应式系统时的开销。

Proxy - JavaScript | MDN (mozilla.org)

let target = {};

let handler = {
  get: function(obj, prop) {
    return prop in obj ? obj[prop] : 37; // 如果属性不存在,返回37
  }
};

let p = new Proxy(target, handler);
console.log(p.a); // 输出 37,因为 'a' 不是 target 的属性, 实际上执行了handler的get方法

模拟vue3的简单例子

简化的 Vue 3 响应式系统的例子:

import { reactive, effect } from 'vue';

// reactive 用于创建响应式对象
const state = reactive({ count: 0 });

// effect 接收一个函数,这个函数会立即执行一次
// 并且当函数中依赖的响应式数据变化时,函数会再次执行
effect(() => {
  console.log(state.count);
});

state.count++; // 修改数据,effect 中的函数会自动重新执行

一个简单的观察者模式来模拟实现这种效果:

function reactive(target) {
  const handler = {
    get(target, prop, receiver) {
      const result = Reflect.get(target, prop, receiver);
      track(target, prop); // 收集依赖
      return result;
    },
    set(target, prop, value, receiver) {
      const oldValue = target[prop];
      const result = Reflect.set(target, prop, value, receiver);
      if (oldValue !== value) {
        trigger(target, prop); // 触发更新
      }
      return result;
    }
  };
  return new Proxy(target, handler);
}

// 存储副作用函数的依赖关系
const targetMap = new WeakMap();

function track(target, key) {
  // 在实际的 Vue 3 中,会有一个全局的活跃的 effect
  // 这里我们简化为一个全局变量
  if (activeEffect) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if (!dep) {
      depsMap.set(key, (dep = new Set()));
    }
    dep.add(activeEffect);
  }
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (depsMap) {
    const dep = depsMap.get(key);
    if (dep) {
      dep.forEach(effect => {
        effect();
      });
    }
  }
}

let activeEffect = null;
function effect(eff) {
  activeEffect = eff;
  activeEffect();
  activeEffect = null;
}

// 使用示例
const state = reactive({ count: 0 });

effect(() => {
  console.log(state.count);
});

state.count++; // 控制台输出:1
state.count++; // 控制台输出:2

effect 类似于 Vue 2 中的 Watcher。当 state.count 被读取时,读取操作被 Proxy 拦截,effect 被订阅到这个状态的变化上。当 state.count 被修改时,修改操作同样被 Proxy 拦截,并通知到所有订阅了这个状态的 effect,从而引起重新执行和更新。

它的执行流程如下:

[创建响应式对象] 
    ↓
[reactive(target)] --> 返回 Proxy 对象
    ↓
[执行 effect 函数]
    ↓
[副作用函数 eff 执行]
    │
    ├─[读取 Proxy 属性] --> [触发 get 陷阱]
    │                           ↓
    │                       [track 依赖收集]
    │
    └─[设置 Proxy 属性] --> [触发 set 陷阱]
                                ↓
                            [trigger 触发更新]
                                ↓
                      [重新执行相关的副作用函数 eff]

reactjs中也有一个useEffect函数,也是监听数据变化,然后执行特定的方法,不过用在系统调用和外部api调用比较多

与React.js的对比

React 通过状态更新引发组件的重新渲染来响应数据的变化,需要显式调用 setState 来触发。而 Vue.js 使用数据劫持机制,自动侦测数据变动并更新 DOM。

React 倾向于使用不可变数据,Vue.js 的数据是可变的,它依赖于其响应式系统来跟踪变化.

#vue(1)

评论