( 第五篇 )仿写'Vue生态'系列___"解析模板事件"css
本次任务 html
项目里面的取值操做, 我以前一直采用的都是eval函数, 可是前段时间忽然发现一个特别棒的函数Function, 下面我来演示一下他的神奇之处.vue
1. 能够执行字符串node
let fn1 = new Function('var a = 1;return a'); console.log(fn1()); // 1
2. 能够传递参数
下面写的name与age就是传入函数的两个参数,git
let fn2 = new Function('name','age', ' return name+age'); console.log(fn2('lulu',24)); // lulu24
第二种传参方式github
let fn3 = new Function('name, age', ' return name+age'); console.log(fn3('lulu',24)); // lulu24
综上我能够推断, 他的原理是把最后一个参数当作执行体, 而后前面若是有参数就被当作新生成函数的参数.express
3. 全局做用域
他执行的时候里面的做用域是全局的, 就算在函数内部, 执行时候也取不到函数内部的值, 因此想要使用的值, 都须要咱们手动传进去.segmentfault
// 报错了, 找不到u function cc(){ let u = 777; let fn = new Function('var a = 5;console.log(u); return a'); console.log(fn()); } cc()
// 执行成功 function cc(){ u = 777; // 直接挂在window上 let fn = new Function('var a = 5;console.log(u); return a'); // 777 console.log(fn()); // 5 } cc()
我也试了一下, 里面的var a 并不会污染全局, 放心使用吧;设计模式
把它介绍清楚了, 我就能够用它来替换以前写的eval了
expression: 表达式, 例如 'obj[name].age'数组
getVal(vm, expression) { let result, __whoToVar = ''; for (let i in vm.$data) { __whoToVar += `let ${i} = vm['${i}'];`; } __whoToVar = `${__whoToVar} return ${expression}`; result = new Function('vm', __whoToVar)(vm); return result; },
这里之后还会改为一个公用的获取变量的'池', 应该会下一章去作.
所谓指令固然是要绑定在元素的身上, 咱们有一个compileElement方法来处理元素节点, 那么正好利用他来让咱们分出一个指令处理模块.
好比说指令, 本次咱们来作v-show指令.
事件的话就是全部的原生事件.
compileElement(node) { let attributes = node.attributes; [...attributes].map(attr => { let name = attr.name, value = attr.value, obj = this.isDirective(name); if (obj.type === '指令') { CompileUtil.dir[obj.attrName] && CompileUtil.dir[obj.attrName]( this.vm, node, CompileUtil.getVal(this.vm, value), value ); } else if (obj.type === '事件') { // 当前只处理了原生事件; if(CompileUtil.eventHandler.list.includes(obj.attrName)){ CompileUtil.eventHandler.handler(obj.attrName,this.vm, node, value); }else{ // eventHandler[obj.attrName] 这个事件不是原生挂载事件, 不能用handler 处理 } } }); }
上面有一个isDirective事件, 这个事件也是一个关键点.
咱们如今分红四种形式.
判断出类型, 切分出后面的指令名称与参数, 返回给处理程序.
isDirective(attrName) { if (attrName.startsWith('c-')) { return { type: '指令', attrName: attrName.split('c-')[1] }; } else if (attrName.startsWith(':')) { return { type: '变量', attrName: attrName.split(':')[1] }; } else if (attrName.startsWith('v-on:')) { return { type: '事件', attrName: attrName.split('v-on:')[1] }; } else if (attrName.startsWith('@')) { return { type: '事件', attrName: attrName.split('@')[1] }; } return {}; }
cc_vue/src/CompileUtil.js
这里面专门抽出一个指令处理模块, 暂命名为dir.
本次就以 c-html 与 c-show 为例
c-html 顾名思义, 就是用户传一段html代码, 而后我把它注入到dom结构中
dir: { html(vm, node, value, expr) { // 只有这样一个操做就能够了, 没有任何高深的东西 node.innerHTML = value; // 这里别忘了用watcher订阅一下变化, 达到双向绑定的效果. new Watcher(vm, expr, (old, newVale) => { node.innerHTML = newVale; }); } },
热身以后剩下的这个'c-center'与'c-show'就很是有趣了
综上分析得出两种方案:
第一种: 把全部外在因素所有考虑进来, 每次进行总体分析, 得出具体的结论究竟是'block'仍是'none' 也多是 'flex' 与 'grid' 等等的.
第二种: 本次我想另辟蹊径的方法, 动态插入'css'代码, 这个想法挺有意思吧, 框架执行时, 先插入一段css代码, 而后能够利用这个css作不少不少有趣的事, 这方面之后会有扩展.
独立出一个插入'css'代码的模块.
单独new一下
cc_vue/src/index.js
import CCStyle from './CCStyle.js'; class C { constructor(options) { for (let key in options) { this['$' + key] = options[key]; } new CCStyle(); // ...
cc_vue/src/CCStyle.js
class CCStyle { constructor() { // 我要把它插到最上, js里面没有插到第一个位置这样的语句, 我只能获取到第一个元素, 而后插在他的前面. let first = document.body.firstChild, style = document.createElement('style'); // 固然是作一个style标签. // 这里先定一个c-show的绝对隐藏属性. style.innerText='.cc_vue-hidden{display:noneimportant}'; // 放进去就生效了, 之后控制v-show就只须要为元素添加与移除这个class名字就能够了. document.body.insertBefore(style, first); } } export default CCStyle;
上面的代码明显不符合设计模式, 咱们来把它的'可扩展性'优化一下.
class CCStyle { constructor() { let first = document.body.firstChild, style = document.createElement('style'), typeList = this.typeList(); // 无论具体的属性是什么, 咱们只管在这里面循环出来, 而后拼接上去,这里咱们本身压缩一下他. for (let key in typeList) { style.innerText += `.${key}{${typeList[key]}}\n`; } document.body.insertBefore(style, first); } // 这里面咱们能够分门别类的扩展不少属性. typeList() { return { // 1: 控制元素隐藏的 'cc_vue-hidden': 'display:none!important' // 2: 控制元素上下左右居中的 'cc_vue-center':'display: flex;justify-content: center;align-items: center;' }; } } export default CCStyle;
v-center 指令
cc_vue/src/CompileUtil.js
center(vm, node, value, expr) { value ? node.classList.remove('cc_vue-center') : node.classList.add('cc_vue-center'); new Watcher(vm, expr, (old, newVale) => { newVale ? node.classList.remove('cc_vue-center') : node.classList.add('cc_vue-center'); }); }
c-show的原理与上面是同样的
show(vm, node, value, expr) { value ? node.classList.remove('cc_vue-hidden') : node.classList.add('cc_vue-hidden'); new Watcher(vm, expr, (old, newVale) => { newVale ? node.classList.remove('cc_vue-hidden') : node.classList.add('cc_vue-hidden'); }); },
methods 晚于 data定义, 在用户出现重复定义的时候, 要给一个友好的提示.
cc_vue/src/index.js
class C { constructor(options) { // ... // proxyVm $data以后来处理$methods this.proxyVm(this.$methods, this, true);
绑定函数要稍做改变, 只要不传target 就是与vm实例绑定, noRepeat是否检测重复数据, 也就是报不报错.
proxyVm(data = {}, target = this, noRepeat = false) { for (let key in data) { if (noRepeat && target[key]) { // 防止data里面的变量名与其余属性重复 throw Error(`变量名${key}重复`); } Reflect.defineProperty(target, key, { enumerable: true, // 描述属性是否会出如今for in 或者 Object.keys()的遍历中 configurable: true, // 描述属性是否配置,以及能否删除 get() { return Reflect.get(data, key); }, set(newVal) { if (newVal !== data[key]) { Reflect.set(data, key, newVal); } } }); } }
处理好methods的数据了, 就要处理事件的绑定了.
分配的逻辑以前已经展现过了
// 若是事件列表里面有这个事件, 那么就绑定这个事件. if(CompileUtil.eventHandler.list.includes(obj.attrName)){ CompileUtil.eventHandler.handler(obj.attrName,this.vm, node, value); }
cc_vue/src/CompileUtil.js
专门处理事件的模块
eventHandler: { // 这个选项用来维护可处理的原生事件, 下面只是举例并不全面. list: [ 'click', 'mousemove', 'dblClick', 'mousedown', 'mouseup', 'blur', 'focus' ], // 肯定含有事件时进行的操做 handler(eventName, vm, node, type) { // ... } } }
handler要解决的问题形式
那咱们就来分步处理这几种状况吧.
handler(eventName, vm, node, type) { // 第一步: 匹配一个是否含有'()'; if (/\(.*\)/.test(type)) { // 第二步: 把'()'里面的内容拿出来 let str = /\((.*)\)/.exec(type)[1]; // 去除空格 str = str.replace(/\s/g, ''); // 以"("分割, 取到事件名字 type = type.split('(')[0]; // '()'里面有内容才进行这一步; if (str) { // 第三步: 参数化'组' let arg = str.split(','); // 第四部: 绑定事件与解析参数 node.addEventListener( eventName, e => { // 循环这个参数组 for (let i = 0; i < arg.length; i++) { // 这样就作到了$event的映射关系 arg[i] === '$event' && (arg[i] = e); } vm[type].apply(vm, arg); }, false ); return; } } // 第二步: 不带括号的直接挂就好了 node.addEventListener( eventName, () => { vm[type].call(vm); // this确定指向vm, 毕竟用户要使用$data等等属性 }, false ); }
上面没有对参数为$data上的变量的状况时作处理, 由于没有太大的必要, 之后写到 c-for的时候, 会着重的改写一下这边的逻辑.
咱们使用vue开发的时候, 只容许在模板中使用表达式, 此次我玩的这个项目, 容许用户使用任何形式去写, 固然了这样有一些性能之类的弊端, 可是为了好玩, 什么我都愿意尝试, 摒弃了return出值的写法, 采起了callback的模式.
关键字 cc_cb(value) value就是要传出来的值.
用法以下:
<div> {{ if(n > 3){ cc_cb(n) }else{ cc_cb('n小于等于3') }; }} </div>
其实这种功能并不复杂, 只是书写起来挺讨厌的, 并且太太太违背设计模式了.
只须要改变getVal函数
getVal(vm, expression) { let result, __whoToVar = ''; for (let i in vm.$data) { __whoToVar += `let ${i} = vm['${i}'];`; } // 检测到存在cc_cb被调用的状况时 if (/cc_cb/.test(expression)) { // 无非就是把返回的值, return出来 __whoToVar = `let _res;function cc_cb(v){ _res = v;}${__whoToVar}${expression};return _res`; } else { __whoToVar = `${__whoToVar} return ${expression}`; } result = new Function('vm', __whoToVar)(vm); return result; },
嘿嘿仅需小小的改动, 就作到了这么神奇的事情.
这个框架刚刚作了一点点就已经出现不少性能问题了, 接下来我会针对取值问题进行一次深层次的优化, 想一想还挺兴奋.
下一集: