面試失敗貼之《如何動手建立一個簡單的MVVM框架》

薛定諤的?、發表於2019-03-10

第一次在掘金寫文章,難免有點小坎坷,各位看官請輕拍。

最近有刷面試題,在刷的過程中,發現如果把下面題目考察的原理結合起來即可實現一個簡單的 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 寫法:

  1. 當我們需要遍歷列表
render() {
  return (
    <ul>
      {
        list.map(item => <li>item</li>)
      }
    </ul>
  )
}
複製程式碼
  1. 當我們渲染值
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 的例子

  1. Count
  2. Todo App 有一點 bug, ?

水平有限,難免有不對之處,還請指出,謝謝.?

相關文章