我们都知道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.html
和 my-vue.js
文件,按照上面 Vue
的写法来书写我们的页面:
<div id="app">
<p>{{message}}p>
<input type="text" v-model="message"/>
div>
<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/reactivity
中 effect
的源代码:
// 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
从效果上看我们已经实现了 reactive
和 effect
,但是我们这样实现有什么问题?我们每次 set
的时候,会执行所有的 effect
,如果我们有 m
个 effect
,n
个 property
,将会执行 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;
通过断点我们可以看到我们定义了两个 reactive
和 effect
,然后调用 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
的属性,我们发现我们只要不修改 counter
的 num
属性,就不会 alert
,而一旦修改 num
的值,立马会 alert
出 num
修改后的值,很神奇吧,这就是双向绑定很重要的一部分,可监听的对象,我们的 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-on
和 v-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;
}