vue3的双向绑定是通过使用ES6新增的Proxy对象来实现的。Proxy对象可以拦截对象的访问和修改操作,并在这些操作发生时执行自定义的函数。这样,vue3可以在数据对象被读取或修改时,触发相应的响应函数,从而实现数据和视图的同步更新。

具体来说,vue3会创建一个Proxy对象,作为数据对象的代理。Proxy对象会定义一些handler函数,用来拦截数据对象的get和set操作。当数据对象被读取时,Proxy对象会执行get函数,返回数据对象的值,并同时收集依赖该数据的视图组件。当数据对象被修改时,Proxy对象会执行set函数,更新数据对象的值,并同时通知依赖该数据的视图组件进行更新。

这种方式比vue2使用Object.defineProperty方法来实现双向绑定有一些优势,例如:

Proxy对象可以拦截整个对象,而不是单个属性,这样可以减少遍历和重写属性的开销。
Proxy对象可以拦截数组和嵌套对象的变化,而不需要额外的处理。
Proxy对象可以支持更多的操作,例如delete、has、ownKeys等,而不仅限于get和set。

响应式系统

Vue3的响应式系统是一种基于Proxy代理和effect函数的机制,它可以让数据和视图保持同步,实现交互效果。Vue3的响应式系统主要由三个部分组成:reactive,effect和track/trigger。reactive函数可以把一个普通的对象转化为一个响应式的对象,effect函数可以创建一个副作用函数,并执行该函数,track/trigger函数可以实现依赖收集和依赖触发的功能。

reactive函数

reactive函数接收一个普通的对象作为参数,然后返回一个Proxy代理对象。Proxy代理对象是一种特殊的对象,它可以拦截对原始对象的所有操作,比如读取或修改属性。Proxy代理对象有两个重要的函数:get和set。get函数会在我们访问原始对象的属性时触发,set函数会在我们修改原始对象的属性时触发。

effect函数

effect函数接收一个函数作为参数,然后执行该函数,并把该函数添加到一个栈中。这个栈叫做activeReactiveEffectStack,它用来存储当前正在执行的effect函数。effect函数通常用来更新视图或者执行一些副作用操作,比如发送请求或者打印日志等。

track/trigger函数

track/trigger函数是Vue3响应式系统的核心功能,它们可以实现依赖收集和依赖触发的功能。依赖收集就是把数据和视图之间的关系记录下来,依赖触发就是根据数据的变化来更新视图或者执行其他操作。

总结

当我们在effect函数中访问Proxy代理对象的属性时,就会触发get函数,这时就会调用track函数,把当前的effect函数添加到targetMap中。targetMap是一个Map对象,它存储了每个响应式对象及其对应的属性和依赖。这样就完成了依赖收集。

当我们修改Proxy代理对象的属性时,就会触发set函数,这时就会调用trigger函数,执行targetMap中存储的所有依赖于该属性的effect函数。这样就完成了依赖触发。

通过这样的机制,Vue3可以实现自动收集视图更新对应的依赖部分,当数据改变后,视图自动更新。

简单实现vue3双向绑定

  1. 创建一个vue.js 文件
// 存储effect函数的栈
const activeReactiveEffectStack = [];

// 把一个普通的对象转化为一个响应式的对象
function reactive(target) {
  // 创建一个Proxy代理
  const observed = new Proxy(target, {
    // 设置get函数
    get(target, key, receiver) {
      // 获取属性值
      const result = Reflect.get(target, key, receiver);
      // 如果当前有effect函数,则添加到activeReactiveEffectStack中
      if (activeReactiveEffectStack.length > 0) {
        track(target, key);
      }
      return result;
    },
    // 设置set函数
    set(target, key, value, receiver) {
      // 设置属性值
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (result && oldValue !== value) {
        // 触发更新
        trigger(target, key);
      }
      return result;
    }
  });
  return observed;
}

// 存储响应式对象及其对应的属性和依赖
const targetMap = new Map();

