一、主要功能
- 实现响应式
- 处理 {{ }}
- 实现 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-bindv-onv-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; }