原文链接: mvvm oop 实现
上一篇: mvvm vue 实现原理 介绍
下一篇: js 继承
实现和vue类似的效果
项目结构
compile.js
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
// 将绑定的dom实例的所有层次子节点保存为fragment
this.$fragment = this.node2Fragment(this.$el);
// 编译fragment中所有层次子节点
this.init();
// 将编译好的fragment添加到挂载的el元素上
this.$el.appendChild(this.$fragment);
}
}
node2Fragment(el) {
let fragment = document.createDocumentFragment()
let child;
// 将原生dom节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
}
init() {
this.compileElement(this.$fragment);
}
// 编译改节点下的所有层次子节点
compileElement(el) {
Array.from(el.childNodes).forEach((node) => {
let text = node.textContent;
// 提取表达式 {{msg}}
let reg = /\{\{(.*)\}\}/;
if (this.isElementNode(node)) {
// 如果是元素结点,递归继续编译
// 编译属性指令
this.compile(node);
} else if (this.isTextNode(node) && reg.test(text)) {
// 如果是使用了{{msg}}形式的 的文本结点
// 将表达式转化为普通字符串
this.compileText(node, RegExp.$1.trim());
}
// 如果该结点下还有其他元素标签
if (node.childNodes && node.childNodes.length) {
this.compileElement(node);
}
});
}
compile(node) {
let nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach(attr => {
let attrName = attr.name;
if (this.isDirective(attrName)) {
let exp = attr.value;
// v-on --> on dir 为指令类型
let dir = attrName.substring(2);
if (this.isEventDirective(dir)) {
// 事件指令
compileUtil.eventHandler(node, this.$vm, exp, dir);
} else {
// 普通指令
compileUtil[dir] && compileUtil[dir](node, this.$vm, exp);
}
// 移除属性值,在渲染时不显示
node.removeAttribute(attrName);
}
});
}
compileText(node, exp) {
compileUtil.text(node, this.$vm, exp);
}
isDirective(attr) {
return attr.indexOf('v-') == 0;
}
isEventDirective(dir) {
return dir.indexOf('on') === 0;
}
isElementNode(node) {
return node.nodeType == 1;
}
isTextNode(node) {
return node.nodeType == 3;
}
};
// 指令处理集合
const compileUtil = {
text: function (node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
html: function (node, vm, exp) {
this.bind(node, vm, exp, 'html');
},
model: function (node, vm, exp) {
// v-model 数据双向绑定
// 先将数据设置到input的value中
this.bind(node, vm, exp, 'model');
// 得到初始值
let val = this._getVMVal(vm, exp);
// 使用闭包,在监听事件的回调函数中,如果值有更新
// 则重新将值更新到input的value中
node.addEventListener('input', (e) => {
let newValue = e.target.value;
if (val === newValue) {
return;
}
this._setVMVal(vm, exp, newValue);
val = newValue;
});
},
class: function (node, vm, exp) {
this.bind(node, vm, exp, 'class');
},
bind: function (node, vm, exp, dir) {
let updaterFn = updater[dir + 'Updater'];
console.log('bind')
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
// 表达式来源有大括号表达式和指令表达式
new Watcher(vm, exp, function (value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
},
// 事件处理
eventHandler: function (node, vm, exp, dir) {
let eventType = dir.split(':')[1]
let fn = vm.$options.methods && vm.$options.methods[exp];
if (eventType && fn) {
// 回调函数使用bind强制绑定this
node.addEventListener(eventType, fn.bind(vm), false);
}
},
_getVMVal: function (vm, exp) {
let val = vm;
exp = exp.split('.');
exp.forEach(function (k) {
val = val[k];
});
return val;
},
_setVMVal: function (vm, exp, value) {
let val = vm;
exp = exp.split('.');
exp.forEach(function (k, i) {
if (i < exp.length - 1) {
// 非最后一个key,更新val的值
val = val[k];
} else {
// 最后一个key, 赋新值
val[k] = value;
}
});
}
};
// 包含多个更新节点的方法工具对象
// 执行node的更新
const updater = {
textUpdater: function (node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
},
htmlUpdater: function (node, value) {
node.innerHTML = typeof value == 'undefined' ? '' : value;
},
classUpdater: function (node, value, oldValue) {
let className = node.className;
console.log(className, node, value, oldValue)
// 'a b' value='c' oldval = 'a b'
// 替换后 ' c' ==> 'c'
className = className.replace(oldValue, '').replace(/\s$/, '');
// 如果有类名用空格连接
console.log(className)
let space = className && String(value) ? ' ' : '';
node.className = className + space + value;
},
modelUpdater: function (node, value, oldValue) {
node.value = typeof value == 'undefined' ? '' : value;
}
};
observer.js
class Observer {
constructor(data) {
this.data = data;
this.walk(data);
}
walk(data) {
Object.keys(data).forEach((key) => {
this.convert(key, data[key]);
});
}
convert(key, val) {
this.defineReactive(this.data, key, val);
}
defineReactive(data, key, val) {
let dep = new Dep();
// let childObj = observe(val);
observe(val);
// 使用闭包完成关系绑定
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: function () {
if (Dep.target) {
// depend() {
// Dep.target.addDep(this);
// }
// 当key被使用的时候,如果target有值(是一个watcher)
// 则该target需要依赖此dep,且dep的subs数组中加入watcher
// addDep(dep) {
// if (!this.depIds.hasOwnProperty(dep.id)) {
// dep.addSub(this);
// this.depIds[dep.id] = dep;
// }
// }
dep.depend();
}
return val;
},
set: function (newVal) {
// 使用闭包,完成更新
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
observe(newVal);
// 通知订阅者,执行订阅者的update方法,更新node中的数据
dep.notify();
}
});
}
};
function observe(value) {
if (!value || typeof value !== 'object') {
// 递归结束条件,value为空或者不是对象
return;
}
new Observer(value);
};
// 标示dep的唯一id,自增
let uid = 0;
class Dep {
constructor() {
this.id = uid++;
this.subs = [];
console.log(uid)
}
addSub(sub) {
this.subs.push(sub);
}
depend() {
Dep.target.addDep(this);
}
removeSub(sub) {
let index = this.subs.indexOf(sub);
if (index != -1) {
this.subs.splice(index, 1);
}
}
notify() {
// 通知所有订阅者更新
this.subs.forEach((sub) => {
sub.update();
});
}
};
// 借助全局变量做了两个类之间的参数传递和数据通信
Dep.target = null;
watcher.js
class Watcher {
constructor(vm, expOrFn, cb) {
// watcher 会在一开始初始化一个实例
// 然后每次有一个表达式或者属性指令,都会再次实例化一个
console.log('watcher init')
this.cb = cb;
this.vm = vm;
this.expOrFn = expOrFn;
// 如果 a.b.c
// 则depIds 就有三个uid
this.depIds = {};
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = this.parseGetter(expOrFn.trim());
}
this.value = this.get();
}
update() {
this.run();
}
run() {
// 新的值
let value = this.get();
// 旧值
let oldVal = this.value;
if (value !== oldVal) {
// 更新
this.value = value;
// 绑定this,并且将新值和旧值传递给回调函数
this.cb.call(this.vm, value, oldVal);
}
}
addDep(dep) {
// 1. 每次调用run()的时候会触发相应属性的getter
// getter里面会触发dep.depend(),继而触发这里的addDep
// 2. 假如相应属性的dep.id已经在当前watcher的depIds里,说明不是一个新的属性,仅仅是改变了其值而已
// 则不需要将当前watcher添加到该属性的dep里
// 3. 假如相应属性是新的属性,则将当前watcher添加到新属性的dep里
// 如通过 vm.child = {name: 'a'} 改变了 child.name 的值,child.name 就是个新属性
// 则需要将当前watcher(child.name)加入到新的 child.name 的dep里
// 因为此时 child.name 是个新值,之前的 setter、dep 都已经失效,如果不把 watcher 加入到新的 child.name 的dep中
// 通过 child.name = xxx 赋值的时候,对应的 watcher 就收不到通知,等于失效了
// 4. 每个子属性的watcher在添加到子属性的dep的同时,也会添加到父属性的dep
// 监听子属性的同时监听父属性的变更,这样,父属性改变时,子属性的watcher也能收到通知进行update
// 这一步是在 this.get() --> this.getVMVal() 里面完成,forEach时会从父级开始取值,间接调用了它的getter
// 触发了addDep(), 在整个forEach过程,当前watcher都会加入到每个父级过程属性的dep
// 例如:当前watcher的是'child.child.name', 那么child, child.child, child.child.name这三个属性的dep都会加入当前watcher
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this);
this.depIds[dep.id] = dep;
}
}
get() {
Dep.target = this;
// 第一个参数绑定this, 第二个参数会传递给准备执行的函数
// 设置全局Dep.target 调用get时, 借助闭包建立关系
let value = this.getter.call(this.vm, this.vm);
Dep.target = null;
return value;
}
parseGetter(exp) {
// 类似于 a. 的不合法表达式
if (/[^\w.$]/.test(exp)) return;
let exps = exp.split('.');
// 实现 类似 a.b.c 的监听
return function (obj) {
for (let i = 0, len = exps.length; i < len; i++) {
if (!obj) return;
obj = obj[exps[i]];
}
return obj;
}
}
};
mvvm.js
class MVVM {
constructor(options) {
// 保存配置,防止使用空对象
this.$options = options || {};
// _data 用于保存真实数据
let data = this._data = this.$options.data;
// 数据代理
// 实现 vm.xxx 代理 vm._data.xxx
Object.keys(data).forEach((key) => {
this._proxyData(key);
});
this._initComputed();
observe(data, this);
this.$compile = new Compile(options.el || document.body, this)
}
$watch(key, cb, options) {
new Watcher(this, key, cb);
}
_proxyData(key, setter, getter) {
setter = setter ||
Object.defineProperty(this, key, {
configurable: false, // 不可再重新定义变量
enumerable: true, // 可枚举
get: () => {
return this._data[key];
},
set: (newVal) => {
this._data[key] = newVal;
}
});
}
_initComputed() {
let computed = this.$options.computed;
if (typeof computed === 'object') {
Object.keys(computed).forEach((key) => {
Object.defineProperty(this, key, {
// 计算属性的写法有两种
// 一种是函数式,一种是对象式
get: typeof computed[key] === 'function'
? computed[key]
: computed[key].get,
set: function () {
}
});
});
}
}
}
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MVVM</title>
<style>
.box {
background: deepskyblue;
}
.font {
color: blue;
}
</style>
</head>
<body>
<div id="mvvm-app">
<input type="text" v-model="someStr">
<input type="text" v-model="child.someStr">
<h1 v-text="child.someStr"></h1>
<h1 v-text="child.someStr"></h1>
<h1>==={{ getHelloWord }}===</h1>
<p v-html="htmlStr"></p>
<button v-on:click="clickBtn">change model</button>
<div class="box" v-class="cla">
box
</div>
<div class="box" v-class="cla">
box
</div>
</div>
<script src="./js_oop/observer.js"></script>
<script src="./js_oop/watcher.js"></script>
<script src="./js_oop/compile.js"></script>
<script src="./js_oop/mvvm.js"></script>
<script>
var vm = new MVVM({
el: '#mvvm-app',
data: {
cla: 'font',
someStr: 'hello ',
className: 'btn',
htmlStr: '<span style="color: #f00;">red</span>',
child: {
someStr: 'World !',
text: 'World !',
}
},
computed: {
getHelloWord: function () {
return this.someStr + this.child.someStr;
}
},
methods: {
clickBtn: function (e) {
let randomStrArr = ['childOne', 'childTwo', 'childThree'];
this.child.someStr = randomStrArr[parseInt(Math.random() * 3)];
}
}
});
vm.$watch('child.someStr', function (val, oldVal) {
console.log('child.someStr', val, '===', oldVal);
});
</script>
</body>
</html>