前言
MVVM 是当前时代前端日常业务开发中的必备模式(相关框架如react,vue,angular 等), 使用 MVVM 可以将开发者的精力更专注于业务上的逻辑,而不需要关心如何操作 dom。虽然现在都 9012 年了,mvvm 相关原理的介绍已经烂大街了,但出于学习基础知识的目的(使用 proxy 实现的 vue3.0 还在开发中), 在参考了之前 vue.js 的整体思路之后,自己动手实现了一个简易的通过 proxy 实现的 mvvm。
本项目代码已经开源在github,项目正在持续完善中,欢迎交流学习,喜欢请点个 star 吧!
最终效果
<html>
 <body>
  <div id="app">
   <div>{{title}}</div>
  </div>
 </body>
</html>
import MVVM from '@fe_korey/mvvm';
new MVVM({
 view: document.getElementById('app'),
 model: {
  title: 'hello mvvm!'
 },
 mounted() {
  console.log('主程编译完成,欢迎使用MVVM!');
 }
});
结构概览
Complier模块实现解析、收集指令,并初始化视图Observer模块实现了数据的监听,包括添加订阅者和通知订阅者Parser模块实现解析指令,提供该指令的更新视图的更新方法Watcher模块实现建立指令与数据的关联Dep模块实现一个订阅中心,负责收集,触发数据模型各值的订阅列表
流程为:Complier收集编译好指令后,根据指令不同选择不同的Parser,根据Parser在Watcher中订阅数据的变化并更新初始视图。Observer监听数据变化然后通知给 Watcher ,Watcher 再将变化结果通知给对应Parser里的 update 刷新函数进行视图的刷新。
模块详解
Complier
将整个数据模型 data 传入Observer模块进行数据监听
this.$data = new Observer(option.model).getData();
循环遍历整个 dom,对每个 dom 元素的所有指令进行扫描提取
function collectDir(element) {
 const children = element.childNodes;
 const childrenLen = children.length;
 for (let i = 0; i < childrenLen; i++) {
  const node = children[i];
  const nodeType = node.nodeType;
  if (nodeType !== 1 && nodeType !== 3) {
   continue;
  }
  if (hasDirective(node)) {
   this.$queue.push(node);
  }
  if (node.hasChildNodes() && !hasLateCompileChilds(node)) {
   collectDir(element);
  }
 }
}
对每个指令进行编译,选择对应的解析器Parser
const parser = this.selectParsers({ node, dirName, dirValue, cs: this });
将得到的解析器Parser传入Watcher,并初始化该 dom 节点的视图
const watcher = new Watcher(parser);
parser.update({ newVal: watcher.value });
所有指令解析完毕后,触发 MVVM 编译完成回调$mounted()
this.$mounted();
使用文档碎片document.createDocumentFragment()来代替真实 dom 节点片段,待所有指令编译完成后,再将文档碎片追加回真实 dom 节点
let child;
const fragment = document.createDocumentFragment();
while ((child = this.$element.firstChild)) {
 fragment.appendChild(child);
}
//解析完后
this.$element.appendChild(fragment);
delete $fragment;
Parser
在Complier模块编译后的指令,选择不同听解析器解析,目前包括ClassParser,DisplayParser,ForParser,IfParser,StyleParser,TextParser,ModelParser,OnParser,OtherParser等解析模块。
switch (name) {
 case 'text':
  parser = new TextParser({ node, dirValue, cs });
  break;
 case 'style':
  parser = new StyleParser({ node, dirValue, cs });
  break;
 case 'class':
  parser = new ClassParser({ node, dirValue, cs });
  break;
 case 'for':
  parser = new ForParser({ node, dirValue, cs });
  break;
 case 'on':
  parser = new OnParser({ node, dirName, dirValue, cs });
  break;
 case 'display':
  parser = new DisplayParser({ node, dirName, dirValue, cs });
  break;
 case 'if':
  parser = new IfParser({ node, dirValue, cs });
  break;
 case 'model':
  parser = new ModelParser({ node, dirValue, cs });
  break;
 default:
  parser = new OtherParser({ node, dirName, dirValue, cs });
}
不同的解析器提供不同的视图刷新函数update(),通过update更新dom视图
//text.js
function update(newVal) {
 this.el.textContent = _toString(newVal);
}
OnParser 解析事件绑定,与数据模型中的 methods字段对应
//详见 https://github.com/zhaoky/mvvm/blob/master/src/core/parser/on.ts
el.addEventListener(handlerType, e => {
 handlerFn(scope, e);
});
ForParser 解析数组
详见 https://github.com/zhaoky/mvvm/blob/master/src/core/parser/for.ts
ModelParser 解析双向绑定,目前支持input[text/password] & textarea,input[radio],input[checkbox],select四种情况的双向绑定,双绑原理:
数据变化更新表单:跟其他指令更新视图一样,通过update方法触发更新表单的value
function update({ newVal }) {
 this.model.el.value = _toString(newVal);
}
表单变化更新数据:监听表单变化事件如input,change,在回调里set数据模型
this.model.el.addEventListener('input', e => {
 model.watcher.set(e.target.value);
});
Observer
MVVM 模型中的核心,一般通过 Object.defineProperty 的 get,set 方法进行数据的监听,在 get 里添加订阅者,set 里通知订阅者更新视图。在本项目采用 Proxy 来实现数据监听,好处有三:
Proxy 可以直接监听对象而非属性
Proxy 可以直接监听数组的变化
Proxy 有多达 13 种拦截方法,查阅
而劣势是兼容性问题,且无法通过 polyfill 磨平。查阅兼容性
注意 Proxy 只会监听自身的每一个属性,如果属性是对象,则该对象不会被监听,所以需要递归监听
设置监听后,返回一个 Proxy 替代原数据对象
var proxy = new Proxy(data, {
 get: function(target, key, receiver) {
  //如果满足条件则添加订阅者
  dep.addDep(curWatcher);
  return Reflect.get(target, key, receiver);
 },
 set: function(target, key, value, receiver) {
  //如果满足条件则通知订阅者
  dep.notfiy();
  return Reflect.set(target, key, value, receiver);
 }
});
Watcher
在 Complier 模块里对每一个解析后的 Parser 进行指令与数据模型直接的绑定,并触发 Observer 的 get 监听,添加订阅者(Watcher)
this._getter(this.parser.dirValue)(this.scope || this.parser.cs.$data);
当数据模型变化时,就会触发 -> Observer 的 set 监听 -> Dep 的 notfiy 方法(通知订阅者的所有订阅列表) -> 执行订阅列表所有 Watcher 的 update 方法 -> 执行对应 Parser 的 update -> 完成更新视图
Watcher 里的 set 方法用于设置双向绑定值,注意访问层级
Dep
MVVM的订阅中心,在这里收集数据模型的每个属性的订阅列表- 包含添加订阅者,通知订阅者等方法
 - 本质是一种发布/订阅模式
 
