面试失败贴之《如何动手创建一个简单的MVVM框架》

1,069 阅读4分钟

第一次在掘金写文章,难免有点小坎坷,各位看官请轻拍。

最近有刷面试题,在刷的过程中,发现如果把下面题目考察的原理结合起来即可实现一个简单的 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, 😅

水平有限,难免有不对之处,还请指出,谢谢.😄