MVC和MVVM,都是一种数据驱动视图的架构模型,相对于传统的面向过程式开发来说,它们是对代码规范的一种统一,方便团队进行开发。
如下俩段代码控制div是否显示:
<body>
<div id="box">我显示的div>
<button id="btn">点击button>
<script>
btn.onclick = function() {
if(box.style.display === 'none') box.style.display = 'block';
else box.style.display = 'none';
}
script>
body>
用数据驱动模型模型来写:
<body>
<div id="box">我显示的</div>
<button id="btn">点击</button>
<script>
let is_shown = true;
function render(el, is_shown) {
if(is_shown) el.style.display = 'block';
else el.style.display = 'none';
}
btn.onclick = function() {
is_shown = !is_shown;
render(box, is_shown);
}
</script>
</body>
看起来好像多了几行代码,但是对于第二种代码来说,简单抽象封装了 render
函数,我们只需要修改 is_shown
的 bool
值,而无需在意 render
函数内部的执行,就可以实现通过数据修改来驱动视图的更新。
1、mvc和mvvm都是一种设计思想。 主要就是mvc中Controller演变成mvvm中的viewModel。 mvvm主要解决了mvc中大量DOM操作使页面首次渲染性能降低,加载速度变慢的问题 。
学过vue.js或者react.js的友友们就会知道,这种框架在渲染页面时会生成一个虚拟DOM树(把页面元素解析成ast抽象语法树,再转化成DocumentFragment)
而我们使用框架时,实际上是在内存进行对虚拟dom的操作,最后再一次性渲染到文本文档中。
这样的好处是浏览器在做初始渲染时只需一次对真实dom的操作。
2、MVVM与MVC最大的区别就是:它实现了View和Model的自动同步:当Model的属性改变时,我们不用再自己手动操作Dom元素来改变View的显示,它会自动变化。
- MVC体现了面向对象编程的思维,而MVVM更是一种函数式编程的思维(后面文章会讲)
- 对于MVC来讲,MVC操作的是真实dom,对于数据的更新需要找到对应抽象类来直接操作真实dom,这样的话,它无法完全将修改视图的操作完全封装成一个方法,然后做到修改数据直接调用该方法做到视图更新(比如上面的render函数的el参数,无法做到准确定位el参数),它会在不同方法中穿插对dom的操作
- 而对于MVVM来讲,它操作的是虚拟dom、在数据的更新后,该框架重新生成一个虚拟dom树,与旧虚拟dom树进行比对,然后替换修改的地方,所以这里我们可以将渲染视图抽象成一个函数类
<div id="app"> <p v-on:click="clickMes">{{mes}}p> div>
- 在上面的代码中,MVVM框架会遍历该模板生成虚拟dom,找到 v-on 等自定义属性并进行事件绑定,做到真正的视图和数据分离
3、整体看来,MVVM比MVC精简很多,我们不用再用选择器频繁地操作DOM。
MVVM并不是用VM完全取代了C,ViewModel存在目的在于抽离Controller中展示的业务逻辑,而不是替代Controller,其它视图操作业务等还是应该放在Controller中实现。
从性能来说,其实非也!
对于页面首次渲染,MVVM框架可能会比MVC框架快一些,因为MVVM只会进行一次对真实dom的操作,而MVC可能会进行多次真实dom的操作
但是!在首屏渲染完毕后,用户开始对页面进行直接操作时,MVVM的性能肯定会输MVC的!
那MVVM究竟比MVC好在哪里?
对于程序来讲,MVVM肯定是比MVC用的爽啊!开发效率又高,因为完全不需要考虑视图更新方面对dom树的操作,框架会自动响应绑定对视图的更新(框架使用Object.defineProperty
或proxy
直接在数据修改时候自动调用 _render
函数进行更新)。
所以对于开发来说,用的爽才是好的,这也是为什么用 Vue
和 react
的人数比用 angular
的人数多的原因
MVC没啥好讲的,它其实也算不上一种模型,它只是对代码规范进行约束而已。
如下面简单实现一个点击 button 实现 div 中数字增长的效果。
DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>MVC Demotitle>
head>
<body>
<h1>MVC Demoh1>
<div id="counter">0div>
<button id="increment-btn">Incrementbutton>
<script>
// Model
const model = {
count: 0,
incrementCount: function() {
this.count++;
return this.count;
}
};
// View
const view = {
updateCount: function(count) {
const counter = document.getElementById('counter');
counter.innerHTML = count;
}
};
// Controller
const controller = {
handleClick: function() {
const newCount = model.incrementCount();
view.updateCount(newCount);
},
init: function() {
const button = document.getElementById('increment-btn');
button.addEventListener('click', this.handleClick);
}
};
controller.init();
script>
body>
html>
MVVM实现起来很复杂,只能模拟实现简单的功能,这里我们模拟实现一下 vue 框架。
我们使用 proxy
来进行响应式处理,对于 Object.defineProperty
和 proxy
和 碎片化文档
还有 设计模式
不熟的友友,可以查看一下文档再进行学习
Object.defineProperty() - JavaScript | MDN (mozilla.org)
Proxy - JavaScript | MDN (mozilla.org)
DocumentFragment - Web API 接口参考 | MDN (mozilla.org)
javascript的23种设计模式 - 掘金 (juejin.cn)
需要先了解以下算法:
// 实现一个 render(template, context) 方法,将 template 中占位符用 context 替换
var template = '{{ name }}现在{{ age }}岁'
var context = {name: 'lhy', age: 19}
console.log(render(template, context))
// 实现!直接用replace进行替换!
function render(template, context) {
return template.replace(/{{(.*?)}}/g, (match, key) => context[key.trim()]) // match 匹配{{xxx}}, key匹配(.*?)
}
对于复杂的模板替换可以用 with
和 eval
实现
{{ }}
里面是表达式的话!可以用 eval()
实现直接运算!with
进行模板解析const data = {
data: {
name: 'hy',
age: 19
},
school: 'cs'
};
// 对于vue来说,它传入的是一个object, 会出现以下情况
const template =
`{{ data.name }}今年{{ data.age < 17 ? '18': data.age }}岁了,在{{ school }}就读`;
/**
* 要么用split('.')
* @param {*} template
* @param {*} data
*/
function render(template, data) {
with(data) {
return template.replace(/{{([^}]*)}}/g, (macth, context) => {
return eval(`${context}`)
})
}
}
console.log(render(template, data));
但是在实现的时候,发现使用with会报错,显示在严格模式下不让使用,不知道怎么修改
compile_text(node) {
function isVariable(variable) { // 判断是不是变量
return !(variable === '' || parseInt(variable).toString() !== 'NaN' || (/[`'"]/).test(variable));
}
node.nodeValue = node.nodeValue.replace(/{{([^}]*)}}/g, (macth, context) => {
context = context.trim();
const exp = context.split(/[?:]/); // 三目运算符
const value = exp[0].trim();
// 变量,解决对于 data || age 这一类的,给data和age分别设置watcher
let variables = value.split(/[^\w.'"`]/);
variables.forEach(variable => {
variable = variable.trim();
if(isVariable(variable)) new Watcher(this.$vm, node, variable);
})
// 如果是三目运算符
if(exp.length === 3) {
// 运算表达式
if(eval(`this.$vm._data.${exp[0]}`)) {
if(isVariable(exp[1])) {
return eval(`this.$vm._data.${exp[1]}`);
}
return exp[1].trim().replace(/['"`]/g, '');
} else {
if(isVariable(exp[2])) {
return eval(`this.$vm._data.${exp[2]}`);
}
return exp[2].trim().replace(/['"`]/g, '');
}
}
// 变量
return eval(`this.$vm._data.${value}`)
})
}
下面参数 el 代表真实dom节点 ,比如
的 app 元素
// el 代表真实dom节点
nodeToFragment(el){ // 这里相当于生成虚拟dom(不是虚拟dom)
let fragment = document.createDocumentFragment();
// fragment 是一个指向空DocumentFragment对象的引用。是 DOM 节点。它们不是主 DOM 树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到 DOM 树。在 DOM 树中,文档片段被其所有的子元素所代替。
// 因为文档片段存在于内存中,并不在 DOM 树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。
let child;
while (child = el.firstChild){
// fragment.appendChild()具有移动性, 相当于把el中节点移动过去.nextElementSibling
fragment.appendChild(child);//append相当于剪切的功能
}
return fragment;
}
把真实dom转化为类虚拟dom,然后在进行js模板修改(修改{{mes}}为文本)和操作
观察者和被观察者模式,vue的响应式原理也是通过这种模式实现绑定的。
// 请补全JavaScript代码,完成"Observer"、"Observerd"类实现观察者模式。要求如下:
// 1. 被观察者构造函数需要包含"name"属性和"state"属性且"state"初始值为"走路"
// 2. 被观察者创建"setObserver"函数用于保存观察者们
// 3. 被观察者创建"setState"函数用于设置该观察者"state"并且通知所有观察者
// 4. 观察者创建"update"函数用于被观察者进行消息通知,该函数需要打印(console.log)数据,数据格式为:小明正在走路。其中"小明"为被观察者的"name"属性,"走路"为被观察者的"state"属性
// 注意:
// 1. "Observer"为观察者,"Observerd"为被观察者
class Observerd { // 被观察者
constructor(name) {
this.name = name
this.state = "走路"
this.observers = [] // 观察者队列
}
setObserver(observer) {
this.observers.push(observer)
}
setState(state) {
this.state = state
this.observers.forEach(observer => observer.update(this))
}
}
class Observer {
update(observerd) {
console.log(`${observerd.name}正在${observerd.state}`)
}
}
MVVM(数据双向绑定)
职责:
组成:
Vue数据双向绑定原理是通过数据劫持结合发布者-订阅者模式的方式来实现的,首先是对数据进行监听,然后当监听的属性发生变化时则告诉订阅者是否要更新,若更新就会执行对应的更新函数从而更新视图
- Dep功能 :1.收集依赖,添加观察者(watcher) 2.通知所有观察者
编译html模板时,发现需要特殊处理的变量,比如v-model=“name”,这个name被发现以后,就准备为其创建watcher,在创建watcher的时候,先把这个watcher挂载到Dep.target这个全局静态变量上,然后触发一次get事件,这样就触发了get函数中的Dep.target && dep.addSub(Dep.target);,等get到了变量以后,也已经添加到subs队列里了,这时候在令Dep.target = null。
总结:vue先用observer劫持监听所有属性,当数据变化时,会触发setter,通知变化并调用Dep.notify(),并通知watcher(连接observer和compile的桥梁),watcher调用 _update()并更新视图 (调用 _render()方法)
真的难,不过我考虑的还是挺全面的,比如深层代理响应,大家可以在浏览器开发工具中输入 app,然后进行数据修改调试实现响应式:(不过可能还是有一些bug,不过还是可以的!)
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<div id="app">
{{name}}
<p>{{ age > 20 ? age : obj.age }}p>
<p>{{ obj.name }}p>
<input v-model="obj.value">
{{obj.value}}
div>
<script>
function isObject(obj) {
return typeof obj === 'object' ? true : false;
}
// vue双向绑定实现:observer(观察者)、watcher(订阅对象)、notify(通知订阅对象的通报)、compiler(解析器)
// vue2使用Object.defineProperty(),缺点:不能直接监听数组,不能监听变化(属性的添加)
// vue3使用proxy -> 只能进行浅层的对象代理(递归循环代理)
/**
* 观察者
*
* */
class Obsever {
/*
* data: 被观察者
*/
constructor(data, vm, prop) {
if(isObject(data)) {
// 双向绑定
vm[prop] = this.defineReactive(data);
}
}
defineReactive(obj) {
if(!isObject(obj)) return;
Object.keys(obj).forEach(key => { // 深层绑定
new Obsever(obj[key], obj, key);
})
const dep = new Dep();
const handler = {
get(target, key, receiver) {
if(Dep.target) dep.addSub(Dep.target);
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver){
if(value === target[key]) return; // 值不变直接返回
Reflect.set(target, key, value, receiver)
// console.log(dep)
// new Obsever(value) // 替换引用类型的地址需要重新绑定响应式
dep.notify(); // 改变值通知所有观察者
return true;
}
}
return new Proxy(obj, handler)
}
}
/*
* 发布订阅模式
*/
class Dep {
static target = null; // 这里会存放当前的Watcher实例,并添加入Dep通知函数
constructor() {
this.subs = []; // 任务队列
}
addSub(sub) {
return this.subs.push(sub);
}
notify() { // 通知所有观察者
this.subs.forEach((sub) => { // 通知变化,此处会循环所有的依赖(Watcher实例),然后调用实例的update方法。
sub.update(); // 执行更新函数 (watcher 通知视图的变化)
// console.log(sub.update)
})
}
}
class Watcher {
// 每一个Watcher都绑定一个更新函数,watcher可以收到属性的变化通知并执行相应的函数,从而更新视图。
constructor(vm, node, prop) {
Dep.target = this;
this.vm = vm; // 实例
this.node = node;
this.prop = prop; // 要监听的属性
this.update();
Dep.target = null;
}
update() {
this.get(); // 触发相应get
// console.log(this.node, this)
this.node.nodeValue = this.value //更改节点内容的关键
}
get() {
this.value = eval(`this.vm._data.${this.prop}`);
}
}
class Compile {
constructor(el, vm) {
this.$vm = vm; //vm为当前实例
this.$el = document.querySelector(el);//获得要解析的根元素
if(this.$el) {
this.$fragment = this.nodeToFragment(this.$el);
this.init(this.$fragment);
this.$el.appendChild(this.$fragment); // 将类dom添加进真实dom内
}
}
nodeToFragment(el){ // 这里相当于生成虚拟dom(不是虚拟dom)
let fragment = document.createDocumentFragment();
// fragment 是一个指向空DocumentFragment对象的引用。是 DOM 节点。它们不是主 DOM 树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到 DOM 树。在 DOM 树中,文档片段被其所有的子元素所代替。
// 因为文档片段存在于内存中,并不在 DOM 树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。
let child;
while (child = el.firstChild){
// fragment.appendChild()具有移动性, 相当于把el中节点移动过去.nextElementSibling
fragment.appendChild(child);//append相当于剪切的功能
}
return fragment;
}
init($fragment) {
const childNodes = $fragment.childNodes;
Array.from(childNodes).forEach(node => {
if(node.nodeType === 1) { // 元素节点
Array.from(node.attributes).forEach(attribute => {
if(attribute.nodeName === 'v-model') {
node.addEventListener('input', (e) => {
const value = e.target.value;
eval(`this.$vm._data.${attribute.nodeValue} = value`);
})
}
})
this.init(node);
}
if(node.nodeType === 3) { // 文本节点
this.compile_text(node);
}
})
}
compile_text(node) {
function isVariable(variable) { // 判断是不是变量
return !(variable === '' || parseInt(variable).toString() !== 'NaN' || (/[`'"]/).test(variable));
}
node.nodeValue = node.nodeValue.replace(/{{([^}]*)}}/g, (macth, context) => {
context = context.trim();
const exp = context.split(/[?:]/); // 三目运算符
const value = exp[0].trim();
// 变量,解决对于 data || age 这一类的,给data和age分别设置watcher
let variables = value.split(/[^\w.'"`]/);
variables.forEach(variable => {
variable = variable.trim();
if(isVariable(variable)) new Watcher(this.$vm, node, variable);
})
// 如果是三目运算符
if(exp.length === 3) {
// 运算表达式
if(eval(`this.$vm._data.${exp[0]}`)) {
if(isVariable(exp[1])) {
return eval(`this.$vm._data.${exp[1]}`);
}
return exp[1].trim().replace(/['"`]/g, '');
} else {
if(isVariable(exp[2])) {
return eval(`this.$vm._data.${exp[2]}`);
}
return exp[2].trim().replace(/['"`]/g, '');
}
}
// 变量
return eval(`this.$vm._data.${value}`)
})
}
}
class myVue {
/*
* options: 配置选项
*/
constructor(options) {
this.$options = options || {};
const data = this._data = options.data;
new Obsever(data, this, '_data'); // 被观察者不能是Vnode或者基本数据类型
this.$compile = new Compile(options.el || document.body, this);
}
}
script>
<script>
const app = new myVue({
el: '#app',
data: {
name: 'y',
age: 19,
obj: {
name: 'hy',
value: '',
age: 15
}
}
});
script>
body>
html>
最后!祝大家早日拿到满意的offer!!冲冲冲!!!
“大多数优秀的程序员从事编程工作,不是因为期望获得报酬或得到公众的称赞,而是因为编程是件有趣的事儿。”——林纳斯·托瓦兹(Linus Torvalds)