前言
当被问到Vue是如何实现数据的双向绑定,大部分人的回答是:其内部是通过Object.definedProperty()的get和set方法实现的。其核心原理是通过这个API实现,但是还是有必要理解整个过程的实现和其运行原理。
什么是MVVM模式
MVVM模式是Model-View-ViewModel的简写,即模型-视图-视图模型。【模型】指的是数据层。【视图】指的是视图层所看到的页面。【视图模型】MVVM模式的核心,它是连接view和model的桥梁。
数据层和视图层直接是不能直接通信的,那么模型(数据层)如何转换成视图:通过绑定数据。视图如何转换成数据:通过DOM事件监听。当这个两个都实现了,就完成了数据的双向绑定。MVVM模式Model和View是通过ViewModel作为桥梁进行通信的。
Vue是如何利用MVVM原理实现数据的双向绑定
- 什么是数据的双向绑定
- 实现数据的双向绑定
- 实现Complier类和Observer类。
- 实现观察者Watcher类
- 实现被观察者Dep类
-
什么是数据的双向绑定
如下代码:当数据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做了什么事?
-
Vue类首先通过Object.defineProperty进行了data选项的代理,当访问this.a实际上是访问this.$data.a
-
将data传入Observe类将数据通过Object.defineProperty方法的get,set重新定义,实现数据劫持
-
调用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.computedkey; }, 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; } }) } }
}
3.如何实现Complier类和Observer类
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,回调函数。最后通过事件监听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方法实现,利用Dep类进行watcher实例的添加和发布。
4.观察者Watcher
Watcher类定义一个get方法用来获取值并将实例绑定到Dep的target属性上。在这个过程也触发了Object.defineProperty的get方法。那么,Object.defineProperty通过new 一个dep实例,在获取值的时候,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);
}
}
}
5.被观察者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类的实现,也帮助大家了解数据双向绑定的基本原理。
本人是近日才用掘金,博客的地址是:
https://blog.csdn.net/Miss_hhl/article/details/107333270
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: lin1997 原文链接:https://juejin.im/post/6850418114808119303