【JS】vue中MVVM原理及其实现

vue中MVVM原理及其实现

一、理解 MVVM

  • MVVM - Model View ViewModel:数据,视图,视图模型。

  • 三者与 Vue 的对应:view 对应 template,vm 对应 new Vue({…}),model 对应 data。

  • 三者的关系:view 可以通过事件绑定的方式影响 model,model 可以通过数据绑定的形式影响到view,viewModel是把 model 和 view 连起来的连接器。

MVVM 框架的三大要素:

  • 响应式:Vue 如何监听到 data 的每个属性变化。

    数据劫持: 使用 Object.defineProperty(obj, 'property',{})来定义属性,将对象属性值的设置和访问 (get,set) 都变成函数,分别在设置和获取该对象的该属性时调用执行。

  • 模板引擎:Vue 的模板如何被解析,指令如何处理
  • 渲染:Vue 的模板如何被渲染成 html,渲染过程是怎样的

二、实现方法

【JS】vue中MVVM原理及其实现

【JS】vue中MVVM原理及其实现

  1. 实现compile,进行模板的编译,包括编译元素(指令)、编译文本等,达到初始化视图的目的,并且还需要绑定好更新函数;
  2. 实现Observe,监听所有的数据,并对变化数据发布通知;
  3. 实现watcher,作为一个中枢,接收到observe发来的通知,并执行compile中相应的更新方法。
  4. 结合上述方法,向外暴露mvvm方法。

首先编辑一个html文件,如下:

<div id="app">

<input type="text" v-model="obj.name">

<div> {{obj.name}}</div>

{{message}}{{obj.name}}

</div>

<script></script>

<script>

let vm = new MVVM({

el: '#app',//或document.querySelector('#app')

data: {

message: 'hello',

obj: {

name: 'susu'

}

}

});

</script>

1.创建类MVVM

class MVVM {

constructor(options) {

this.$el = options.el;

this.$data = options.data;

if (this.$el) {

// 数据劫持 把对象所有的属性 增加get set 方法

new Observer(this.$data);

this.proxyData(this.$data);

// 用数据和元素进行编译

new Compile(this.$el, this);

}

}

// 把对象的属性全部绑定在实例上,this.xx

proxyData(data) {

// defineProperty解析 : https://www.jianshu.com/p/8fe1382ba135

Object.keys(data).forEach(key => {

Object.defineProperty(this, key, {

get() {

return data[key];

},

set(newValue) {

data[key] = newValue;

}

})

})

}

}

2.实现compile(编译模板)

1.把真实DOM移入到内存中 fragment,因为fragment在内存中,操作比较快

2.编译 : 提取想要的元素节点 v-model 和文本节点 {{}}

3.把编译好的fragment,添加到DOM中

class Compile {

constructor(el, vm) {

this.el = this.isElementNode(el) ? el : document.querySelector(el);

this.vm = vm;

if (this.el) {

// 1.把真实DOM移入到内存中 fragment,因为fragment在内存中,操作比较快

let fragment = this.node2fragment(this.el);

// 2.编译 => 提取想要的元素节点 v-model 和文本节点 {{}}

this.compile(fragment);

// 3.把编译好的fragment,添加到DOM中

this.el.appendChild(fragment);

}

}

// 是否是元素节点

isElementNode(el) {

//nodeType : 1 Element 代表元素节点

return el.nodeType == 1;

}

node2fragment(el) {

// 在内存中,创建一个新的文档片段,

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)) {

// 元素节点,编译元素

this.compileElement(node);

// 如果有子节点,再次执行

this.compile(node);

} else {

// 文本节点,编译文本

this.compileText(node);

}

})

}

// 编译文本 {{msg}}

compileText(node) {

let expr = node.textContent; //{{msg}}

let reg = /\{\{([^}]+)\}\}/g;

if (reg.test(expr)) {

CompileUtil.text(node, this.vm, expr);

}

}

// 编译元素 v-

compileElement(node) {

let attrs = node.attributes;

Array.from(attrs).forEach(attr => {

// 是否是指令

if (attr.name.includes('v-')) {

let expr = attr.value;// message

let type = attr.name.split('-')[1];//model

CompileUtil[type](node, this.vm, expr);

}

})

}

}

CompileUtil = {

// 文本处理

text(node, vm, expr) {

let updaterFn = this.updater.textUpdater;

let value = this.getTextVal(vm, expr);

// 监控数据变化 eg:因为{{message}} {{obj.name}} ,所以需要循环

expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {

new Watcher(vm,arguments[1],newValue => {

updaterFn && updaterFn(node, this.getTextVal(vm, expr));

})

})

updaterFn && updaterFn(node, value);

},

