vue的MVVM
Vue的相关知识有
- 字符串模板
- MVVM
- 虚拟dom和domdiff,查看下一篇笔记
字符串模板
function render(template, data) {
const reg = /\{\{(\w+)\}\}/; // 模板字符串正则
if (reg.test(template)) { // 判断模板里是否有模板字符串
const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
return render(template, data); // 递归的渲染并返回渲染后的结构
}
return template; // 如果模板没有模板字符串直接返回
}
// 使用
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
name: '姓名',
age: 18
}
render(template, data); // 我是姓名,年龄18,性别undefined
MVVM
MVVM 设计模式,是由 MVC(最早来源于后端)、MVP 等设计模式进化而来
- M - 数据模型(Model)
- VM - 视图模型(ViewModel)
- V - 视图层(View)
在 Vue 的 MVVM 设计中,我们主要针对Compile
(模板编译),Observer
(数据劫持),Watcher
(数据监听),Dep
(发布订阅)几个部分来实现,核心逻辑流程可参照下图:
数据监听API
- vue2.0和vue2.x是用
defineProperty
- vue3.0即将使用
proxy
为什么要改用proxy,因为defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。 为了解决这个问题,defineProperty需要判断如果是数组,需要重写他的原型方法,而proxy就不需要
为什么还不上线,因为proxy的兼容性太差
defineProperty监听
// 监听普通属性
function isKey(obj,key){
return Object.defineProperty(obj,key,{
get: function() {
console.log('get :', key);
return eval(key) || "";
},
set: function(newValue) {
console.log('set :', newValue);
key = newValue;
}
})
}
// 监听数组属性
function toNewArray(data,key){
// 实例具名回调函数
window.eval("var callback = function "+key+" (args,k){console.log('数组'+k+'发生变化...');}")
return new NewArray(data[key],callback) // 注入回调函数
}
class NewArray extends Array{
constructor(arr,callback){
if(arguments.length === 1){
return super()
} // 产生中间数组会再进入构造方法
// let args = arr // 原数组
arr.length === 1 ? super(arr[0].toString()) : super(...arr)
this.callback = callback // 注入回调具名函数
}
push(...args){
super.push(...args)
this.callback(this, this.callback.name) // 切面调用具名回调函数
}
pop(){
super.pop()
this.callback(this, this.callback.name)
}
splice(...args){
super.splice(...args)
this.callback(this, this.callback.name)
}
}
var data = {
arr:[1,2,3,4],
name:"pdt"
}
function init(data){
Object.keys(data).forEach(key => {
let value = data[key]
// 如果是obj就递归
if(value是对象){
init(value)
}else if(Array.isArray(value)){
// 如果value是数组
data[key] = toNewArray(data,key)
}else{
// 如果是普通的值
isKey(data,key)
}
})
}
init(data)
proxy监听
var data = {
arr:[1,2,3,4],
name:"pdt"
}
function init(data){
Object.keys(data).forEach(key => {
let value = data[key]
if(value 是对象){
data[key] = init(value)
}
})
data = newData(data)
}
init(data)
function newData(data){
return new Proxy(data, {
get: function(target, key, receiver) {
console.log(target, key, receiver)
return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver) {
console.log(target, key, value, receiver);
return Reflect.set(target, key, value, receiver);
}
})
}
defineProperty实现一个Vue
<script>
class MVVM {
constructor (options) {
// 一上来 先把可用的东西挂载在实例上
this.$el = options.el;
this.$data = options.data;
// 如果有要编译的模板就开始编译
if(this.$el) {
// 数据劫持,就是把对象的所有属性添加 set 和 get 方法
new Observer(this.$data);
// 用数据和元素进行编译
new Compile(this.$el, this);
}
}
}
class Observer {
constructor (data) {
this.observe(data);
}
observe (data) {
// 验证 data
if(!data || typeof data !== 'object') {
return;
}
// 要对这个 data 数据将原有的属性改成 set 和 get 的形式
// 要将数据一一劫持,先获取到 data 的 key 和 value
Object.keys(data).forEach(key => {
// 劫持(实现数据响应式)
this.defineReactive(data, key, data[key]);
this.observe(data[key]); // 深度劫持
});
}
defineReactive (object, key, value) { // 响应式
let _this = this;
// 每个变化的数据都会对应一个数组,这个数组是存放所有更新的操作
let dep = new Dep();
// 获取某个值被监听到
Object.defineProperty(object, key, {
enumerable: true,
configurable: true,
get () { // 当取值时调用的方法
Dep.target && dep.addSub(Dep.target);
console.log(dep.subs);
return value;
},
set (newValue) { // 当给 data 属性中设置的值适合,更改获取的属性的值
if(newValue !== value) {
_this.observe(newValue); // 重新赋值如果是对象进行深度劫持
value = newValue;
dep.notify(); // 通知所有人数据更新了
}
}
});
}
}
class Dep {
constructor () {
// 订阅的数组
this.subs = [];
}
addSub (watcher) { // 添加订阅
this.subs.push(watcher);
}
notify () { // 通知
this.subs.forEach(watcher => watcher.update());
}
}
// 观察者的目的就是给需要变化的那个元素增加一个观察者,当数据变化后执行对应的方法
class Watcher {
constructor (vm, exp, callback) {
this.vm = vm;
this.exp = exp;
this.callback = callback;
// 先获取一下老的值
this.value = this.get();
}
get () { // 获取实例上老值得方法
Dep.target = this;
let value = CompileUtil.getVal(this.vm, this.exp);
Dep.target = null;
return value;
}
update () {
let newValue = CompileUtil.getVal(this.vm, this.exp);
let oldValue = this.value;
// 用新值和老值进行对比,如果变化,就调用更新方法
if(newValue !== oldValue) {
this.callback(newValue); // 如果修改后得新旧值不等就执行回调
}
}
}
class Compile {
constructor (el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if(this.el) {
// 如果这个元素能获取到,我们才开始编译
// 1.先把这些真实的 DOM 移动到内存种 fragment
let fragment = this.nodeToFragment(this.el);
// 2.编译 => 提取想要的元素节点 v-model 和文本节点 {{message}}
this.compile(fragment);
// 把编译好的 fragment再塞回到页面中去
this.el.appendChild(fragment);
}
}
/* 专门写一些辅助方法 */
isElementNode (node) { // 是不是 dom 节点
return node.nodeType === 1;
}
isDirective (name) { // 是不是指令
return name.includes('v-');
}
/* 核心方法 */
nodeToFragment (el) { // 需要将 el 中的内容全部放到内存中
// 文档碎片 内存中的 dom 节点
let fragment = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild) {
fragment.appendChild(firstChild);
}
return fragment; // 内存中的节点
}
compile (fragment) { // 编译文档碎片方法
// 需要递归
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach(node => {
if(this.isElementNode(node)) {
// 是元素节点,还需要继续深入的检查
// console.log('element', node);
this.compile(node);
// 这里需要编译元素
this.compileElement(node);
} else {
// 是文本节点
// console.log('text', node);
// 这里需要编译文本
this.compileText(node);
}
});
}
compileElement (node) { // 编译元素节点
// 带 v-model 的
let attrs = node.attributes; // 取出当前节点的属性
Array.from(attrs).forEach(attr => {
// 判断属性名字是不是包含 v-
let attrName = attr.name;
if(this.isDirective(attrName)) {
// 取到对应的值,放在节点中
let exp = attr.value;
let [, type] = attrName.split('-');
// node this.vm.$date exp
CompileUtil[type](node, this.vm, exp);
}
});
}
compileText (node) { // 编译文本节点
// 带 {{}} 的
let exp = node.textContent; // 获取文本中的内容
let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}} {{c}}
if(reg.test(exp)) {
// node this.vm.$date exp
CompileUtil['text'](node, this.vm, exp);
}
}
}
CompileUtil = {
getVal (vm, exp) { // 获取实例上对应的数据
exp = exp.split('.');
return exp.reduce((prev, next) => {
return prev[next];
}, vm.$data);
},
setVal (vm, exp, newVal) { // 设置实例上对应的数据
exp = exp.split('.');
return exp.reduce((prev, next, currentIndex) => {
if(currentIndex === exp.length - 1) {
return prev[next] = newVal;
}
return prev[next];
}, vm.$data);
},
getTextVal (vm, exp) { // 获取编译文本后的结果
return exp.replace(/\{\{([^}]+)\}\}/g, (...arg) => {
return this.getVal(vm, arg[1]);
});
},
text (node, vm, exp) { //文本处理
let updateFn = this.updater['textUpdater'];
let value = this.getTextVal(vm, exp);
exp.replace(/\{\{([^}]+)\}\}/g, (...arg) => {
new Watcher(vm, arg[1], newValue => {
// 如果数据变化了,文本节点应该重新获取依赖的数据更新文本中的内容
updateFn && updateFn(node, newValue);
});
});
updateFn && updateFn(node, value);
},
model (node, vm, exp) { // 输入框处理
let updateFn = this.updater['modelUpdater'];
let value = this.getVal(vm, exp);
// 这里应该加一个监控,数据变化了,应该调用 watch 的回调
new Watcher(vm, exp, newValue => {
updateFn && updateFn(node, newValue);
});
// 添加输入框事件实现双向绑定
node.addEventListener('input', e => {
let newValue = e.target.value;
this.setVal(vm, exp, newValue);
});
// 防止没有的指令解析时报错
updateFn && updateFn(node, value);
},
updater: {
// 文本更新
textUpdater (node, value) {
node.textContent = value;
},
// 输入框更新
modelUpdater (node, value) {
node.value = value;
}
}
};
</script>
// 使用
<div id="app">
<!-- 双向数据绑定 靠的是表单 -->
<input type="text" v-model="message">
<div>{{message}}</div>
</div>
<script>
let vm = new MVVM({
el: '#app',
data: {
message: 'hello world!'
}
});
</script>
使用proxy写一个简易版的vue
<script type="text/javascript">
class Vue {
constructor(options) {
this.$el = document.querySelector(options.el);
this.$methods = options.methods;
this._binding = {};
this._observer(options.data);
this._compile(this.$el);
}
_pushWatcher(watcher) {
if (!this._binding[watcher.key]) {
this._binding[watcher.key] = [];
}
this._binding[watcher.key].push(watcher);
}
/*
observer的作用是能够对所有的数据进行监听操作,通过使用Proxy对象
中的set方法来监听,如有发生变动就会拿到最新值通知订阅者。
*/
_observer(datas) {
const me = this;
const handler = {
set(target, key, value) {
const rets = Reflect.set(target, key, value);
me._binding[key].map(item => {
item.update();
});
return rets;
}
};
this.$data = new Proxy(datas, handler);
}
/*
指令解析器,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相对应的更新函数
*/
_compile(root) {
const nodes = Array.prototype.slice.call(root.children);
const data = this.$data;
nodes.map(node => {
if (node.children && node.children.length) {
this._compile(node.children);
}
const $input = node.tagName.toLocaleUpperCase() === "INPUT";
const $textarea = node.tagName.toLocaleUpperCase() === "TEXTAREA";
const $vmodel = node.hasAttribute('v-model');
// 如果是input框 或 textarea 的话,并且带有 v-model 属性的
if (($vmodel && $input) || ($vmodel && $textarea)) {
const key = node.getAttribute('v-model');
this._pushWatcher(new Watcher(node, 'value', data, key));
node.addEventListener('input', () => {
data[key] = node.value;
});
}
if (node.hasAttribute('v-html')) {
const key = node.getAttribute('v-html');
this._pushWatcher(new Watcher(node, 'innerHTML', data, key));
}
if (node.hasAttribute('@click')) {
const methodName = node.getAttribute('@click');
const method = this.$methods[methodName].bind(data);
node.addEventListener('click', method);
}
});
}
}
/*
watcher的作用是 链接Observer 和 Compile的桥梁,能够订阅并收到每个属性变动的通知,
执行指令绑定的响应的回调函数,从而更新视图。
*/
class Watcher {
constructor(node, attr, data, key) {
this.node = node;
this.attr = attr;
this.data = data;
this.key = key;
}
update() {
this.node[this.attr] = this.data[this.key];
}
}
</script>
// 使用
<div id="app">
<input type="text" v-model='count' />
<input type="button" value="增加" @click="add" />
<input type="button" value="减少" @click="reduce" />
<div v-html="count"></div>
</div>
<script type="text/javascript">
new Vue({
el: '#app',
data: {
count: 0
},
methods: {
add() {
this.count++;
},
reduce() {
this.count--;
}
}
});
</script>
总结上面的代码
可以看到上面的两个写法都需要这么几个构造函数
- MVVM.js 把参数传给Observer和Compile
- Observer.js 劫持,就是监听,每个key都需要搭配一个Dep,数据更新就执行Dep.updata
- Dep.js 就是一个订阅发布的数组而已
- Compile.js,解析dom元素,通过属性【v-xx】创建更新规则,每个规则创建一个watch,watch装在对应位置的Dep数组里
- Watch.js,闭包存着所有的dom和对应的无数个更新规则,由Dep调用它的更新
真正的Vue
- 不能保留原来的dom,遍历过程成vDom【生命周期是created】
- 遍历虚拟dom数据,生成新的真实的dom,再结合data数据,methods,计算属性,watch,数据绑定到新的dom上
- 当数组发生改变,生成新的vDom,对比新旧的vDom,更新到真实dom上【如果使用proxy,就不需要domdiff了】,就是一个真正的vue了
vue的MVVM的更多相关文章
- 实现一个类 Vue 的 MVVM 框架
Vue 一个 MVVM 框架.一个响应式的组件系统,通过把页面抽象成一个个组件来增加复用性.降低复杂性 主要特色就是数据操纵视图变化,一旦数据变化自动更新所有关联组件~ 所以它的一大特性就是一个数据响 ...
- Vue和MVVM对应关系
Vue和MVVM的对应关系 Vue是受MVVM启发的,那么有哪些相同之处呢?以及对应关系? MVVM(Model-view-viewmodel) MVVM还有一种模式model-view-binder ...
- Vue的mvvm模式
传统的MVC模式: Model:模型-->负责数据存储 View:视图-->负责页面展示 Control:控制器-->事件交互(根据视图与用户交互后改变数据) Vue的MVVM模式: ...
- vue和mvvm的一些小区别
Vue.js 和 MVVM 小细节 MVVM 是Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对View 和 ViewModel 的双向数据绑定,这 ...
- Vue.js MVVM及数据绑定原理
什么是数据驱动 数据驱动是vuejs最大的特点.在vuejs中,所谓的数据驱动就是当数据发生变化的时候,用户界面发生相应的变化,开发者不需要手动的去修改dom. 比如说我们点击一个button,需要元 ...
- 实现一个简易的vue的mvvm(defineProperty)
这是一个最近一年很火的面试题,很多人看到这个题目从下手,其实查阅一些资料后,简单的模拟还是不太难的: vue不兼容IE8以下是因为他的实现原理使用了 Object.defineProperty 的ge ...
- 对类Vue的MVVM前端库的实现
关于实现MVVM,网上实在是太多了,本文为个人总结,结合源码以及一些别人的实现 关于双向绑定 vue 数据劫持 + 订阅 - 发布 ng 脏值检查 backbone.js 订阅-发布(这个没有使用过, ...
- vue的MVVM模式和生命周期总结(一)
一.MVVM模式 MVVM 是Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对View 和 ViewModel 的双向数据绑定,这使得ViewModel ...
- Vue中MVVM模式的双向绑定原理 和 代码的实现
今天带大家简单的实现MVVM模式,Object.defineProperty代理(proxy)数据 MVVM的实现方式: 模板编译(Compile) 数据劫持(Observer) Object ...
随机推荐
- 面试官:说说Spring中的事务传播行为
前言 在开发中,相信大家都使用过Spring的事务管理功能.那么,你是否有了解过,Spring的事务传播行为呢? Spring中,有7种类型的事务传播行为.事务传播行为是Spring框架提供的一种事务 ...
- nginx的addition模块在响应的前后报文添加内容与变量的运行原理
nginx默认未编译此模块:让nginx编译启用此模块 ./configure --prefix=/data/web --sbin-path=/usr/bin --user=nginx --group ...
- linux磁盘管理1-分区格式化挂载,swap,df,du,dd
一些基础 硬盘接口类型 ide 早期家庭电脑 scsi 早期服务器 sata 目前家庭电脑 sas 目前服务器 raid卡--阵列卡 网卡绑定 ABI 应用程序与OS之间的底层接口 API 应用程序调 ...
- tomcat中servlet冲突问题
在启动tomcat以后,控制台发现“Offending class: javax/servlet/Servlet.class”信息: 信息: validateJarFile(E:\code\MyApp ...
- 【项目】小试牛刀-polo360静态网页项目(附psd文件资源)
笔者尝试下开发简单的静态网页,下面分享过程及源码.这是polo360的下载链接:https://pan.baidu.com/s/1WqGxKMYY_DHfrSJ9lLL-WA 提取码:v2qi (一 ...
- Spring Boot+Jpa(MYSQL)做一个登陆注册系统(前后端数据库一站式编程)
Spring Boot最好的学习方法就是实战训练,今天我们用很短的时间启动我们第一个Spring Boot应用,并且连接我们的MySQL数据库. 我将假设读者为几乎零基础,在实战讲解中会渗透Sprin ...
- Python的类(class)和实例(Instance)如何操作使用
面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模板,比如Student类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可 ...
- HTML元素和测试用例的简要介绍
HTML和CSS的基本语法就不出教程,线下自己看电子书即可 HTML元素 标签 内容 属性 标签+内容+属性 <html> <head> <title>我的主页&l ...
- python词云图与中文分词
2019-12-12中文文本分词和词云图具体功能介绍与学习代码: import jiebaa="由于中文文本的单词不是通过空格或者标点符号来进行分割"#jieba.lcut()s是 ...
- 吴裕雄--天生自然HADOOP操作实验学习笔记:pvuv统计案例理论
实验目的 复习pv.uv的概念和原理 了解pv.uv的实际意义和获取方法 实验原理 前面我们已经基于mapreduce进行数据的etl处理,实验有很多不足之处,这次实验我们是基于url数据进行pv和u ...