怕你走开文章末尾有惊喜的!偷偷的告诉你这篇文章是有配套视频的哦!javascript
直接从主流的提及!
vue的特色没必要多说(简单易用)。修改数据方便不须要记忆api方法,这都归功于Object.defineProperty,它能够在数据的设置和获取时增长咱们本身的功能!(像墙同样)html
MVVM模式就要将这些板块进行整合,实现模板和数据的绑定!vue
看看我画图的功底,有个印象就好! html5
看段大众代码,接下来咱们就基于这段代码搞一下MVVM的实现java
<div id="app">
<!-- 双向数据绑定 靠的是表单 -->
<input type="text" v-model="message.a">
<div>我很帅</div>
{{message.a}} {{b}}
</div>
<script src="watcher.js"></script>
<script src="observer.js"></script>
<script src="compile.js"></script>
<script src="MVVM.JS"></script>
<script> // 咱们的数据通常都挂载在vm上 let vm = new MVVM({ el:'#app', data:{ message:{a:'珠峰架构'}, b:'珠峰MVVM' } }) </script>
复制代码
这里咱们用了本身的MVVM库,这个库是用来整合全部板块的!node
对es6还陌生的同窗,在这我在给你们附个经典es6视频~ 直接用ES6来打造咱们的MVVMes6
class MVVM{
constructor(options){
// 一上来 先把可用的东西挂载在实例上
this.$el = options.el;
this.$data = options.data;
// 若是有要编译的模板我就开始编译
if(this.$el){
// 用数据和元素进行编译
new Compile(this.$el, this);
}
}
}
复制代码
MVVM中调用了Compile类来编译咱们的页面,开始来实现模板编译面试
先来个基础的架子api
class Compile {
constructor(el, vm) {
// 看看传递的元素是否是DOM,不是DOM我就来获取一下~
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if (this.el) {
// 若是这个元素能获取到 咱们才开始编译
// 1.先把这些真实的DOM移入到内存中 fragment (性能优化)
let fragment = this.node2fragment(this.el);
// 2.编译 => 提取想要的元素节点 v-model 和文本节点 {{}}
this.compile(fragment);
// 3.把编译号的fragment在塞回到页面里去
this.el.appendChild(fragment);
}
}
/* 专门写一些辅助的方法 */
isElementNode(node) {
return node.nodeType === 1;
}
/* 核心的方法 */
compileElement(node) {}
compileText(node) {}
compile(fragment) {}
node2fragment(el) {}
}
复制代码
接下来一个个的方法来搞数组
node2fragment(el) { // 须要将el中的内容所有放到内存中
// 文档碎片 内存中的dom节点
let fragment = document.createDocumentFragment();
let firstChild;
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild);
// appendChild具备移动性
}
return fragment; // 内存中的节点
}
复制代码
compile(fragment) {
// 须要递归 每次拿子元素
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach(node => {
if (this.isElementNode(node)) {
// 是元素节点,还须要继续深刻的检查
// 这里须要编译元素
this.compileElement(node);
this.compile(node)
} else {
// 文本节点
// 这里须要编译文本
this.compileText(node);
}
});
}
复制代码
咱们在弄出两个方法compileElement,compileText来专门处理对应的逻辑
/*辅助的方法*/
// 是否是指令
isDirective(name) {
return name.includes('v-');
}
----------------------------
compileElement(node) {
// 带v-model v-text
let attrs = node.attributes; // 取出当前节点的属性
Array.from(attrs).forEach(attr => {
// 判断属性名字是否是包含v-model
let attrName = attr.name;
if (this.isDirective(attrName)) {
// 取到对应的值放到节点中
let expr = attr.value;
let [, type] = attrName.split('-'); //
// 调用对应的编译方法 编译哪一个节点,用数据替换掉表达式
CompileUtil[type](node, this.vm, expr);
}
})
}
compileText(node) {
let expr = node.textContent; // 取文本中的内容
let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}} {{c}}
if (reg.test(expr)) {
// 调用编译文本的方法 编译哪一个节点,用数据替换掉表达式
CompileUtil['text'](node, this.vm, expr);
}
}
复制代码
咱们要实现一个专门用来配合Complie类的工具对象
先只处理文本和输入框的状况
CompileUtil = {
text(node, vm, expr) { // 文本处理
let updateFn = this.updater['textUpdater'];
// 用处理好的节点和内容进行编译
updateFn && updateFn(node, value)
},
model(node, vm, expr) { // 输入框处理
let updateFn = this.updater['modelUpdater'];
// 用处理好的节点和内容进行编译
updateFn && updateFn(node, value);
},
updater: {
// 文本更新
textUpdater(node, value) {
node.textContent = value
},
// 输入框更新
modelUpdater(node, value) {
node.value = value;
}
}
}
复制代码
实现text方法
text(node, vm, expr) { // 文本处理
let updateFn = this.updater['textUpdater'];
// 文本比较特殊 expr多是'{{message.a}} {{b}}'
// 调用getTextVal方法去取到对应的结果
let value = this.getTextVal(vm, expr);
updateFn && updateFn(node, value)
},
getTextVal(vm, expr) { // 获取编译文本后的结果
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
// 依次去去数据对应的值
return this.getVal(vm, arguments[1]);
})
},
getVal(vm, expr) { // 获取实例上对应的数据
expr = expr.split('.'); // {{message.a}} [message,a] 实现依次取值
// vm.$data.message => vm.$data.message.a
return expr.reduce((prev, next) => {
return prev[next];
}, vm.$data);
}
复制代码
实现Model方法
model(node, vm, expr) { // 输入框处理
let updateFn = this.updater['modelUpdater'];
// 这里应该加一个监控 数据变化了 应该调用这个watch的callback
updateFn && updateFn(node, this.getVal(vm, expr));
}
复制代码
看下编译后的效果^_^
咱们一直说Object.defineProperty有劫持功能咱就看看这个是怎样劫持的
默认状况下定义属性给属性设置的操做是这样的
let school = {name:''}
school.name = 'zfpx'; // 当我给属性设置时但愿作一些操做
console.log(school.name); // 当我获取属性时也但愿对应有写操做
复制代码
这时候Object.defineProperty登场
let school = {name:''}
let val;
Object.defineProperty(school, 'name', {
enumerable: true, // 可枚举,
configurable: true, // 可配置
get() {
// todo
return val;
},
set(newVal) {
// todo
val = newVal
}
});
school.name = '珠峰培训';
console.log(school.name);
复制代码
这样咱们能够在设置值和获取值时作咱们想要作的操做了
接下来咱们就来写下一个类Observer
// 在MVVM加上Observe的逻辑
if(this.$el){
// 数据劫持 就是把对想的全部属性 改为get和set方法
new Observer(this.$data);
// 用数据和元素进行编译
new Compile(this.$el, this);
}
--------------------------------------
class Observer{
constructor(data){
this.observe(data);
}
observe(data){
// 要对这个data数据将原有的属性改为set和get的形式
// defineProperty针对的是对象
if(!data || typeof data !== 'object'){
return;
}
// 要将数据 一一劫持 先获取取到data的key和value
Object.keys(data).forEach(key=>{
// 定义响应式变化
this.defineReactive(data,key,data[key]);
this.observe(data[key]);// 深度递归劫持
});
}
// 定义响应式
defineReactive(obj,key,value){
// 在获取某个值的适合 想弹个框
let that = this;
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){ // 当取值时调用的方法
return value;
},
set(newValue){ // 当给data属性中设置值的适合 更改获取的属性的值
if(newValue!=value){
// 这里的this不是实例
that.observe(newValue);// 若是是设置的是对象继续劫持
value = newValue;
}
}
});
}
}
复制代码
来再看看效果^_^
观察者的目的就是给须要变化的那个元素增长一个观察者,用新值和老值进行比对,若是数据变化就执行对应的方法
class Watcher{ // 由于要获取老值 因此须要 "数据" 和 "表达式"
constructor(vm,expr,cb){
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先获取一下老的值 保留起来
this.value = this.get();
}
// 老套路获取值的方法,这里先不进行封装
getVal(vm, expr) {
expr = expr.split('.');
return expr.reduce((prev, next) => {
return prev[next];
}, vm.$data);
}
get(){
let value = this.getVal(this.vm,this.expr);
return value;
}
// 对外暴露的方法,若是值改变就能够调用这个方法来更新
update(){
let newValue = this.getVal(this.vm, this.expr);
let oldValue = this.value;
if(newValue != oldValue){
this.cb(newValue); // 对应watch的callback
}
}
}
复制代码
在哪里使用watcher?答案确定是compile呀,给须要从新编译的DOM增长watcher
text(node, vm, expr) { // 文本处理
let updateFn = this.updater['textUpdater'];
let value = this.getTextVal(vm, expr);
+ expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
+ new Watcher(vm, arguments[1],(newValue)=>{
+ // 若是数据变化了,文本节点须要从新获取依赖的属性更新文本中的内容
+ updateFn && updateFn(node,this.getTextVal(vm,expr));
+ });
+ })
updateFn && updateFn(node, value)
},
model(node, vm, expr) { // 输入框处理
let updateFn = this.updater['modelUpdater'];
+ new Watcher(vm,expr,(newValue)=>{
+ // 当值变化后会调用cb 将新的值传递过来
+ updateFn && updateFn(node, newValue);
+ });
updateFn && updateFn(node, this.getVal(vm, expr));
}
复制代码
如何将视图和数据关联起来呢?就是将每一个数据和对应的watcher关联起来。当数据变化时让对应的watcher执行update方法便可!再想一想在哪作操做呢?就是咱们的set和get!
Dep实现
class Dep{
constructor(){
// 订阅的数组
this.subs = []
}
addSub(watcher){
this.subs.push(watcher);
}
notify(){
this.subs.forEach(watcher=>watcher.update());
}
}
复制代码
关联dep和watcher
watcher中有个重要的逻辑就是this.get();每一个watcher被实例化时都会获取数据从而会调用当前属性的get方法
// watcher中的get方法
get(){
// 在取值前先将watcher保存到Dep上
Dep.target = this;
let value = this.getVal(this.vm,this.expr); // 会调用属性对应的get方法
Dep.target = null;
return value;
}
// 更新Observer中的defineReactive
defineReactive(obj,key,value){
let that = this;
+ let dep = new Dep(); // 每一个变化的数据 都会对应一个数组,这个数组是存放全部更新的操做
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){ // 当取值时调用的方法
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue){
if(newValue!=value){
that.observe(newValue);
value = newValue;
dep.notify(); // 通知全部人 数据更新了
}
}
});
}
复制代码
到此数据和视图就关联起来了!^_^
setVal(vm,expr,value){
expr = expr.split('.');
return expr.reduce((prev,next,currentIndex)=>{
if(currentIndex === expr.length-1){
return prev[next] = value;
}
return prev[next];
},vm.$data);
},
model(node, vm, expr) {
let updateFn = this.updater['modelUpdater'];
new Watcher(vm,expr,(newValue)=>{
// 当值变化后会调用cb 将新的值传递过来 ()
updateFn && updateFn(node, this.getVal(vm, expr));
});
+ node.addEventListener('input',(e)=>{
+ let newValue = e.target.value;
+ // 监听输入事件将输入的内容设置到对应数据上
+ this.setVal(vm,expr,newValue)
+ });
updateFn && updateFn(node, this.getVal(vm, expr));
}
复制代码
class MVVM{
constructor(options){
this.$el = options.el;
this.$data = options.data;
if(this.$el){
new Observer(this.$data);
// 将数据代理到实例上直接操做实例便可,不须要经过vm.$data来进行操做
this.proxyData(this.$data);
new Compile(this.$el, this);
}
}
proxyData(data){
Object.keys(data).forEach(key=>{
Object.defineProperty(this,key,{
get(){
return data[key]
},
set(newValue){
data[key] = newValue
}
})
})
}
}
复制代码
看看最终效果!
MVVM相关视频:
关注咱们珠峰培训架构课程: QQ群号:464438594