【vue3】Proxy手写Vue数据双向绑定和指令

实现一个简单的vue3

我们都知道vue2响应式数据的原理:
整体思路是数据劫持 + 观察者模式

对象内部通过 defineReactive 方法,使用 Object.defineProperty 将属性进行劫持(只会劫持已存在的属性),数组则是通过重写数组来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存在它所依赖的 watcher (依赖收集)get,当属性变化后会通知自己对应的 watcher 去更新(派发更新)set。

1、Object.defineProperty 数据劫持。
2、使用 getter 收集依赖 ,setter 通知 watcher派发更新。
3、watcher 发布订阅模式。

Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化。

vue-next 是 vue3 的源码仓库,它的核心 @vue/reactivity 被单独划分了一个package。这个包提供了个核心的api。

effect

effect 是一个观察函数,它的作用是 收集依赖 。effect 接受一个函数,这个函数内部对于响应式数据的访问都可以收集依赖,在响应式数据更新之后,就会触发响应的更新事件。

reactive

响应式机制核心 api ,将传入的对象转换为 proxy ,劫持上面所有属性的 getter 、setter 等方法,从而在访问数据的时候收集依赖(也就是 effect 函数),在修改数据的时候触发更新。

ref

reactive 函数可以将对象转换为响应式,不能转换基本类型,而 ref 函数可以转换基本类型,原理就是将基本类型用对象包装了一下,ref(0) 相当于 reactive({value: 0})

computed

计算属性,依赖值更新以后,它的值也会随之自动更新。其实 computed 内部也是一个 effect

我们用Proxy,reactive和effect来实现vue的数据双向绑定:

<div id="app">
  <p>{{ message }}</p>
  <input v-model="message">
</div>
var vm = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

我们先创建一个 index.htmlmy-vue.js文件,按照上面 Vue 的写法来书写我们的页面:

<div id="app">
  <p>{{message}}</p>
  <input type="text" v-model="message"/>
</div>

<!-- 为了支持import导入js,type=module -->
<script type="module">
  import { MyVue as Vue } from './my-vue.js';
  var app = new Vue({
    el: '#app',
    data: {
      message: 'Hello Vue'
    }
  })
</script>

然后本地启动一个 Web Services, 我们可以通过 npx http-server -p 3000 启动一个本地服务,设置端口为3000, 默认端口为8080。服务启动以后我们就可以在浏览器运行我们的页面,然后我们开始编写我们的 my-vue.js,我们看js的写法,是通过 new Vue来创建一个实例对象,通过 el, data绑定模板和数据,我们先实现 myVue的构造。

// my-vue.js
// 定义myvue类
export class MyVue {
  // 构造方法
  constructor(config) {
    // this关键字则代表实例对象
    this.template = document.querySelector(config.el);
    this.data = config.data;
  }
}

这样就简单实现了我们 vue 到导出与引用,然后我们来实现 reactive 来实现我们对数据的监听, 实现 reactive 的核心就是 ES6 中的 Proxy

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

var proxy = new Proxy(target, handler);

target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

reactive实现:

// 核心Proxy
const reactive = (object) => {
  const observed = new Proxy(object, {
    get(target, key) {
      console.log('get', target, key);
      return target[key];
    },
    set(target, key, value) {
      console.log('set:', target, key, value)
      target[key] = value;
      return value;
    }
  })

  return observed;
}

let data = reactive({a: 1});
data.a; // get {a: 1} 'a'
data.c = 2; // set: {a: 1} c 2

从上面我们看到,我们获取和修改对象的时候,通过 Proxy 可以实现一个数据的可监听,我们基本上实现了observed,接下来我们看一下 vue3 有一个比较神奇的东西 effect,我们看下面这段 vue3 核心 @vue/reactivityeffect 的源代码:

// reactivity/__tests__/effect.spec.ts
it('should observe basic properties', () => {
  let dummy;
  const counter = reactive({ num: 0 });
  effect(() => (dummy = counter.num));

  expect(dummy).toBe(0);
  counter.num = 7;
  expect(dummy).toBe(7);
})

当我们定义 dummy 变量, 创建一个 counter 对象,我们写了一个 effect,它里面是一个函数,函数里将 counter.num 赋值给 dummy,然后我们修改counter.num的值, dummy的值也随着修改。我们也可以实现一个简化的版本。

let effects = [];
function effect(fn) {
  effects.push(fn);
  fn();
}

我们在 set 的时候去调用 effect,然后我们把 vue 源码中 effect 的例子拿过来试一下:

let effects = [];
function effect(fn) {
  effects.push(fn);
  fn();
}

const reactive = (object) => {
  const observed = new Proxy(object, {
    get(target, key) {
      return target[key];
    },
    set(target, key, value) {
      target[key] = value;
      for(let effect of effects) {
        effect();
      }
      return value;
    }
  })

  return observed;
}

