深入剖析:Vue核心之MVVM原理其手动实现数据双向绑定
前言
当被问到Vue是如何实现数据的双向绑定,大部分人的回答是:其内部是通过Object.definedProperty()的get和set方法实现的。其核心原理是通过这个API实现,但是还是有必要理解整个过程的实现和其运行原理。
什么是MVVM模式
MVVM模式是Model-View-ViewModel的简写,即模型-视图-视图模型。【模型】指的是数据层。【视图】指的是视图层所看到的页面。【视图模型】MVVM模式的核心,它是连接view和model的桥梁。
数据层和视图层直接是不能直接通信的,那么模型(数据层)如何转换成视图:通过绑定数据。视图如何转换成数据:通过DOM事件监听。当这个两个都实现了,就完成了数据的双向绑定。MVVM模式Model和View是通过ViewModel作为桥梁进行通信的。
Vue是如何利用MVVM原理实现数据的双向绑定
在大致了解了MVVM的原理之后,接下来就一起来探讨vue是如何实现数据的双向绑定。
- 问题一:什么是数据的双向绑定
如下代码:当数据a发生变化的时候,视图会发生变化;当用户输入视图发生变化时,数据也会发生变化,即这个过程就是数据的双向绑定。
<template>
<div id="app">
<input @input="handler" value="a"/>
</div>
</template>
<script>
export default {
name: 'app',
data(){
return {
a:1
}
},
methods:{
handler(){
}
},
}
</script>
- 问题二:如何实现数据的双向绑定
当我们修改a的值比如this.a=10,为什么视图发生了变化?并且这中间Vue做了什么事?
1、Vue类首先通过Object.defineProperty进行了data选项的代理,当访问this.a实际上是访问this.$data.a
2、将data传入Observe类将数据通过Object.defineProperty方法的get,set重新定义,实现数据劫持
3、调用Complier类开启模板初始化编译
其代码如下:
class Vue {
constructor(option) {
//this.$el $data $options
this.$el = option.el;
this.$data = option.data;
this.computed = option.computed;
this.methods = option.methods;
//如果存在根元素 就编译模板
if (this.$el) {
//把数据 全部转化成用 Object.defineProperty来定义
new Observe(this.$data);
for (let key in this.computed) { //根据依赖的数据添加watcher
Object.defineProperty(this.$data, key, {
get: () => {
return this.computed[key]();
},
set: (newVal) => {
}
})
}
//将methods对象代理到vm头上
for (let key in this.methods) {
Object.defineProperty(this, key, {
get: () => {
return this.methods[key];
}
})
}
//数据获取操作 vm上的取值操作
//都代理到 vm.$data
this.getVmProxy()
new Compiler(this.$el, this);
}
}
getVmProxy() {
for (let key in this.$data) {
Object.defineProperty(this, key, { //实现可以通过vm取到对应的内容
get: () => {
return this.$data[key];
},
set: (value) => {
this.$data[key] = value;
}
})
}
}
}
- 问题三:如何实现Complier类和Observer类。
Complier类代码如下
class Compiler {
constructor(el, vm) {
this.vm = vm;
this.el = this.isElementNode(el) ? el : document.querySelector(el);
//把当前节点中的元素获取到放到内存中
let fragment = this.node2fragment(this.el)
//把节点中的内容进行替换
//编译模板 用数据编译
this.compile(fragment)
//把这个内容在塞到页面中
this.$el.appendChild(fragment);
}
isDirective(attrName) {
return attrName.startWith('v-');
}
compileElement(node) {
let attr = node.attributes; //类数组
[...attr].forEach(atr => {
let {
name,
value: expr
} = atr;
if (this.isDirective(name)) {
console.log(node) //指令元素
let [, directive] = name.split('-');
let [directiveName, eventName] = directive.split('.'); //处理v-on:click指令
CompilerUtils[directiveName](node, expr, )
CompilerUtils[directive](node, expr, this.vm, eventName)
}
})
}
compileText(node) {
let content = node.textcontent; //节点的文本
if (/\{\{(.+?)\}\}/.test(content)) {
console.log(content) //找到所有文本
CompilerUtils['text'](node, content, this.vm)
}
}
//用来编译内存中的dom节点
compile(node) {
let childNodes = node.childNodes; //dom的第一层
[...childNodes].forEach(child => {
if (this.isElementNode(child)) {
console.log('element');
this.compileElement(child);
//如果是元素的话,需要遍历子节点
this.compile(child);
} else {
console.log('text');
this.compileText(child)
}
})
}
node2fragment(node) {
//把所有的节点都拿到,创建一个文档碎片。
let fragment = document.createDocumentFragment();
let firstChild;
while (firstChild = node.firstChild) {
fragment.appendChild(firstChild);
}
return fragment;
}
isElementNode(node) {
return node.nodeType === 1
}
}
//工具类
//处理不同指令不同的功能调用不同的处理方式
CompilerUtils = {
//获取值
getValue(vm, expr) {
let value = expr.split('.').reduce((data, current) => {
return data[current];
}, vm.$data)
return value;
},
//设置值
setValue(vm, expr, value) {
return expr.split('.').reduce((data, current, index, arr) => {
if (index === arr.length - 1) {
return data[current] = value;
}
return data[current];
}, vm.$data)
},
model(node, expr, vm) {
let fn = this.updater['modelUpdater']
let value = this.getValue(vm, expr);
new Watcher(vm, expr, (newValue) => {
fn(node, newValue);
})
node.addEventListener('input', (e) => {
let value = e.target.value;
this.setValue(vm, expr, value);
})
fn(node, value);
},
on(node, expr, vm, eventName) {
node.addEventListener(eventName, (e) => {
return vm[expr].call(vm, e);
})
},
html() {
},
getContentValue(vm, expr) {
return expr.replace(/\{\{(.+?)\}\}/g, (...agrs) => {
return this.getValue(vm, args[1]);
})
},
text(node, expr, vm) { //expr {{a}} {{b}} {{c}}
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], () => {
let value = this.getContentValue(vm, expr); //返回了一个全的字符串
fn(node, value)
})
return this.getValue(vm, args[1]);
})
let fn = this.updater['textUpdater'];
fn(node, content);
},
updater: {
modelUpdater(node, value) {
node.value = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
textUpdater(node, value) {
node.textContent = value; //将值放在节点的文本内容上
}
}
}
在初始化编译的过程中就是在收集每一个watcher。比如v-model指令:先通过vm获取值挂载到到视图模板,实现视图层数据的展示。然后订阅数据则是通过new Watcher实例传入keyname,回调函数。(Watcher的实现稍许片刻)最后通过事件监听input事件,当数据发生改变时,重新设置数据。在重新设置数据的过程其就是调用Object.defineProperty的set方法,那么在set方法里执行发布每一个收集的依赖,并重新设置值。
Observer类代码如下
class Observe {
constructor(obj) {
console.log(obj);
this.observe(obj);
//对数组的原生方法进行重写
let arrayProto = Array.prototype; //先存一份原生的原型
let proto = Object.create(Array.prototype); //复制一份一模一样原生的原型。
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(item => {
proto[item] = function(...ary) {
let insert; //数组新增的元素也要进行观察
switch (item) {
case 'push':
insert = ary;
case 'unshift':
insert = ary;
case 'splice':
insert = ary.slice(2);
default:
break;
}
this.observer(insert);
console.log('视图更新')
return arrayProto[item].call(arrayProto, ...ary);
}
})
}
observerArray(array) {
for (let i = 0, len = array.length; i < len; i++) {
let item = array[i];
this.observer(item);
}
}
observer(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
Object.setPrototypeOf(obj, proto) //设置数组的原型,进行数组方法重写
this.observerArray(obj) //对数组已有的元素进行检测
} else {
for (let key in obj) {
this.defineReactive(key, obj[key], obj)
}
}
}
defineReactive(obj, key, value) {
Observe(value);
let dep = new Dep()
Object.defineProperty(obj, key, {
get: () => {
Dep.target && dep.addSubs(Dep.target);
return value;
},
set: (newValue) => {
this.observe(newValue);
if (value != newValue) value = newValue;
dep.notify();
}
})
}
}
Observer类就是对每个在data选项上定义的数据进行劫持检测。通过Object.defineProperty的get和set方法实现watcher实例的添加和发布。
- 问题四:观察者Watcher
Watcher类通过在构造器上获取keyName所对应的具体值,在这个过程触发了Object.defineProperty()的get方法,并且将自己(watcher实例)赋值给Dep类的target静态属性。那么,Object.defineProperty()通过new 一个dep实例并通过get方法将dep实例身上的target属性进行watcher实例的收集。
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
this.oldval = this.get()
}
get() {
Dep.target = this;
let value = CompilerUtils.getValue(this.vm, this.expr);
Dep.target = null;
return value;
}
updater() { //更新操作 数据变化后 会调用观察者的update方法
let newValue = CompilerUtils.getValue(this.vm, this.expr);
if (newValue !== this.oldval) {
this.cb(newValue);
}
}
}
- 问题五:被观察者Dep类的实现
收集完成每一个watcher实例之后,当数据发生改变时又会触发Object.defineProperty()的set方法,那么通过dep实例的发布函数,触发watcher的updater函数,并将新的数据传入回调函数,回调函数触发视图层更新。
class Dep {
constructor() {
this.subs = []; //存放watcher
}
//订阅
addSubs(watcher) {
this.subs.push(watcher)
}
//发布
notify() {
this.subs.forEach(watcher => {
watcher.updater();
})
}
}
上面的每个类可通过如下图的关系进行进一步理解:
总结
以上是我对MVVM原理在数据双向绑定运用的理解,通过监听器 Observer 、被观察者 Dep 、观察者 Watcher 和解析器 Complier类的实现,帮助大家了解数据双向绑定的基本原理。
上一篇: webAPI案例之三分钟玩转动画封装