一、主要功能
- 实现响应式
- 处理 {{ }}
- 实现 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">javascript">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. 模板 -> 真实 domthis.tempalte = document.querySelector(config.el);// 2. data -> 响应式 datathis.data = reactive(config.data);// 3. 处理 methodsthis.handleMethods(config.methods);// 4. 处理 templatethis.traversal(this.tempalte);}handleMethods(methods) {for (let name in methods) {// 保证方法中的 this 指向为 this.datathis[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.msgeffect(() => {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-modelif (attribute.name === "v-model") {let name = attribute.value;// 2.1.1 更新 dom 元素 valueeffect(() => {node.value = this.data[name];});// 2.1.2 给 dom 元素注册事件,根据不同表单类型注册不同事件node.addEventListener("input", () => {this.data[name] = node.value;});}// 2.2 处理 v-bindif (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-onif (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(); // 初始化执行,目的是为了自动收集依赖 dependscurrentEffect = 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. 根据收集的依赖,执行 effectif (effects.has(object) && effects.get(object).has(prop)) {for (let effect of effects.get(object).get(prop)) {effect();}}return true;},});return observe;
}