let dummy;
let counter = reactive({num: 1});
effect(() => (dummy = counter.num));
console.log('dummy1:', dummy); // 1
counter.num = 7;
console.log('dummy2:', dummy); // 7

从效果上看我们已经实现了 reactiveeffect,但是我们这样实现有什么问题?我们每次 set 的时候,会执行所有的 effect,如果我们有 meffectnproperty,将会执行 m*n 次,性能上一定是有问题的!vue实现 effect 的时候并没有像 react 一样 dependence, 那么effect 真的只执行一次吗?

effect(() => (dummy = counter.num), [counter]);

当然不是的,vue 在实现的时候,每一个 effect 在第一次执行的时候,都会做依赖收集, 我们每次调用set的时候,都会执行这个函数,如果我们用一种特殊的方式,我们就可以知道哪个 setter 对应 哪个 effect:

// 定义effect为Map对象
let effects = new Map();
let currentEffect = null;
function effect(fn) {
  currentEffect = fn;
  fn();
  currentEffect = null;
}

const reactive = (object) => {
  const observed = new Proxy(object, {
    get(target, key) {
      // 我们在get中做依赖收集
      if(currentEffect) {
        // 判断是否这个值
        if(!effects.has(target)) 
          effects.set(target, new Map());

        if(!effects.get(target)?.get(key))
          effects.get(target).set(key, new Array());
        // 如果想写更多的功能,方便后面删除等操作,effects可定义为Set类型,下面就不能用push用add添加  
        // 先写实现逻辑
        effects.get(target).get(key).push(currentEffect);
      }
      return target[key];
    },
    set(target, key, value) {
      target[key] = value;
      let _effects = effects?.get(target)?.get(key);
      if(_effects) {
        for(let effect of _effects) {
          effect();
        }
      }
      return value;
    }
  })

  return observed;
}

// 我们定义两个变量和reactive,然后调用set的时候,看effect执行了几次
let dummy, dummy2;
let counter = reactive({ num: 1 });
let counter2 = reactive({ num: 1 });
effect(() => (dummy = counter.num));
effect(() => (dummy2 = counter2.num));

counter.num = 7;

通过断点我们可以看到我们定义了两个 reactiveeffect,然后调用 set 的时候,只调用了一次 effect,我们完成了依赖收集,set 调用的时候该依赖谁就依赖谁。我们可以看到我们定义了一个 counter,当我们修改了 counter 的属性后,effect 就会执行,我们的 dummy 就会随着改变。如果你还不能理解 dummy 的修改,我们可以将例子中 effect 的结果 alert 出来,将 counter 挂载到 window 对象上:

示例:

let counter = reactive({ num: 1 });
window.counter = counter;
effect(() => alert(counter.num));

然后我们在控制台中修改 counter 的属性,我们发现我们只要不修改 counternum 属性,就不会 alert,而一旦修改 num 的值,立马会 alertnum 修改后的值,很神奇吧,这就是双向绑定很重要的一部分,可监听的对象,我们的 counter 现在就是一个可被监听的对象。

接下来我们看一下模板的部分,我们去遍历 template 里面的部分:

export class MyVue {
  constructor(config) {
    this.template = document.querySelector(config.el);
    this.data = config.data;
    this.traversal(this.template);
  }
  // 遍历template
  traversal(node) {
    // 如果节点类型为文本
    if(node.nodeType === Node.TEXT_NODE) {
      if(node.textContent.trim().match(/^{{([\s\S]+)}}$/)) {
        let name = RegExp.$1.trim();
        effect(() => node.textContent = this.data[name])
      }
    }
    // 用递归循环子节点
    if (node.childNodes && node.childNodes.length) {
      for (let child of node.childNodes) {
        this.traversal(child);
      }
    }
  }
}

至此我们已经实现了文字的绑定,然后我们来实现数据的双向绑定,我们来实现一个 v-model:

 // 遍历template
  traversal(node) {
    // 如果节点类型为文本
    if(node.nodeType === Node.TEXT_NODE) {
      if(node.textContent.trim().match(/^{{([\s\S]+)}}$/)) {
        let name = RegExp.$1.trim();
        effect(() => node.textContent = this.data[name])
      }
    }
    // 访问元素节点上的属性
    if(node.nodeType === Node.ELEMENT_NODE) {
      let attributes = node.attributes;
      for(let attr of attributes) {
        // console.log(attr);
        if(attr.name === 'v-model') {
          // console.log(attr.value);
          let name = attr.value;
          effect(() => node.value = this.data[name]);
          // 监听input变化,实现双向绑定
          node.addEventListener('input', () => this.data[name] = node.value);
        }
      }
    }
    // 用递归循环子节点
    if (node.childNodes && node.childNodes.length) {
      for (let child of node.childNodes) {
        this.traversal(child);
      }
    }
  }
}

我们已经实现了数据的双向绑定,然后我们也可以去试着去实现vue中的 v-onv-bind 指令:

