( 第五篇 )仿寫'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; },
嘿嘿僅需小小的改動, 就作到了這麼神奇的事情.
這個框架剛剛作了一點點就已經出現不少性能問題了, 接下來我會針對取值問題進行一次深層次的優化, 想一想還挺興奮.
下一集: