MVVM
MVVM是指Model-View-ViewModel
- Model:模型
- View: 视图,可组件化
- ViewModel: 抽象视图,集成了数据绑定引擎,实现View和Model的双向绑定
Vue的运行机制
初始化流程
- 创建vue实例对象
init
过程中初始化生命周期,初始化事件,初始化渲染,执行beforeCreate
周期函数,初始化data
,props
,computed
,watcher
,执行create
周期函数- 初始化后,调用
$mount
方法对vue实例进行挂载,包括模板编译,渲染,更新 - 如果定义了
template
,则需要进行编译:将template
字符串编译为render function
- 调用
$mount
的mountComponent
方法,先执行beforeCreate
周期函数,实例化一个渲染watcher
,在它的回调函数(初始化及数据变化时执行)中调用updateComponent
方法。 - 调用
render
方法将render function
渲染成虚拟dom - 生成虚拟DOM树后,调用
update
方法,update
方法会调用pacth
方法把虚拟DOM转换成真正的DOM节点
响应式流程
- 在
init
时会调用Object.defineProperty方法
监听实例的数据变化(get和set方法),从而实现数据劫持。 - 在初始化的编译阶段,会读取vue实例中与视图相关的响应式数据,
get
函数会进行订阅收集(把监听watcher
实例放到订阅者Dep
的数组sub中),这是数据劫持和订阅发布模式就形成了ViewModel - 数据或视图变化时,会触发数据劫持的
set
方法,set
会通知Dep
中相应的watcher
,watcher
调用update
方法来更新视图。
MVVM实现思路
MVVM双向数据绑定原理是通过 数据劫持+发布订阅 实现的。
通过
Object.defineProperty()
来给对象的属性添加get,set方法,在数据变动时触发相应的监听回调。
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
要实现一个MVVM的思路为:
- 实现一个数据劫持Observe,给对象添加get,set方法,监听到数据变动时,通知订阅者(Watcher)。get用来收集订阅,set用来派发更新。
- 实现一个解析编译Compile,对每个元素节点进行匹配,绑定和替换{{}}的内容
- 实现一个监听Watcher,连接Observe和Compile,收到数据变动的通知,执行相应回调函数,更新视图
- 实现一个消息订阅Dep,用一个数组来收集订阅者,数据变动触发notify,再调用订阅者的update方法
- 实现一个Vue入口函数
具体实现
vue.js
//Vue构造函数
function Vue(option = {}) {
this.$option = option;
let data = this._data = this.$option.data;
observe(data); // 数据劫持
// 数据代理,简化data数据的写法,如vue._data.name变成vue.name
for(let key in data) {
Object.defineProperty(this, key, {
configurable: true,
get() {
return this._data[key];
},
set(newVal) {
this._data[key] = newVal;
}
})
}
//初始化computed,将this指向实例
initComputed.call(this);
// 数据编译,解析{{}}的内容
new Compile(option.el, this);
//执行mounted钩子函数
option.mounted.call(this);
}
//数据劫持就是给对象增加get,set
function Observe(data) {
let dep =new Dep();
for(let key in data) {
let val = data[key];
observe(val) //递归继续向下,实现深度的数据劫持
// Object.defineProperty定义对象的属性
Object.defineProperty(data, key, {
configurable: true, // 可以配置对象,删除属性
get() {
Dep.target && dep.addSub(Dep.target); //将watcher实例添加到订阅事件中
return val
},
set(newVal){ //修改值的时候
if(val == newVal) { //值相同就不理
return;
}
val = newVal;
observe(newVal); //把新值也定义成属性
dep.notify(); //执行watcher中的update方法
}
})
}
}
//递归函数
function observe(data) {
if(!data || typeof data != 'object') return;
return new Observe(data);
}
//编译函数
function Compile(el, vm){
vm.$el = document.querySelector(el); // 将el挂载到实例上
let fragment = document.createDocumentFragment(); // 创建一个新的空白文档片段
while(child = vm.$el.firstChild) { //将el的内容都拿到,放入内存中,节省开销
fragment.appendChild(child);
}
//替换内容
function replace(frag){
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent;
let reg = /\{\{(.*?)\}\}/g; // 正则匹配{{}}
if(node.nodeType === 1 && reg.test(txt)) { //既是文本节点又是大括号{{}}
function replaceTxt() {
node.textContent = txt.replace(reg, (matched, placholder) => {
//placholder匹配到的分组,name,age
new Watcher(vm, placholder, replaceTxt); // 监听数据变化,替换{{}}的内容
return placholder.split('.').reduce((val, key) => { //reduce为数组的每个元素依次执行回调函数
return val[key]; //将vm的数据传给val做初始值
}, vm)
})
}
replaceTxt();
}
//实现双向绑定
if(node.nodeType === 1) {
let nodeAttr = node.attributes; //获取元素上的属性,类数组
Array.from(nodeAttr).forEach(attr => {
let name = attr.name; // v-model type
let exp = attr.value; // c
if(name.includes('v-')){
node.value = vm[exp]; // 将vm中的c的值,挂载到节点上
}
//监听数据变化
new Watcher(vm, exp, function(newVal){
node.value = newVal;
})
node.addEventListener('input', e => {
let newVal = e.target.value;
//给vm中的值赋值
vm[exp] = newVal;
})
})
}
//子节点
if(node.childNodes && node.childNodes.length) {
replace(node);
}
})
}
replace(fragment);
vm.$el.appendChild(fragment);
}
//发布订阅,把函数放入数组就是订阅,发布就是让函数执行
function Dep(){
this.subs = [];
}
Dep.prototype.addSub = function(sub) {
this.subs.push(sub);
}
Dep.prototype.notify = function() {
this.subs.forEach(sub => sub.update());
}
//监听函数,给这个类创建的实例,添加update方法
function Watcher(vm, exp, fn){
this.fn = fn; //将fn放到实例上
this.vm = vm;
this.exp = exp;
// 定义一个属性,target是Dep的一个静态属性,是一个全局watcher,dep实际上是对watcher的一种管理
Dep.target = this;
let arr = exp.split('.');
let val = vm;
arr.forEach(key => {
val = val[key]; //获取值的时候调用get()方法
})
Dep.target = null;
}
Watcher.prototype.update = function() {
// 值已经修改,再通过vm,exp来获取新的值
let arr = this.exp.split('.');
let val = this.vm;
arr.forEach(key => {
val = val[key]; //通过get()获取到新的值
})
this.fn(val); //fn为替换{{}}的内容
}
//实现Computed
function initComputed() {
let vm = this;
let computed = this.$option.computed; // 从option上拿到computed属性
Object.keys(computed).forEach(key => {
Object.defineProperty(vm, key, {
// 判断computed的key是对象还是函数,如果是函数会调get方法,如果是对象,手动调get方法
// sum获取a,b的值会调get方法
get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
set() {}
})
})
}
测试:
<div id="mvvm">
<p>{{name}}</p>
<p>{{age}}</p>
<input type="text" v-model='c'/>
<p>{{c}}</p>
</div>
<script>
let mvvm = new Vue({
el:'#mvvm',
data: {
name: '小明',
age: 20,
a: 10,
b: 30,
c: ''
},
computed: {
sum() {
return this.a + this.b
},
noop() {}
},
mounted() {
setTimeout(() => {
console.log('完成');
}, 1000);
}
})
</script>
参考: