我是靠谱客的博主 爱听歌铅笔,最近开发中收集的这篇文章主要介绍【vue3】Proxy手写Vue数据双向绑定和指令实现一个简单的vue3,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

实现一个简单的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(/^{{([sS]+)}}$/)) {
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(/^{{([sS]+)}}$/)) {
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:([sS]+)$/)) {
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:([sS]+)$/)) {
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(/^{{([sS]+)}}$/)) {
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:([sS]+)$/) || attr.name.match(/^:([sS]+)$/)) {
let attrName = RegExp.$1.trim();
effect(() => node.setAttribute(attrName, this.data[attr.value]))
}
// v-on
if(attr.name.match(/^v-on:([sS]+)$/) || attr.name.match(/^@([sS]+)$/)) {
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;
}

最后

以上就是爱听歌铅笔为你收集整理的【vue3】Proxy手写Vue数据双向绑定和指令实现一个简单的vue3的全部内容,希望文章能够帮你解决【vue3】Proxy手写Vue数据双向绑定和指令实现一个简单的vue3所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(57)

评论列表共有 0 条评论

立即
投稿
返回
顶部