MVVM是指Model-View-ViewModeljavascript
init
过程当中初始化生命周期,初始化事件,初始化渲染,执行beforeCreate
周期函数,初始化data
,props
,computed
,watcher
,执行create
周期函数$mount
方法对vue实例进行挂载,包括模板编译,渲染,更新template
,则须要进行编译:将template
字符串编译为render function
$mount
的mountComponent
方法,先执行beforeCreate
周期函数,实例化一个渲染watcher
,在它的回调函数(初始化及数据变化时执行)中调用updateComponent
方法。render
方法将render function
渲染成虚拟domupdate
方法,update
方法会调用pacth
方法把虚拟DOM转换成真正的DOM节点init
时会调用Object.defineProperty方法
监听实例的数据变化(get和set方法),从而实现数据劫持。get
函数会进行订阅收集(把监听watcher
实例放到订阅者Dep
的数组sub中),这是数据劫持和订阅发布模式就造成了ViewModelset
方法,set
会通知Dep
中相应的watcher
,watcher
调用update
方法来更新视图。MVVM双向数据绑定原理是经过 数据劫持+发布订阅 实现的。html
经过
Object.defineProperty()
来给对象的属性添加get,set方法,在数据变更时触发相应的监听回调。vue
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。java
要实现一个MVVM的思路为:node
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() {}
})
})
}
复制代码
测试:app
<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>
复制代码
参考:dom