第一次在掘金寫文章,難免有點小坎坷,各位看官請輕拍。
最近有刷面試題,在刷的過程中,發現如果把下面題目考察的原理結合起來即可實現一個簡單的 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 元素。
整體實現
大家可以點選這裡進行檢視MVVM
如果可以,還請給個 star,star 是面試加分項。?
基於我們建立的 MVVM 的例子
水平有限,難免有不對之處,還請指出,謝謝.?