自家製のシンプルなフレームワークのMVVM
原則
- データ機関のためのObject.defineProperty()、getterメソッドとsetterメソッドを追加すると。データが変更されたときにこのように、セッターメソッドが実行されます
- フラグメントと()、テンプレートをコピーするには、メモリに保存します。
- 次に、ページ・フラグメント上に配置されたノードの種類、テンプレートと、命令解析上のノード、及びノードに対応するデータを付加する方法によって得られるNode.nodeType断片。
実装手順
MVVMを達成するために1。
- Vmがデータエージェントへのデータオブジェクトのオブジェクトを作成し 、保存するために渡されるデータパラメータの1.1ペアので、データ内のデータにアクセスするためにvm.keyを達成するために、キーのプロキシにデータの1.2ペアをVMに観察する方法に格納されたデータのために1.3の呼び出しすべてのデータ、プロキシデータを、説明およびゲッターとセッターメソッド追加方法はDEPオブジェクトと呼ばれ2.getterするVMに依存して、管理、セッターメソッド呼び出しは、更新メソッドを呼び出すためにDEP方法に通知がウォッチャーと呼ばれています。
- テンプレートのコンパイルが完了した後、コンパイルデータコールを代行(プロキシデータの完了後に行われなければなりません)
class mvvm{
constructor(options) {
// 将选项对象保存到vm
this.$options = options;
// 将data对象保存到vm和data变量中
this._data = this.$options.data
// 实现data对象的数据代理
this.observe(this._data);
// 执行编译
this.$compile = new Compile(options.el, this);
}
observe(value) {
if (!this.isObject(value)) {
return;
}
// 遍历value中所有的属性
Object.keys(value).forEach(key => {
// 为vue的data做属性代理
this.proxyData(key);
// 对指定属性实现响应式数据绑定
this.defineReactive(value, key, value[key])
});
}
proxyData(key) {
Object.defineProperty(this, key, {
configurable: false, // 不能再重新定义
enumerable: true, // 可以枚举
get() {
// 当通过vm.属性名 读取属性值时自动调用
return this._data[key];
},
set(newVal) {
// 将最新的值保存到data中对应的属性上(实现代理写操作)
this._data[key] = newVal;
},
});
}
defineReactive(obj, key, val) {
// 创建与当前属性相对应的Dep对象
let dep = new Dep();
// 递归查找嵌套属性
this.observe(val);
let this_ = this;
// 为data对象定义属性
Object.defineProperty(obj, key, {
configurable: false,
enumerable: true,
get() {
// 建立dep与watcher的关系
if (Dep.target) {
dep.depend();
}
console.log(dep.deps)
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
this_.observe(newVal);
// 通知watcher属性发生改变
dep.notify()
}
})
}
isObject(obj) {
return obj !== null && typeof obj === 'object'
}
}
2. comlileを達成するために
- ページノードを取得します。
- 子ノードは、フラグメントにコピーされます
- 解析操作カテゴリのノードの異なるタイプを使用してNODETYPE断片、更新メソッドを呼び出すは、結合またはデータ置換方法を解析し
、テキストノードを解析するために3.1:存在する場合は{{ }}
、直接、次いでtextContent
、対応するデータの交換
3.2指示要素ノードが解析されるが:
+v-html
:使用してinnerHTML
データの別の方法を
+v-model
:最初の値が割り当てられ、その後、addEventListener
人工的に変更した場合の結合方法は、対応するメソッド呼び出し
+@click
:コンテンツによりメソッドVMから取得addEventListener
し、この結合法に結合する方法に向け直し。
現在のウォッチャーの間でブリッジを構築し、コンパイルし、鍵と現在のデータのコールバックメソッドを保存するために使用されるデータウォッチャー加入者データにメソッドを追加するデータや割り当てを交換した後など、テキスト、HTML、モデル、3.3対、(コールバックメソッド)メソッドノードの更新を呼び出します。
// 扫描模板中所有依赖创建更新函数和watcher
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
if (this.$el) {
// 将dom节点转换为Frament提高执行效率
this.$fragment = this.node2Fragment(this.$el);
// 编译fragment中所有层次子节点
this.init()
// 将生成的结果追加到el中
this.$el.appendChild(this.$fragment)
}
}
node2Fragment(el) {
// 创建一个新的Fragment
let fragment = document.createDocumentFragment();
let child;
// 将原生节点拷贝至fragment
while ((child = el.firstChild)) {
fragment.appendChild(child);
}
return fragment;
}
init() {
// 编译fragment
this.compileElement(this.$fragment)
}
compileElement(el) {
let childNode = el.childNodes;
Array.from(childNode).forEach(node => {
if (this.isElementNode(node)) {
// 元素节点要识别指令属性
this.compile(node)
} else if (this.isTextNode(node) && /\{\{(.*)\}\}/.test(node.textContent)) {
// 文本节点,只关心{{xx}}格式
this.compileText(node, RegExp.$1) // RegExp.$1 匹配上面正则分组的内容
}
// 遍历可能存在的子节点
if (node.childNodes && node.childNodes.length) {
// 递归调用实现所有层次节点的编译
this.compileElement(node);
}
})
}
compile(node) {
// 获取所有标签属性节点
const nodeAttrs = node.attributes;
// 遍历所有属性
Array.from(nodeAttrs).forEach(attr => {
// attr: v-on:click="onclick" 或 @click="onClick", v-text="test"
// 属性名: v-on:click, v-text,@click
const attrName = attr.name
// 判断是否是指令属性
if (this.isDirective(attrName)) {
// 得到表达式(属性值): test,onClick,
let exp = attr.value;
// 判断是否是事件指令
if (this.isEventDirective(attrName)) {
// 解析事件指令
let dir = attrName.indexOf("@") >= 0 ? attrName.substr(1) : attrName.substr(5);
this.eventHandler(node, this.$vm, exp, dir)
} else {
let dir = attrName.substr(2);
// 解析普通指令
this[dir] && this[dir](node, this.$vm, exp)
}
// 移除指令属性
node.removeAttribute(attrName);
}
})
}
compileText(node, exp) {
this.text(node, this.$vm, exp.replace(/^\s+|\s+$/g, ''));
}
isElementNode(node) {
return node.nodeType == 1; // 元素节点
}
isTextNode(node) {
return node.nodeType == 3; // 文本节点
}
// 判断是否是指令
isDirective(attr) {
return /^v-|^@|^:/.test(attr);
}
// 判断是否是事件指令
isEventDirective(dir) {
return /^@|^v-on:/.test(dir);
}
// 解析v-text/{{}}
text(node, vm, exp) {
this.update(node, vm, exp, 'text')
}
// 解析v-html
html(node, vm, exp) {
this.update(node, vm, exp, 'html')
}
// 解析v-class
class(node, vm, exp) {
this.update(node, vm, exp, 'class')
}
// 解析v-mode
model(node, vm, exp) {
this.update(node, vm, exp, 'model')
let val = this.getVMVal(vm, exp)
node.addEventListener('input', e => {
let newValue = e.target.value;
if (val === newValue) {
return;
}
this.setVmVal(vm, exp, newValue);
val = newValue;
})
}
// 得到表达式对应的value
getVMVal(vm, exp) {
let val = vm._data;
exp = exp.split('.');
exp.forEach(k => {
val = val[k];
});
return val;
}
setVmVal(vm, exp, value) {
let val = vm._data;
exp = exp.split('.');
exp.forEach((k, i) => {
if (i < exp.length - 1) {
val = val[k];
} else {
val[k] = value
}
})
}
// 事件处理
eventHandler(node, vm, exp, dir) {
let fn = vm.$options.methods && vm.$options.methods[exp];
if (dir && fn) {
// 1.绑定指定事件名和回调函数的DOM事件监听
// 2.将回调函数中的this强制绑定为vm
node.addEventListener(dir, fn.bind(vm), false)
}
}
// 更新节点方法
update(node, vm, exp, dir) {
let updaterFn = this[dir + "Updater"];
// 执行节点更新
updaterFn && updaterFn(node, this.getVMVal(vm, exp));
// 创建表达式对应的watcher对象
new Watcher(vm, exp, (val, oldVal) => {
// 当对应的属性值发生了变化时, 自动调用, 更新对应的节点
updaterFn && updaterFn(node, val, oldVal);
})
}
textUpdater(node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
}
htmlUpdater(node, value) {
node.innerHTML = typeof value == 'undefined' ? '' : value;
}
modelUpdater(node, value) {
node.value = typeof value == 'undefined' ? '' : value;
}
classUpdater(node, value, oldValue) {
let className = node.className;
className = className.replace(oldValue, '').replace(/\s$/, '');
let space = className && String(value) ? ' ' : '';
node.className = className + space + value;
}
}
3.ウォッチャーを達成するために
観察し、コンパイルの間の通信のブリッジとしてウォッチャーの加入者。
- 加入者は、DEPにウォッチャーサブスクライバを追加橋を構築し、ウォッチャーを観察するために、プロキシデータのgetterメソッドをトリガーする追加の時間をcomlile。
- データ代理コールでsetterメソッドは、すべてのマネージャーウォッチャーを頼るDEPに保存され、その後、最新のデータのためのgetメソッドのトリガー、トリガーのコールバックを遅らせる呼び出し、更新ノードを呼び出す控えめに更新されたページがデータであるアップデータメソッドをトリガーします。
// Watcher: 订阅者:具体的更新执行者
class Watcher {
constructor(vm, exp, callack) {
this.vm = vm;
this.exp = exp;
this.cb = callack;
this.depIds = {};
this.value = this.get();
}
update() {
// 得到最新的值
let value = this.get();
// 得到旧值
let oldVal = this.value;
// 如果不相同
if (value !== oldVal) {
this.value = value;
// 调用回调函数更新对应的界面
this.cb.call(this.vm, value, oldVal);
}
}
addDep(dep) {
if (!this.depIds.hasOwnProperty(dep.id)) {
// 建立dep到wacther的关系
dep.addDep(this)
// 建立wacther到dep的关系
this.depIds[dep.id] = dep;
}
}
get() {
Dep.target = this;
// 获取当前表达式的值, 内部会导致属性的get()调用
let value = this.getVMVal();
Dep.target = null;
return value;
}
getVMVal() {
let exp = this.exp.split('.');
let val = this.vm._data;
exp.forEach(key => {
val = val[key]
})
return val;
}
}
4. DEPを実装
通知を追加し、頼る含め、すべての依存コレクトVMの管理責任:DEPがマネージャーを頼ります
var uid = 0;
// 依赖管理器:负责将vm中的所有依赖收集管理,包括依赖添加和通知
class Dep {
constructor() {
this.id = uid++;
this.deps = []; // 存放相关的所有watcher数组
}
addDep(dep) {
this.deps.push(dep)
}
depend() {
Dep.target.addDep(this);
}
notify() {
// 通知所有相关的watcher执行更新
this.deps.forEach(dep => {
dep.update();
})
}
}
テスト:
<div id="app">
<p>{{ msg }}</p>
<p v-html="msg" v-class="red"></p>
<input type="text" v-model="msg">
<button @click="click">点击</button>
</div>
<!-- <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> -->
<script type="text/javascript" src="js/compile.js"></script>
<script type="text/javascript" src="js/mvvm.js"></script>
<script type="text/javascript" src="js/Dep.js"></script>
<script type="text/javascript" src="js/watcher.js"></script>
<script>
var vm = new mvvm({
el: '#app',
data: {
msg: '<span>test</span>',
red:'red'
},
methods: {
click() {
alert('点击')
}
}
})
</script>