v-bind:

<span v-bind:title="message">
  鼠标悬停几秒钟查看此处动态绑定的提示信息!
</span>

我们在节点循环匹配 v-bind 属性:

// v-bind
  if(attr.name.match(/^v\-bind:([\s\S]+)$/)) {
    let attrName = RegExp.$1.trim();
    effect(() => node.setAttribute(attrName, this.data[attr.value]))
  }

v-on 是类似的处理方法,我们来试一下:

<button v-on:click="reverseMessage">反转消息</button>
<script type="module">
  import { MyVue as Vue } from './src/js/toy-vue.js';
  var app = new Vue({
    el: '#app',
    data: {
      message: 'Hello Vue'
    },
    methods: {
      reverseMessage: function () {
        console.log(this)
        this.message = this.message.split('').reverse().join('')
      }
    }
  })
</script>
 // v-on
  if(attr.name.match(/^v-on:([\s\S]+)$/)) {
    let eventName = RegExp.$1.trim();
    let fnName = attr.value;
    node.addEventListener(eventName, this.methods[fnName]);
  }

而事件是写在 methods中的,我们直接通过 props 构造 this 的指向会被改变,所以我们需要在构造函数中来处理一下 this 的指向:

constructor(config) {
    this.template = document.querySelector(config.el);
    this.data = reactive(config.data);
    for(let name in config.methods) {
      this[name] = () => {
        config.methods[name].apply(this.data);
      }
    }
    this.traversal(this.template);
  }

我们就是实现了vue的数据双向绑定和一些指令的编写,下面是我们的完整代码:

html代码:

<div id="app">
  <p>{{message}}</p>
  <input type="text" v-model="message"/></br>
  <span v-bind:title="message">
    鼠标悬停几秒钟查看此处动态绑定的提示信息!
  </span></br>
  <button v-on:click="reverseMessage">反转消息</button>
</div>

<script type="module">
  import { MyVue as Vue } from './my-vue.js';
  var app = new Vue({
    el: '#app',
    data: {
      message: 'Hello Vue'
    },
    methods: {
      reverseMessage: function () {
        this.message = this.message.split('').reverse().join('')
      }
    }
  })
</script>

my-vue实现代码:

// 自己实现vue的绑定
export class MyVue {
  constructor(config) {
    this.template = document.querySelector(config.el);
    this.data = reactive(config.data);
    for(let name in config.methods) {
      // console.log(name)
      this[name] = () => {
        config.methods[name].apply(this.data);
      }
    };
    this.traversal(this.template);
  }
  traversal(node) {
    // 模板语法
    if(node.nodeType === Node.TEXT_NODE) {
      if(node.textContent.trim().match(/^{{([\s\S]+)}}$/)) {
        let name = RegExp.$1.trim();
        effect(() => node.textContent = this.data[name])
      }
    }
    // 访问元素节点上的属性
    if (node.nodeType === Node.ELEMENT_NODE) {
      let _attributes = node.attributes;
      for (let attr of _attributes) {
        if (attr.name === "v\-model") {
          let value = attr.value;
          // console.log('value', value)
          effect(() => (node.value = this.data[value]));
          node.addEventListener("input", () => (this.data[value] = node.value));
        }
        // v-bind 与 缩写:
        if(attr.name.match(/^v\-bind:([\s\S]+)$/) || attr.name.match(/^\:([\s\S]+)$/)) {
          let attrName = RegExp.$1.trim();
          effect(() => node.setAttribute(attrName, this.data[attr.value]))
        }
        // v-on
        if(attr.name.match(/^v\-on:([\s\S]+)$/) || attr.name.match(/^@([\s\S]+)$/)) {
          let eventName = RegExp.$1.trim();
          let fnName = attr.value;
          node.addEventListener(eventName, this[fnName]);
        }
      }
    }

    if (node.childNodes && node.childNodes.length) {
      for (let child of node.childNodes) {
        this.traversal(child);
      }
    }
  }
}

// 定义effect为Map对象
let effects = new Map();
let currentEffect = null;
function effect(fn) {
  currentEffect = fn;
  fn();
  currentEffect = null;
}

const reactive = (object) => {
  const observed = new Proxy(object, {
    get(target, key) {
      // 我们在get中做依赖收集
      if(currentEffect) {
        // 判断是否这个值
        if(!effects.has(target)) 
          effects.set(target, new Map());
          
        if(!effects.get(target)?.get(key))
          effects.get(target).set(key, new Array())
        // 先写实现逻辑
        effects.get(target).get(key).push(currentEffect);
      }
      return Reflect.get(target, key);
    },
    set(target, key, value) {
      // target[key] = value;
      Reflect.set(target, key, value);
      let _effects = effects?.get(target)?.get(key);
      if(_effects) {
        for(let effect of _effects) {
          effect();
        }
      }
      return value;
    }
  })

  return observed;
}
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

small_Axe

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值