vue源码解析-实现一个基础的MVVM框架
基本介绍
vue.js采用数据劫持结合发布-订阅模式的方式,通过Object.defineProperty()来劫持各个属性的getter,setter,在数据变动时发布消息给订阅者,触发响应的监听回调。
主要功能:
- 实现一个指令解析器Compile
- 实现一个数据监听器Observer
- 实现一个Watcher去更新视图
入口函数和Compile编译类实现
HTML代码示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-text="msg"></div>
<div v-text="person.fav"></div>
<div v-html="htmlStr"></div>
<input type="test" v-model="msg">
<button v-on:click="handleClick">点击我on</button>
<button @click="handleClick">点击我@</button>
<img v-bind:src="imgUrl" />
<img :src="imgUrl" />
</div>
<script src="./MVue.js"></script>
<script>
let vm = new MVue({
el: '#app',
data: {
person: {
name: '编程哥',
age: 18,
fav: '看电影'
},
msg: '学习MVVM实现原理',
htmlStr: '大家学习得怎么样',
imgUrl: './logo.png'
},
methods: {
handleClick () {
console.log(this)
}
}
})
</script>
</body>
</html>
MVue.js代码示例:
const compileUtil = {
getValue(expr, vm) {
return expr.split(".").reduce((pre, cur) => {
return pre[cur];
}, vm.$data);
},
text(node, expr, vm) {
// expr:msg 学习MVVM实现原理 // <div v-text="person.fav"></div>
let value;
if (expr.indexOf("{{") !== -1) {
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm);
});
} else {
value = this.getValue(expr, vm);
}
this.updater.textUpdater(node, value);
},
html(node, expr, vm) {
const value = this.getValue(expr, vm);
this.updater.htmlUpdater(node, value);
},
model(node, expr, vm) {
const value = this.getValue(expr, vm);
this.updater.modelUpdater(node, value);
},
on(node, expr, vm, eventName) {
const fn = vm.$options.methods && vm.$options.methods[expr];
node.addEventListener(eventName, fn.bind(vm), false);
},
bind(node, expr, vm, attrName) {
const value = this.getValue(expr, vm);
this.updater.bindUpdater(node, attrName, value);
},
// 更新的函数
updater: {
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
modelUpdater(node, value) {
node.value = value;
},
bindUpdater(node, attrName, value) {
node.setAttribute(attrName, value);
},
},
};
class Compiler {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 1.获取文档碎片对象,放入内存中会减少页面的回流和重绘
const fragment = this.node2Fragment(this.el);
// 2.编译模板
this.Compiler(fragment);
// 3.追加子元素到根元素
this.el.appendChild(fragment);
}
/*
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-test="msg"></div>
<div v-html="htmlStr"></div>
<input type="test" v-model="msg">
*/
Compiler(fragment) {
// 获取子节点
const childNodes = fragment.childNodes;
[...childNodes].forEach((child) => {
// console.log(child);
if (this.isElementNode(child)) {
// 是元素节点
// 编译元素节点
// console.log("元素节点", child);
this.compilerElement(child);
} else {
// 文本节点
// 编译文本节点
// console.log("文本节点", child);
this.compilerText(child);
}
if (child.childNodes && child.childNodes.length) {
this.Compiler(child);
}
});
}
compilerElement(node) {
const attributes = node.attributes;
[...attributes].forEach((attr) => {
const { name, value } = attr;
if (this.isDirective(name)) {
// 是一个指令 v-text v-html v-model v-on:click v-bind:src
const [, directive] = name.split("-"); // text html model on:click bind:src
const [dirName, eventName] = directive.split(":"); // text html model on bind
// 更新数据,数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName);
// 删除有指令的标签上的属性
node.removeAttribute("v-" + directive);
} else if (this.isEventName(name)) {
// @click="handleClick"
const [, eventName] = name.split("@");
compileUtil["on"](node, value, this.vm, eventName);
// 删除标签上的绑定的事件
node.removeAttribute("@" + eventName);
} else if (this.isAttrName(name)) {
// :src="imgUrl"
const [, attrName] = name.split(":");
compileUtil["bind"](node, value, this.vm, attrName);
// 删除标签上绑定的属性
node.removeAttribute(":" + attrName);
}
});
}
compilerText(node) {
// {{}} v-text
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil["text"](node, content, this.vm);
}
}
isAttrName(attrName) {
return attrName.startsWith(":");
}
isEventName(attrName) {
return attrName.startsWith("@");
}
isDirective(attrName) {
return attrName.startsWith("v-");
}
node2Fragment(el) {
// 创建文档碎片
const f = document.createDocumentFragment();
let firstChild;
while ((firstChild = el.firstChild)) {
f.appendChild(firstChild);
}
return f;
}
isElementNode(node) {
// 判断是否是元素节点对象
return node.nodeType === 1;
}
}
class MVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 1.实现一个数据观察者
// 2.实现一个指令解析器
new Compiler(this.$el, this);
}
}
}
实现Observer劫持并监听所有属性
新建一个Observer.js类,示例代码如下:
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
if (data && typeof data === "object") {
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key]);
});
}
}
defineReactive(obj, key, value) {
// 递归遍历劫持
this.observe(value);
Object.defineProperty(obj, key, {
configurable: false,
enumerable: true,
get() {
// 订阅数据变化时,往Dep中添加观察者
return value;
},
set: (newVal) => {
this.observe(newVal);
if (newVal !== value) {
value = newVal;
}
},
});
}
}
html代码引用:
<script src="./Observer.js"></script>
<script src="./MVue.js"></script>
在Mvue.js类中的调用方式:
class MVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 1.实现一个数据观察者
new Observer(this.$data);
// 2.实现一个指令解析器
new Compiler(this.$el, this);
}
}
}
实现观察者Watcher和依赖收集器Dep
Observer.js代码中新增类Dep(收集观察者与通知观察者)和Watcher:
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先把旧值保存起来
this.oldVal = this.getOldVal();
}
getOldVal() {
Dep.target = this;
let oldVal = compileUtil.getValue(this.expr, this.vm);
Dep.target = null;
return oldVal;
}
update() {
const newVal = compileUtil.getValue(this.expr, this.vm);
if (newVal !== this.oldVal) {
this.cb(newVal);
}
}
}
class Dep {
constructor() {
this.subs = [];
}
// 收集观察者
addSub(watcher) {
this.subs.push(watcher);
}
// 通知观察者去更新
notify() {
console.log("观察者", this.subs);
this.subs.forEach((w) => w.update());
}
}
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
if (data && typeof data === "object") {
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key]);
});
}
}
defineReactive(obj, key, value) {
// 递归遍历劫持
this.observe(value);
const dep = new Dep();
Object.defineProperty(obj, key, {
configurable: false,
enumerable: true,
get() {
// 订阅数据变化时,往Dep中添加观察者
Dep.target && dep.addSub(Dep.target);
return value;
},
set: (newVal) => {
this.observe(newVal);
if (newVal !== value) {
value = newVal;
}
// 告诉Dep通知变化
dep.notify();
}
});
}
}
MVue.js代码:
const compileUtil = {
getValue(expr, vm) {
return expr.split(".").reduce((pre, cur) => {
return pre[cur];
}, vm.$data);
},
getContentValue(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm);
});
},
text(node, expr, vm) {
// expr:msg 学习MVVM实现原理 // <div v-text="person.fav"></div>
let value;
if (expr.indexOf("{{") !== -1) {
// {{person.name}} -- {{person.age}}
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], () => {
this.updater.textUpdater(node, this.getContentValue(expr, vm));
});
return this.getValue(args[1], vm);
});
} else {
new Watcher(vm, expr, (newVal) => {
this.updater.textUpdater(node, newVal);
});
value = this.getValue(expr, vm);
}
this.updater.textUpdater(node, value);
},
html(node, expr, vm) {
const value = this.getValue(expr, vm);
new Watcher(vm, expr, (newVal) => {
this.updater.htmlUpdater(node, newVal);
});
this.updater.htmlUpdater(node, value);
},
model(node, expr, vm) {
const value = this.getValue(expr, vm);
// 绑定更新函数 数据=>视图
new Watcher(vm, expr, (newVal) => {
this.updater.modelUpdater(node, newVal);
});
this.updater.modelUpdater(node, value);
},
on(node, expr, vm, eventName) {
const fn = vm.$options.methods && vm.$options.methods[expr];
node.addEventListener(eventName, fn.bind(vm), false);
},
bind(node, expr, vm, attrName) {
const value = this.getValue(expr, vm);
this.updater.bindUpdater(node, attrName, value);
},
// 更新的函数
updater: {
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
modelUpdater(node, value) {
node.value = value;
},
bindUpdater(node, attrName, value) {
node.setAttribute(attrName, value);
}
}
};
class Compiler {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 1.获取文档碎片对象,放入内存中会减少页面的回流和重绘
const fragment = this.node2Fragment(this.el);
// 2.编译模板
this.Compiler(fragment);
// 3.追加子元素到根元素
this.el.appendChild(fragment);
}
/*
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-test="msg"></div>
<div v-html="htmlStr"></div>
<input type="test" v-model="msg">
*/
Compiler(fragment) {
// 获取子节点
const childNodes = fragment.childNodes;
[...childNodes].forEach((child) => {
// console.log(child);
if (this.isElementNode(child)) {
// 是元素节点
// 编译元素节点
// console.log("元素节点", child);
this.compilerElement(child);
} else {
// 文本节点
// 编译文本节点
// console.log("文本节点", child);
this.compilerText(child);
}
if (child.childNodes && child.childNodes.length) {
this.Compiler(child);
}
});
}
compilerElement(node) {
const attributes = node.attributes;
[...attributes].forEach((attr) => {
const { name, value } = attr;
if (this.isDirective(name)) {
// 是一个指令 v-text v-html v-model v-on:click v-bind:src
const [, directive] = name.split("-"); // text html model on:click bind:src
const [dirName, eventName] = directive.split(":"); // text html model on bind
// 更新数据,数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName);
// 删除有指令的标签上的属性
node.removeAttribute("v-" + directive);
} else if (this.isEventName(name)) {
// @click="handleClick"
const [, eventName] = name.split("@");
compileUtil["on"](node, value, this.vm, eventName);
// 删除标签上的绑定的事件
node.removeAttribute("@" + eventName);
} else if (this.isAttrName(name)) {
// :src="imgUrl"
const [, attrName] = name.split(":");
compileUtil["bind"](node, value, this.vm, attrName);
// 删除标签上绑定的属性
node.removeAttribute(":" + attrName);
}
});
}
compilerText(node) {
// {{}} v-text
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil["text"](node, content, this.vm);
}
}
isAttrName(attrName) {
return attrName.startsWith(":");
}
isEventName(attrName) {
return attrName.startsWith("@");
}
isDirective(attrName) {
return attrName.startsWith("v-");
}
node2Fragment(el) {
// 创建文档碎片
const f = document.createDocumentFragment();
let firstChild;
while ((firstChild = el.firstChild)) {
f.appendChild(firstChild);
}
return f;
}
isElementNode(node) {
// 判断是否是元素节点对象
return node.nodeType === 1;
}
}
class MVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 1.实现一个数据观察者
new Observer(this.$data);
// 2.实现一个指令解析器
new Compiler(this.$el, this);
}
}
}
实现双向的数据绑定和Proxy代理
MVue.js代码:
const compileUtil = {
getValue(expr, vm) {
return expr.split(".").reduce((pre, cur) => {
return pre[cur];
}, vm.$data);
},
setValue(expr, vm, inputVal) {
// 将expr的点语法的字符串转换成数组并设置最有一个值为inputVal
return expr.split(".").reduce((pre, cur, index, arr) => {
if (index === arr.length - 1) {
pre[cur] = inputVal;
}
return pre[cur];
}, vm.$data);
},
getContentValue(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm);
});
},
text(node, expr, vm) {
// expr:msg 学习MVVM实现原理 // <div v-text="person.fav"></div>
let value;
if (expr.indexOf("{{") !== -1) {
// {{person.name}} -- {{person.age}}
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], () => {
this.updater.textUpdater(node, this.getContentValue(expr, vm));
});
return this.getValue(args[1], vm);
});
} else {
new Watcher(vm, expr, (newVal) => {
this.updater.textUpdater(node, newVal);
});
value = this.getValue(expr, vm);
}
this.updater.textUpdater(node, value);
},
html(node, expr, vm) {
const value = this.getValue(expr, vm);
new Watcher(vm, expr, (newVal) => {
this.updater.htmlUpdater(node, newVal);
});
this.updater.htmlUpdater(node, value);
},
model(node, expr, vm) {
const value = this.getValue(expr, vm);
// 绑定更新函数 数据=>视图
new Watcher(vm, expr, (newVal) => {
this.updater.modelUpdater(node, newVal);
});
// 视图=>数据=>视图
node.addEventListener("input", (e) => {
// 设置值
this.setValue(expr, vm, e.target.value);
});
this.updater.modelUpdater(node, value);
},
on(node, expr, vm, eventName) {
const fn = vm.$options.methods && vm.$options.methods[expr];
node.addEventListener(eventName, fn.bind(vm), false);
},
bind(node, expr, vm, attrName) {
const value = this.getValue(expr, vm);
this.updater.bindUpdater(node, attrName, value);
},
// 更新的函数
updater: {
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
modelUpdater(node, value) {
node.value = value;
},
bindUpdater(node, attrName, value) {
node.setAttribute(attrName, value);
}
}
};
class Compiler {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 1.获取文档碎片对象,放入内存中会减少页面的回流和重绘
const fragment = this.node2Fragment(this.el);
// 2.编译模板
this.Compiler(fragment);
// 3.追加子元素到根元素
this.el.appendChild(fragment);
}
/*
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-test="msg"></div>
<div v-html="htmlStr"></div>
<input type="test" v-model="msg">
*/
Compiler(fragment) {
// 获取子节点
const childNodes = fragment.childNodes;
[...childNodes].forEach((child) => {
// console.log(child);
if (this.isElementNode(child)) {
// 是元素节点
// 编译元素节点
// console.log("元素节点", child);
this.compilerElement(child);
} else {
// 文本节点
// 编译文本节点
// console.log("文本节点", child);
this.compilerText(child);
}
if (child.childNodes && child.childNodes.length) {
this.Compiler(child);
}
});
}
compilerElement(node) {
const attributes = node.attributes;
[...attributes].forEach((attr) => {
const { name, value } = attr;
if (this.isDirective(name)) {
// 是一个指令 v-text v-html v-model v-on:click v-bind:src
const [, directive] = name.split("-"); // text html model on:click bind:src
const [dirName, eventName] = directive.split(":"); // text html model on bind
// 更新数据,数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName);
// 删除有指令的标签上的属性
node.removeAttribute("v-" + directive);
} else if (this.isEventName(name)) {
// @click="handleClick"
const [, eventName] = name.split("@");
compileUtil["on"](node, value, this.vm, eventName);
// 删除标签上的绑定的事件
node.removeAttribute("@" + eventName);
} else if (this.isAttrName(name)) {
// :src="imgUrl"
const [, attrName] = name.split(":");
compileUtil["bind"](node, value, this.vm, attrName);
// 删除标签上绑定的属性
node.removeAttribute(":" + attrName);
}
});
}
compilerText(node) {
// {{}} v-text
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil["text"](node, content, this.vm);
}
}
isAttrName(attrName) {
return attrName.startsWith(":");
}
isEventName(attrName) {
return attrName.startsWith("@");
}
isDirective(attrName) {
return attrName.startsWith("v-");
}
node2Fragment(el) {
// 创建文档碎片
const f = document.createDocumentFragment();
let firstChild;
while ((firstChild = el.firstChild)) {
f.appendChild(firstChild);
}
return f;
}
isElementNode(node) {
// 判断是否是元素节点对象
return node.nodeType === 1;
}
}
class MVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 1.实现一个数据观察者
new Observer(this.$data);
// 2.实现一个指令解析器
new Compiler(this.$el, this);
this.proxyData(this.$data);
}
}
proxyData(data) {
for (const key in data) {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newVal) {
data[key] = newVal;
}
});
}
}
}
HTML示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{person.td.msg}}</h3>
<div v-text="msg"></div>
<div v-text="person.fav"></div>
<div v-html="htmlStr"></div>
<input type="test" v-model="person.td.msg">
<button v-on:click="handleClick">点击我on</button>
<button @click="handleClick">点击我@</button>
<img v-bind:src="imgUrl" />
<img :src="imgUrl" />
</div>
<script src="./Observer.js"></script>
<script src="./MVue.js"></script>
<script>
let vm = new MVue({
el: '#app',
data: {
person: {
td: {
msg: '系统td'
},
name: '编程哥',
age: 18,
fav: '看电影'
},
msg: '学习MVVM实现原理',
htmlStr: '大家学习得怎么样',
imgUrl: './logo.png'
},
methods: {
handleClick () {
this.person.name = '学习222';
}
}
})
</script>
</body>
</html>
核心面试题讲解:阐述一下MVVM响应式原理
Vue是采用数据劫持配合发布订阅模式的方式,通过Object.defineProperty()
来劫持各个属性的getter和setter,在数据变动时,发布消息给依赖收集器(Dep),去通知观察者(Watcher)做出对应的回调函数,去更新视图。
MVVM作为绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听数据变化,通过Compile来解析编译模板指令,最终利用Watcher建立Observer、Compile之间的桥梁,达到数据变化=>视图更新;视图交互变化=>数据model变更的双向绑定效果