实现一个简单的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
7var vm = new Vue({ el: '#app', data: { message: 'Hello Vue!' } })
我们先创建一个 index.html
和 my-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
2var 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/reactivity
中 effect
的源代码:
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
6let 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
27let 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
从效果上看我们已经实现了 reactive
和 effect
,但是我们这样实现有什么问题?我们每次 set
的时候,会执行所有的 effect
,如果我们有 m
个 effect
,n
个 property
,将会执行 m*n
次,性能上一定是有问题的!vue
实现 effect
的时候并没有像 react
一样 dependence
, 那么effect
真的只执行一次吗?
1
2effect(() => (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;
通过断点我们可以看到我们定义了两个 reactive
和 effect
,然后调用 set
的时候,只调用了一次 effect
,我们完成了依赖收集,set
调用的时候该依赖谁就依赖谁。我们可以看到我们定义了一个 counter,当我们修改了 counter
的属性后,effect 就会执行,我们的 dummy
就会随着改变。如果你还不能理解 dummy
的修改,我们可以将例子中 effect
的结果 alert
出来,将 counter
挂载到 window
对象上:
示例:
1
2
3
4let counter = reactive({ num: 1 }); window.counter = counter; effect(() => alert(counter.num));
然后我们在控制台中修改 counter
的属性,我们发现我们只要不修改 counter
的 num
属性,就不会 alert
,而一旦修改 num
的值,立马会 alert
出 num
修改后的值,很神奇吧,这就是双向绑定很重要的一部分,可监听的对象,我们的 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
24export 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-on
和 v-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
11constructor(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数据双向绑定和指令实现一个简单内容请搜索靠谱客的其他文章。
发表评论 取消回复