我是靠谱客的博主 爱听歌铅笔,这篇文章主要介绍【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的数据双向绑定:

复制代码
1
2
3
4
5
<div id="app"> <p>{{ message }}</p> <input v-model="message"> </div>
复制代码
1
2
3
4
5
6
7
var vm = new Vue({ el: '#app', data: { message: 'Hello Vue!' } })

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

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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的构造。

复制代码
1
2
3
4
5
6
7
8
9
10
11
// 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 实例。

复制代码
1
2
var proxy = new Proxy(target, handler);

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

reactive实现:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 核心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 的源代码:

复制代码
1
2
3
4
5
6
7
8
9
10
// 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的值也随着修改。我们也可以实现一个简化的版本。

复制代码
1
2
3
4
5
6
let effects = []; function effect(fn) { effects.push(fn); fn(); }

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

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 真的只执行一次吗?

复制代码
1
2
effect(() => (dummy = counter.num), [counter]);

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

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 定义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 对象上:

示例:

复制代码
1
2
3
4
let counter = reactive({ num: 1 }); window.counter = counter; effect(() => alert(counter.num));

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

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

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 遍历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:

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

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

复制代码
1
2
3
4
5
6
// v-bind if(attr.name.match(/^v-bind:([sS]+)$/)) { let attrName = RegExp.$1.trim(); effect(() => node.setAttribute(attrName, this.data[attr.value])) }

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

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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>
复制代码
1
2
3
4
5
6
7
// 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 的指向:

复制代码
1
2
3
4
5
6
7
8
9
10
11
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代码:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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实现代码:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// 自己实现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数据双向绑定和指令实现一个简单内容请搜索靠谱客的其他文章。

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

评论列表共有 0 条评论

立即
投稿
返回
顶部