// 获取文本的key eg:message

getTextVal(vm, expr) {

return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {

return this.getValue(vm, arguments[1]);// message

})

},

// 获取data中对象的值

getValue(vm, expr) {

let arr = expr.split('.'); //处理对象obj.name =>[obj,name]

return arr.reduce((prev, next) => {

return prev[next]

}, vm.$data);

},

// 设置data中对象的值

setValue(vm, expr, value) {

let arr = expr.split('.'); //处理对象obj.name =>[obj,name]

arr.reduce((prev, next, curIndex) => {

if (curIndex == arr.length - 1) {

return prev[next] = value;

}

return prev[next];

}, vm.$data);

},

// 输入框处理

model(node, vm, expr) {

let updaterFn = this.updater.modalUpdater;

let value = this.getValue(vm, expr);

node.addEventListener('input', (event) => {

this.setValue(vm, expr, event.target.value);

})

// 监控数据变化

new Watcher(vm,expr,newValue => {

updaterFn && updaterFn(node, this.getValue(vm, expr));

})

updaterFn && updaterFn(node, value);

},

updater: {

textUpdater(node, value) {

node.textContent = value;

},

modalUpdater(node, value) {

node.value = value;

}

}

}

3.实现observe(数据监听/劫持)

vue采用的observe + sub/pub 实现数据的劫持,通过js原生的方法Object.defineProperty()来劫持各个属性的setter,getter,在属性对应数据改变时,发布消息给订阅者,然后触发相应的监听回调。

为何要监听 get,而不是直接监听 set ?

  • 因为 data 中有很多属性,有些被用到,有些可能不被用到
  • 只有被用到的才会走 get
  • 没有走到 get 中的属性,set 的时候我们也无需关心
  • 避免不必要的重新渲染

主要内容:observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter。

class Observer {

constructor(data) {

this.observe(data);

}

// 获取data的key value

observe(data) {

if (!data || typeof data != 'object') return;

Object.keys(data).forEach(key => {

this.defineReactive(data, key, data[key]);

this.observe(data[key]);

})

}

// 定义响应式

defineReactive(obj, key, value) {

let that = this;

// 每个变化的数据 都会对应一个数组,这个数组是存放所有更新的操作

let dep = new Dep();

Object.defineProperty(obj, key, {

enumerable: true,

configurable: true,

get() {

console.log('observe',key,Dep.target);

Dep.target && dep.addSubs(Dep.target);

return value;

},

set(newValue) {

if (value != newValue) {

// 如果是对象继续劫持

that.observe(newValue);

value = newValue;

// 通知更新

dep.notify();

}

}

})

}

}

实现数据劫持后,接下来的任务怎么通知订阅者了,我们需要在监听数据时实现一个消息订阅器,具体的方法是:定义一个数组,用来存放订阅者,数据变动通知(notify)订阅者,再调用订阅者的update方法。

添加Dep类:

class Dep {

constructor() {

this.subs = [];

}

// 添加订阅

addSubs(watcher) {

this.subs.push(watcher);

}

// 通知更新

notify() {

this.subs.forEach(watcher => {

watcher.update();

})

}

}

4.实现watcher(订阅中心)

Observer和Compile之间通信的桥梁是Watcher订阅中心,其主要职责是:

1、在自身实例化时往属性订阅器(Dep)里面添加自己,与Observer建立连接;

2、自身必须有一个update()方法,与Compile建立连接;

3、当属性变化时,Observer中dep.notify()通知,然后能调用自身(Watcher)的update()方法,并触发Compile中绑定的回调,实现更新。

// 观察者的目的就是给需要变化的那个元素增加一个观察者,当数据变化后执行对应的方法

class Watcher {

constructor(vm, expr, cb) {

this.vm = vm;

this.expr = expr;

this.cb = cb;

// 获取老的值

this.value = this.get();

}

// 获取data中对象的值

getValue(vm, expr) {

let arr = expr.split('.');

return arr.reduce((prev, next) => {

return prev[next]

}, vm.$data);

}

get() {

Dep.target = this;

let value = this.getValue(this.vm, this.expr);

Dep.target = null;

return value;

}

// 更新,外部调用的方法

update() {

let newValue = this.getValue(this.vm, this.expr);

if (newValue != this.value) {

this.cb(newValue);

}

}

}

以上是 【JS】vue中MVVM原理及其实现 的全部内容, 来源链接: utcz.com/a/97172.html

回到顶部