class Dep {
 constructor() {
  this.dependList = [];
 }
 addDep() {
  this.dependList.push(dep);
 }
 notfiy() {
  this.dependList.forEach(item => {
   item.update();
  });
 }
}
后记
目前该 mvvm 项目只实现了数据绑定和视图更新的功能,通过这个简易轮子的实现,对 dom 操作,proxy,发布订阅模式等若干基础知识都进行了再次理解,查漏补缺。同时欢迎大家一起探讨交流,后面会继续完善!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
proxy,MVVM库
稳了!魔兽国服回归的3条重磅消息!官宣时间再确认!
昨天有一位朋友在大神群里分享,自己亚服账号被封号之后居然弹出了国服的封号信息对话框。
这里面让他访问的是一个国服的战网网址,com.cn和后面的zh都非常明白地表明这就是国服战网。
而他在复制这个网址并且进行登录之后,确实是网易的网址,也就是我们熟悉的停服之后国服发布的暴雪游戏产品运营到期开放退款的说明。这是一件比较奇怪的事情,因为以前都没有出现这样的情况,现在突然提示跳转到国服战网的网址,是不是说明了简体中文客户端已经开始进行更新了呢?
更新动态
- 小骆驼-《草原狼2(蓝光CD)》[原抓WAV+CUE]
 - 群星《欢迎来到我身边 电影原声专辑》[320K/MP3][105.02MB]
 - 群星《欢迎来到我身边 电影原声专辑》[FLAC/分轨][480.9MB]
 - 雷婷《梦里蓝天HQⅡ》 2023头版限量编号低速原抓[WAV+CUE][463M]
 - 群星《2024好听新歌42》AI调整音效【WAV分轨】
 - 王思雨-《思念陪着鸿雁飞》WAV
 - 王思雨《喜马拉雅HQ》头版限量编号[WAV+CUE]
 - 李健《无时无刻》[WAV+CUE][590M]
 - 陈奕迅《酝酿》[WAV分轨][502M]
 - 卓依婷《化蝶》2CD[WAV+CUE][1.1G]
 - 群星《吉他王(黑胶CD)》[WAV+CUE]
 - 齐秦《穿乐(穿越)》[WAV+CUE]
 - 发烧珍品《数位CD音响测试-动向效果(九)》【WAV+CUE】
 - 邝美云《邝美云精装歌集》[DSF][1.6G]
 - 吕方《爱一回伤一回》[WAV+CUE][454M]
 
                        