Vue源码学习之实现model数据双向绑定和代理数据
- 接之前,首先实现model数据的绑定,修改MyVue.js文件,在compileUtile中修改model方法.
/**
*
* @param {*} exp 数据key
* @param {*} vm vue实例
* @param {*} inputVal 修改的值
* @returns
*/
setValue(exp, vm, inputVal) {
return exp.split('.').reduce((data, currentVal) => {
data[currentVal] = inputVal;
}, vm.$data);
},
model(node, exp, vm) {
let value = this.getValue(exp, vm);
new Watcher(vm, exp, (newValue) => {
this.updater.modelUpdater(node, newValue);
})
//-----------修改代码--------------
// V ==> M 视图修改数据
node.addEventListener('input', e => {
// 设置值
this.setValue(exp, vm, e.target.value);
})
//-----------修改代码--------------
this.updater.modelUpdater(node, value);
},
即初始化时,添加对input事件的监听,在修改输入值时同时修改vue中data对应的值。
-
添加数据代理
修改MyVue.js
class MyVue { // 构造函数 constructor(options) { this.$el = options.el; this.$data = options.data; this.$options = options; if (this.$el) { // 实现数据监听类 new Observer(this.$data); // 实现指令解析类 new Compile(this.$el, this); // 将this.$data 代理到 this this.proxyData(this.$data); } } proxyData(data) { for (let key in data) { Object.defineProperty(this, key, { get() { return data[key]; }, set(val) { data[key] = val; } }) } } }
即通过
Object.defineProperty
方法将this.$data
代理到Vue实例上。至此,简单实现了Vue中MVVM模式。
demo:Demo
整个过程在我的博客有全部文章。
完整源码:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue学习demo实现</title>
</head>
<body>
<div id="app">
<h2>大家好!我是{{person.name}},今年{{person.age}}岁。</h2>
<h3>我最喜欢的游戏是{{person.fav}}</h3>
<div v-text="msg">这不是真的msg</div>
<div v-html="htmlStr"></div>
<input type="text" v-model="msg">
<br />
<button v-on:click="handlerClick">按钮</button>
<button @click="handlerClick2">按钮2</button>
<br />
<a v-bind:href="url" target="_blank">{{url}}</a>
<a :href="url" target="_blank">百度一下</a>
</div>
<!-- <script src="Vue.js"></script> -->
<script src="MyVue.js"></script>
<script src="Observer.js"></script>
<script src="Watcher.js"></script>
<script>
vm = new MyVue({
el: "#app",
data: {
person: {
name: "小明",
age: 19,
fav: "LOL"
},
msg: "我很喜欢聊天",
htmlStr: "<h4>还喜欢和大家一起学习。</h4>",
url: "https://www.yanfee.com"
},
methods: {
handlerClick() {
console.log("this is a button!");
},
handlerClick2() {
this.$data.person.name = "大明"
console.log("button2");
}
}
})
</script>
<style>
#app {
background-color: rgb(209, 255, 136);
}
</style>
</div>
</body>
</html>
MyVue.js
/* 对指令进行编译实现 */
const compileUtil = {
/**
* 获取data中的值 如person.name="小明"
*/
getValue(exp, vm) {
// exp = person.fav
return exp.split(".").reduce((data, curValue) => {
return data[curValue]
}, vm.$data)
},
/**
*
* @param {*} exp 数据key
* @param {*} vm vue实例
* @param {*} inputVal 修改的值
* @returns
*/
setValue(exp, vm, inputVal) {
return exp.split('.').reduce((data, currentVal) => {
data[currentVal] = inputVal;
}, vm.$data);
},
getContentValue(exp, vm) {
return exp.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm)
})
},
/**
*
* @param {*} node 元素节点
* @param {string} exp 指令表达式 在data中找值需要
* @param {*} vm Vue实例
*/
text(node, exp, vm) {
let value;
if (exp.indexOf("{{") !== -1) {
// 对{{person.name}}进行替换
value = exp.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 添加watcher
new Watcher(vm, args[1], (newValue) => {
console.log(this.getContentValue(exp, vm));
this.updater.textUpdater(node, this.getContentValue(exp, vm));
})
// 获取到要替换的表达式 args中第二个元素即为需要的元素
// console.log(args);
return this.getValue(args[1], vm)
})
} else {
// 获取指令表达式的值
value = this.getValue(exp, vm);
new Watcher(vm, exp, (newValue) => {
this.updater.textUpdater(node, newValue);
})
}
// 更新
this.updater.textUpdater(node, value);
},
html(node, exp, vm) {
let value = this.getValue(exp, vm);
new Watcher(vm, exp, (newValue) => {
this.updater.htmlUpdater(node, newValue);
})
this.updater.htmlUpdater(node, value);
},
model(node, exp, vm) {
let value = this.getValue(exp, vm);
new Watcher(vm, exp, (newValue) => {
this.updater.modelUpdater(node, newValue);
})
// V ==> M 视图修改数据
node.addEventListener('input', e => {
// 设置值
this.setValue(exp, vm, e.target.value);
})
this.updater.modelUpdater(node, value);
},
/**
*
* @param {*} node 元素节点
* @param {*} exp 指令表达式
* @param {*} vm Vue实例
* @param {*} eventName 事件名称
*/
on(node, exp, vm, eventName) {
let fn = vm.$options.methods && vm.$options.methods[exp];
node.addEventListener(eventName, fn.bind(vm), false)
},
/**
*
* @param {*} node 元素节点
* @param {*} exp 指令表达式
* @param {*} vm Vue实例
* @param {*} attrName 属性名称
*/
bind(node, exp, vm, attrName) {
let value = this.getValue(exp, vm);
new Watcher(vm, exp, (newValue) => {
this.updater.bindUpdater(node, attrName, newValue);
})
this.updater.bindUpdater(node, attrName, value);
},
updater: {
// 更新文本内容
textUpdater(node, value) {
node.textContent = value;
},
// 插入html碎片
htmlUpdater(node, value) {
node.innerHTML = value;
},
// 绑定input的value
modelUpdater(node, value) {
node.value = value;
},
// 绑定属性
bindUpdater(node, attrName, attrValue) {
node.setAttribute(attrName, attrValue)
}
}
}
class Compile {
constructor(el, vm) {
// 检查el是否为dom节点 如果不是则获取节点
this.el = this.isElmentNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 获取文档碎片
const fragment = this.node2Fragment(this.el);
// 编译文档碎片
this.compile(fragment);
// 追加编译好的元素到根节点
this.el.appendChild(fragment);
}
// 实现指令编译方法
compile(fragment) {
// 获取子节点
const childNodes = fragment.childNodes;
// 识别节点类型并处理
[...childNodes].forEach(child => {
if (this.isElmentNode(child)) {
// 元素节点处理
this.compileElment(child);
} else {
// 文本节点处理
this.compileText(child)
}
if (child.childNodes && child.childNodes.length) {
this.compile(child)
}
})
}
// 处理元素节点
compileElment(node) {
const attributes = node.attributes; //获取元素属性
[...attributes].forEach(attr => {
const { name, value } = attr; // v-text="msg"
// console.log(name, value); // v-text msg
// 是否为指令
if (this.isDirective(name)) { // 得到指令 v-text v-model... v-on:click v-bind
const [, directive] = name.split("-"); // 得到指令 text model bind on:click
// 对on:click 进一步处理
const [directName, attrName] = directive.split(":")
// 编译指令工具类
compileUtil[directName](node, value, this.vm, attrName);
// 删除带有指令的属性标签
node.removeAttribute('v-' + directive);
} else if (this.isEventName(name)) { //绑定事件指令 @click
// 获取事件类型
let [, eventName] = name.split("@");
compileUtil["on"](node, value, this.vm, eventName);
// 删除带有指令的属性标签
node.removeAttribute('@' + eventName);
} else if (this.isBindName(name)) { //绑定属性简写 :src :style...
// 获取绑定属性的key
let [, attrName] = name.split(":");
compileUtil["bind"](node, value, this.vm, attrName);
// 删除带有指令的属性标签
node.removeAttribute(':' + attrName);
}
})
}
/**
* 编译文本指令 如 {{person.name}}
*/
compileText(node) {
const content = node.textContent;
// 正则匹配 {{***}} 内容
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil["text"](node, content, this.vm)
}
}
node2Fragment(node) {
const fragment = document.createDocumentFragment();
while (node.firstChild) {
fragment.appendChild(node.firstChild);
}
return fragment;
}
/** 识别是否为属性简写绑定 */
isBindName(attrName) {
return attrName.startsWith(":")
}
/** 识别是否为事件简写 */
isEventName(attrName) {
return attrName.startsWith("@")
}
/** 识别是否为指令 */
isDirective(attrName) {
return attrName.startsWith("v-")
}
/** 识别是否为元素节点 */
isElmentNode(node) {
return node.nodeType === 1;
}
}
class MyVue {
// 构造函数
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 实现数据监听类
new Observer(this.$data);
// 实现指令解析类
new Compile(this.$el, this);
// 将this.$data 代理到 this
this.proxyData(this.$data);
}
}
proxyData(data) {
for (let key in data) {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(val) {
data[key] = val;
}
})
}
}
}
Observer.js
class Observer {
constructor(data) {
this.observer(data);
}
// 实现数据变化监听
observer(data) {
// console.log(data);
// 判断数据是否为对象并处理
if (data && typeof data === 'object') {
// console.log(Object.keys(data));
// 利用Object.keys方法取出data的key值
Object.keys(data).forEach(key => {
// 定义响应方法 即劫持数据的getter和setter方法
this.defineReactive(data, key, data[key]);
})
}
}
/**
*
* @param {object} data vue实例中的data数据
* @param {string} key 遍历到的key值
* @param {string} value key值对应的value
*/
defineReactive(data, key, value) {
const self = this;
// 递归遍历 直到value不再为object
self.observer(value);
const dep = new Dep();
// 劫持并监听所有属性 利用Object.defineProperty重新定义get和set方法
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
// 如果是初始化Watcher 则添加到dep数组监听
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newVal) {
// 注意在修改对象值时,要对新值添加监听
self.observer(newVal)
if (value !== newVal) {
console.log("监听到数据变化:" + key + " 由 " + value + " 变成 " + newVal);
value = newVal;
}
dep.notify();
}
})
}
}
Watcher.js
class Watcher {
/**
*
* @param {*} vm Vue实例
* @param {*} exp data数据key值
* @param {*} cb 回调函数 用以修改视图
*/
constructor(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.preValue = this.getInitValue();
}
getInitValue() {
Dep.target = this;
// 添加Watcher时 获取初始值
const preValue = compileUtil.getValue(this.exp, this.vm);
Dep.target = null;
return preValue;
}
update() {
// 更新时检查当前值是否有变化 有变化则更新数值
const newValue = compileUtil.getValue(this.exp, this.vm);
if (newValue !== this.oldValue) {
this.cb(newValue)
}
}
}
class Dep {
constructor() {
// 初始化数组存放watcher
this.subs = []
}
// 收集watcher
addSub(watcher) {
this.subs.push(watcher)
}
// 通知watcher 更新
notify() {
// console.log("watcher:", this.subs);
this.subs.forEach(e => {
e.update()
})
}
}