一、主要功能
- 实现响应式
- 处理 {{ }}
- 实现 v-model
- 实现 v-bind
- 实现 v-on
二、实现思路
实现响应式
通过使用 Proxy 实现数据劫持 ,通过 effect 函数实现数据响应,从而更新页面内容.
- get 阶段
- 在 get 中保存当前依赖到 effects 上,由于 effects 是 Map 结构,于是可以将当前被访问对象当成对应 key 进行保存,其对应的 vlaue 初始为 Array,并根据 get 触发时将 当前的 currentEffect 存存储在 effects 中与当前对应的 effects[key] 中.
- set 阶段
- 当对被劫持的数据对象进行赋值更新时,会触发 set, 在 set 中会取出 effects 中与当前对应的依赖数组遍历执行,这一步是为了将更新后的数据同步到视图进行更新.
处理 tempalte 模板
当进行 new ToyVue 时,会通过模板中的 el 获取到真实的 dom,这样是为了便于在 effect 中最直接的更新视图.
- 获取到真实 dom 后,可以获取 dom 元素的文本节点,将文本节点中被 {{}} 包裹的内容替换成 data 中的值
- 基于 Dom Api 可以获取到对应的 dom 属性,包括自定义指令 v-bind v-on v-model
- 对于 v-bind 的处理,这里直接将 bind 的名称和数据内容,设置成为 dom 上的一个属性名和属性值
- 对于 v-on 的处理,通过 addEventListener 为 dom 实现事件注册,同时要保证 methods 中所有方法的 this 指向问题,这里默认让 this 指向了 this.data
- 对于 v-model 的处理,首先为 dom 设置 value 属性并指向 v-model 中对应 data 的值,同时为 dom 注册对应的事件,这里默认注册 input 事件,并在 input 事件中更新 data 中的值
- 遍历处理所有子节点,进行上述处理
三、具体实现
html
<div id="app" style="text-align: center;">
<h1>v-model</h1>
<p>{{ msg }}</p>
<input type="text" v-model="msg" />
<hr />
<h1>v-bind</h1>
<p v-bind:title="title">{{ title }}</p>
<hr />
<h1>v-on</h1>
<p>{{ reverseMessage }}</p>
<p v-on:click="reverseMessage">反转</p>
</div>
<script type="module">
import { ToyVue as Vue } from './toy-vue.js'
var app = new Vue({
el: '#app',
data: {
title: 'this is a title !!!',
msg: 'this is a message !!!',
reverseMessage:'this is reverseMessage !!!'
},
methods: {
reverseMessage(){
this.reverseMessage = this.reverseMessage.split('').reverse().join('');
}
}
});
</script>
toy-vue
export class ToyVue {
constructor(config) {
// 1. 模板 -> 真实 dom
this.tempalte = document.querySelector(config.el);
// 2. data -> 响应式 data
this.data = reactive(config.data);
// 3. 处理 methods
this.handleMethods(config.methods);
// 4. 处理 template
this.traversal(this.tempalte);
}
handleMethods(methods) {
for (let name in methods) {
// 保证方法中的 this 指向为 this.data
this[name] = methods[name].bind(this.data);
}
}
traversal(node) {
// 1. 处理文本节点,处理 {{}}
if (node.nodeType === Node.TEXT_NODE) {
console.log("textContent = ", node.textContent);
if (node.textContent.trim().match(/^{{([\s\S]+)}}$/)) {
let name = RegExp.$1.trim();
// 1.1 替换 {{ msg }} 为 data.msg
effect(() => {
node.textContent = this.data[name];
});
}
}
// 2. 处理 dom 元素 attributes 属性, 处理指令
if (node.nodeType === Node.ELEMENT_NODE) {
let attributes = node.attributes;
for (let attribute of attributes) {
console.log("attribute = ", attribute);
// 2.1 处理 v-model
if (attribute.name === "v-model") {
let name = attribute.value;
// 2.1.1 更新 dom 元素 value
effect(() => {
node.value = this.data[name];
});
// 2.1.2 给 dom 元素注册事件,根据不同表单类型注册不同事件
node.addEventListener("input", () => {
this.data[name] = node.value;
});
}
// 2.2 处理 v-bind
if (attribute.name.match(/^v\-bind:([\s\S]+)$/)) {
let attrName = RegExp.$1;
let name = attribute.value;
console.log("v-bind = ", attrName, name);
// 2.2.1 为 dom 元素设置对应属性和属性内容
effect(() => {
node.setAttribute(attrName, this.data[name]);
});
}
// 2.3 处理 v-on
if (attribute.name.match(/^v\-on:([\s\S]+)$/)) {
let attrName = RegExp.$1;
let name = attribute.value;
console.log("v-on = ", attrName, name);
// 2.2.1 为 dom 元素注册事件
effect(() => {
node.addEventListener(attrName, this[name]);
});
}
}
}
// 3. 递归处理子节点
if (node.childNodes && node.childNodes.length) {
for (let child of node.childNodes) {
this.traversal(child);
}
}
}
}
let effects = new Map();
let currentEffect = null;
// 收集依赖
function effect(fn) {
currentEffect = fn;
fn(); // 初始化执行,目的是为了自动收集依赖 depends
currentEffect = null;
}
// 响应式
function reactive(traget) {
let observe = new Proxy(traget, {
get(object, prop) {
console.log("get = ", object, prop);
// 1. 当前 currentEffect 存在,证明当前 effect 中依赖了当前响应式数据中的属性,此时要收集依赖
if (currentEffect) {
// 1.1 当前 effects 不存在和 object 对应属性, 收集依赖
if (!effects.has(object)) {
effects.set(object, new Map());
}
// 1.2 从 effects 中获到对应依赖项,判断当前访问 key 是否已存在对应依赖中,不存在则给定初始值为 array ,方便之后添加和删除
if (!effects.get(object).has(prop)) {
effects.get(object).set(prop, new Array());
}
// 1.3 将当前 currentEffect 存储在对应的 effects[object][prop] 中
effects.get(object).get(prop).push(currentEffect);
}
// 2. 返回当前访问属性对应值
return object[prop];
},
set(object, prop, value) {
console.log("set = ", object, prop, value);
// 1. 更新当前属性对应值
object[prop] = value;
// 2. 根据收集的依赖,执行 effect
if (effects.has(object) && effects.get(object).has(prop)) {
for (let effect of effects.get(object).get(prop)) {
effect();
}
}
return true;
},
});
return observe;
}