如何動手建立一個簡單的MVVM框架
第一次在掘金寫文章,難免有點小坎坷,各位看官請輕拍。
最近有刷面試題,在刷的過程中,發現如果把下面題目考察的原理結合起來即可實現一個簡單的 MVVM 框架。
注: 下面不會給出直接答案,只是我的一些歷程。
面試題
Vue 中是如何解析 template 字串為 VNode 的?
在接觸 React 時候,我只瞭解到通過 babel 可以把 JSX 轉成 VNode(通過呼叫 React.createElement 方法),但是對其具體是如何轉換的卻不瞭解。
很明顯,回答失敗。通過 github 上搜索 template+vnode 的關鍵詞,讓我搜到了htm
庫,發現簡直就是我想要的。讓我們看下用法:
const htm = require("htm"); function h(type, props, ...children) { return { type, props, children }; } const html = htm.bind(h); html` <div>Hello World</div> `; // 返回: { type: 'div', props: null, children: ['Hello World'] } 複製程式碼
htm 的大概思路是通過一個個字元遍歷 template 字串,並設定狀態型別,當遇到<>表示進入元素狀態,遇到="'則表示屬性狀態。子元素的關係通過陣列的 push 和 slice 某一位來確定。 更詳細可以看看這篇文章如何解析 template 成 VNODE
為什麼要用 VNode?
我想這裡應該是通過比較 VNode 和 DOM,並給出 VNode 的優勢和 DOM 的不足。
當前 Vue 和 React 都使用了 VNode,是出於什麼原因,讓兩大目前最火熱的框架都選擇使用了 VNode 呢?
這裡我們直接看下寫的比較好的文章吧.深度剖析:如何實現一個 Virtual DOM 演算法
瞭解到上面知識的大致原理後,回顧了下 React 的 JSX 寫法:
- 當我們需要遍歷列表
render() { return ( <ul> { list.map(item => <li>item</li>) } </ul> ) } 複製程式碼
- 當我們渲染值
render() { return ( <p>{{ msg }}</p> ) } 複製程式碼
思考了下,如果結合 ejs 等模板引擎(這些模板引擎大致的思路是結合 template+data->html->設定到 DOM 的 innerHTML),先把資料填充進去,轉變成 html 字串。
之後使用htm
轉成 VNode,再使用 Virtual Dom,使用 Virtual Dom 的 diff 和 patch,便可以實現了簡單的 MVVM 體驗。
沒錯,就是這麼簡單,廢話不多說,開幹吧。
MVVM
模板引擎
<!-- 比如我們需要渲染陣列列表: --> <ul> <% for (let item of list) { %> <li></li> <% } %> </ul> <!-- 比如我們需要條件渲染 --> <% if (condition) { %> <span>open</span> <% } else { %> <span>close</span> <% } %> <!-- 比如我們需要渲染資料 --> <p><%= msg %></p> 複製程式碼
我的思路的先處理邏輯運算如:(for,if 等), 通過正則/<%[^=]([^%]*)%>/g
來匹配,並通過str += 匹配內容
, 因為 exec 會含有 index 屬性,所以匹配之前的 html 通過 slice 來獲取,並拼接到 str。
let _str = 'let str = "";\n'; let exec; let index = 0; let content; while ((exec = REG.exec(str))) { content = str_format(str.slice(index, exec.index)); if (content) { _str += `str += '${content}';\n`; } _str += `${str_format(exec[1])}\n`; index = exec.index + exec[0].length; } // some code 複製程式碼
處理完邏輯的程式碼,通過正則/<%=([^%]*)%>/g
直接對上面的字串進行 replace 操作替換。
具體程式碼:template.js
html 字串 -> VNode
這裡我們使用simple-virtual-dom 庫來實現虛擬 DOM 處理,我們對上面函式 h 做一點調整。
import { el } from "simple-virtual-dom"; import htm from "htm"; function h(tagName, props, ...children) { return new el(tagName, props, children); } const html = htm.bind(h); const vnode = html([html_str]); 複製程式碼
這裡我們就實現了template+data
->html str
->VNode
的轉換。使用 VNode 庫提供的 render 轉成具體的 DOM 並掛載到 document 上。
但是我們貌似還沒有對事件進行處理,這裡我使用了事件委託機制,也就是掛載事件到 window 物件上進行監聽處理。所以這裡需要對simple-virtual-dom
庫的 element.js 做一點小調整.
// 唯一Id let uid = 0; function Element(tagName, props, children) { // 給每個VNode增加uid this.uid = uid++; } Element.prototype.render = function() { for (var propName in props) { var propValue = props[propName]; // 這裡模仿vue的事件繫結 if (propName.startsWith("@")) { // 事件處理 const callback = (vm.$methods[propValue] || function() {}).bind(vm); delegate(window, `[dance-el-${this.uid}]`, propName.slice(1), callback); continue; } } // 新增uid屬性, 為了事件代理 _.setAttr(el, "dance-el-" + this.uid, ""); }; 複製程式碼
這樣,事件處理我們也解決好了,哦對了,對 delegate 實現原理感興趣的可以閱讀delegate原始碼
如何更新呢?
這裡我加入了 React 中的 setState,當我們呼叫這個方法,我們會得到新的 data 資料,這個時候再次觸發template+data
->html str
->VNode
的轉換.
然後使用 virtual dom 的 diff 和 patch 差異比較,修改只需改變的 DOM 元素。