// 把当前的effect函数添加到targetMap中
function track(target, key) {
  // 获取当前的effect函数
  const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1];
  if (effect) {
    // 获取target对应的Map对象
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      // 如果不存在,则创建一个新的Map对象,并添加到targetMap中
      depsMap = new Map();
      targetMap.set(target, depsMap);
    }
    // 获取key对应的Set对象
    let dep = depsMap.get(key);
    if (!dep) {
      // 如果不存在,则创建一个新的Set对象,并添加到depsMap中
      dep = new Set();
      depsMap.set(key, dep);
    }
    // 把effect函数添加到Set对象中
    dep.add(effect);
  }
}

// 执行targetMap中存储的所有依赖于某个属性的effect函数
function trigger(target, key) {
  // 获取target对应的Map对象
  const depsMap = targetMap.get(target);
  if (depsMap) {
    // 获取key对应的Set对象
    const dep = depsMap.get(key);
    if (dep) {
      // 遍历Set对象,执行每个effect函数
      dep.forEach(effect => {
        effect();
      });
    }
  }
}

// 创建一个effect函数,并执行传入的函数
function effect(fn) {
  // 把fn添加到activeReactiveEffectStack中
  activeReactiveEffectStack.push(fn);
  // 执行fn
  fn();
  // 把fn从activeReactiveEffectStack中移除
  activeReactiveEffectStack.pop();
}

// 编译模板
function compile(el) {
  // 获取根元素
  let element = document.querySelector(el);
  if (element) {
    // 遍历根元素的子节点
    compileNodes(element.childNodes);
  }
}

// 遍历子节点
function compileNodes(nodes) {
  [...nodes].forEach(node => {
    if (node.nodeType === 1) {
      // 如果是元素节点,解析指令
      compileElement(node);
    } else if (node.nodeType === 3) {
      // 如果是文本节点,解析插值表达式
      compileText(node);
    }
    // 如果有子节点,递归遍历
    if (node.childNodes && node.childNodes.length > 0) {
      compileNodes(node.childNodes);
    }
  });
}

// 解析元素节点中的指令
function compileElement(node) {
  // 获取所有属性
  let attrs = node.attributes;
  [...attrs].forEach(attr => {
    // 获取属性名和属性值
    let attrName = attr.name;
    let attrValue = attr.value;
    if (attrName === 'v-model') {
      // 如果是v-model指令,创建一个effect函数,并绑定input事件监听器
      effect(() => {
        node.value = data[attrValue];
      });
      node.addEventListener('input', e => {
        data[attrValue] = e.target.value;
      });
    } else if (attrName.startsWith('v-on:')) {
      // 如果是v-on指令,获取事件名和方法名,并绑定事件监听器
      let eventName = attrName.slice(5);
      let methodName = attrValue;
      node.addEventListener(eventName, methods[methodName].bind(this));
    }
  });
}

// 解析文本节点中的插值表达式
function compileText(node) {
  // 获取文本内容,并匹配插值表达式
  let text = node.textContent;
  let reg = /\{\{(.+?)\}\}/g;
  let match = reg.exec(text);
  if (match) {
    // 获取第一个插值表达式的内容,并去除两端的空格
    let exp = match[1].trim();
    // 创建一个effect函数
    effect(() => {
      node.textContent = text.replace(match[0], data[exp]);
    });
  }
}
  1. 在页面中引入
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>双向绑定演示</title>
  <!-- 引入vue.js文件 -->
  <script src="vue.js"></script>
</head>
<body>
  <div id="app">
    <p>姓名:{{name}}</p>
    <p>年龄:{{age}}</p>
    <input type="text" v-model="name">
    <input type="number" v-model="age">
    <button v-on:click="sayHello">打招呼</button>
  </div>
  <script>
    // 创建一个响应式对象
    const data = reactive({
      name: '张三',
      age: 18
    });

    // 创建一个methods对象,存储方法
    const methods = {
      sayHello() {
        alert(`你好,我叫${data.name},我今年${data.age}岁`);
      }
    };

    // 编译模板
    compile('#app');
  </script>
</body>
</html>
文档更新时间: 2023-08-11 15:15   作者:朱勇老师