先来看下目录结构:
vue原理学习
├─ MVue.js
├─ index.html
└─ vue.js
1.index.html模板
这个是常见的模板哈,从这个常见的vue模板去思考🤔
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue原理实现</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="text" v-model='msg'>
<button v-on:click='handlerClick'>按钮on</button>
<button @click='handlerClick'>按钮@</button>
</div>
<script src="./MVue.js"></script>
<script>
let vm = new MVue({
el: '#app',
data: {
person: {
name: "吾乃常山赵子龙",
age: 18,
fav: '花姑娘'
},
msg: '学习MVVM实现原理',
htmlStr: '<h3>搞他搞他搞他</h3>'
},
methods: {
handlerClick() {
console.log(this.$data);
this.person.name = '学习MVVM';
// this.$data.person.name = '学习MVVM';
}
}
})
</script>
</body>
</html>
2.MVue.js
这篇主要是实现从 new MVVM() → Compile → 初始化视图相关部分内容。
首先是入口类Mvue,然后是编译类Compile,下面来看下部分的实现,Compile类中关于节点的具体编译还有待进一步实现。
Mvue入口类拿到options后将el和this传递给Compile编译类
//入口类MVue
class MVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
//实现一个指令解析器
new Compile(this.$el, this)
}
}
}
- 进入编译类
compile后先对el的节点类型进行判断,获取到对应的节点后赋值给this.el - 创建文档碎片
document.createDocumentFragment,将this.el中的节点内容放入其中,从而减少回流和重绘从文档中一个节点插入文档碎片后,这个节点会从原本的文档树中删除,所以将el循环插入文档碎片模型之后,就暂时不会显示到页面中。(下图为打印粗来的文档碎片内容)-
- blog.csdn.net/xzxlemontea…
- www.cnblogs.com/suihang/p/9…
- 将
文档碎片插入到this.el中后就可以看到页面中的内容,当然,这之前还需要对文档碎片进行编译。
//Compile编译类
class Compile {
constructor(el, vm) {
// 是否是元素节点
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
console.log(this.el);
//1.获取文档碎片对象 放入内存中,会减少页面的回流和重绘
const fragment = this.getFragment(this.el); //获取当前的文档碎片
console.log(fragment);
//2.编译模板
this.compile(fragment);
//3.插入到根节点
this.el.appendChild(fragment);
}
getFragment(el) {
const f = document.createDocumentFragment(); //这里创建文档碎片
// let firstChild;
while (el.firstChild) { //如果el有子节点,那么就给给这个节点添加到新创建的的文档碎片中去
f.appendChild(el.firstChild);
}
return f;
}
compile(fragment) {
const childNodes = fragment.childNodes;
[...childNodes].forEach(child => {
if (this.isElementNode(child)) {
console.log("元素节点", child)
this.compileElement(child);
} else {
// console.log("文本节点",child)
this.compileTxt(child);
}
//如果子节点还有子节点的处理方式--递归
if (child.childNodes && child.childNodes) {
this.compile(child);
}
})
}
//1.编译元素节点
compileElement(node) {
}
//2.编译其他类型节点
compileTxt(node) {
}
//监测是否是元素节点:nodeType等于1 那么就是元素节点
isElementNode(node) {
return node.nodeType === 1;
}
}
3.编译类的完整实现:
1.以编译元素节点函数compileElement为开始:
1.compileElement函数:
- 接收到
node节点,获取节点上的属性 - 通过
isDirective来判断是不是v-开头的指令 - 进行两次分割:
- 第
1次分割:分割出v-后面的部分(html,model,on) - 第
2次分割:分割出:后面的eventName事件名(click等),如果没有:则保留原来的(html,model,on)。
- 第
- 然后是根据
dirName来执行compileUtil中对应的处理方法
//...省略
compileElement(node){
// 例如:<div v-text='msg'></div>
const attrs = node.attributes;
[...attrs].forEach(attr=>{
const {name,value} = attr;
//属性名字是否是一个指令:类似于v-html,v-model,v-on:click
if(this.isDirective(name)){
//这里值取directive的值(html,model,on),not不要
const [not,directive] = name.split("-");
//这里还需要在分割,主要是针对类似v-on:click的
const [dirName,eventName] = directive.split(":");
//根据dirName执行对应的函数,传入this.vm主要是为了获取data上定义的值
compileUtil[dirName](node,value,this.vm,eventName)
}
})
}
//判断是否是“v-”开头的
isDirective(attrName){
return attrName.startsWith("v-")
}
2.compileUtil的实现:
1.text方法:
compileUtil对象中,封装了各种处理函数,这里先以text为例。
- 为了处理
v-text='person.name'这种情况,通过getValue方法获取attrValue在vm.$data中对应的值。- getValue方法中,先将
person.name通过.分割,然后通过reduce方法获取到person.name对应的value,返回回去。
- getValue方法中,先将
- 将获得的
value和以及之前的node传递给this.updater.textUpdater(node,value) textUpdater中通过node.textContent更新节点内容。html以及model方法的实现上面的基本类似,这里不做说明了。
const compileUtil = {
text(node,attrValue,vm){
// 这里需要更新值,以<div v-text='msg'></div>,首先得拿到msg在data中对应的值value;
// 但是当<div v-text='person.name'></div>这种形式的时候会失效,所以下面这种形式不可取
// const value = vm.$data[attrValue];
//进而需要一个getValue的方法,当调用的时候得到的就是最终的值
const value = this.getValue(attrValue,vm);
this.updater.textUpdater(node,value)
},
html(node,attrValue,vm){
const value = this.getValue(attrValue,vm);
this.updater.htmlUpdater(node,value);
},
model(node,attrValue,vm){
},
on(node,attrValue,vm,eventName){
},
// 更新函数对象
updater:{
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHtml = value;
},
modelUpdater(node, value) {
node.value = value;
}
},
// 获取<div v-text='person.name'></div>中person.name以及其他形式的值
// 这里将vm.$data传入,第一次的随后prevValue的值就是vm.$data
//然后return 回去的prevValue将作为新的prevValue,之道获取到person.name的值为止。
getValue(attrValue,vm){
return attrValue.split(".").reduce((prevValue,currValue)=>{
console.log("我是currentValue",currValue)
return prevValue[currValue]
},vm.$data)
}
}
3.删除以v-开头的指令属性:
v-开头的指令属性渲染到页面后需要删除掉。所以需要在compileElement函数中添加如下处理:
node.removeAttribute("v-"+directive);
//compileElement函数
compileElement(node){
// 例如:<div v-text='msg'></div>
const attrs = node.attributes;
[...attrs].forEach(attr=>{
const {name,value} = attr;
//属性名字是否是一个指令:类似于v-html,v-model,v-on:click
if(this.isDirective(name)){
//这里值取directive的值(html,model,on),not不要
const [not,directive] = name.split("-");
//这里还需要在分割,主要是针对类似v-on:click的
const [dirName,eventName] = directive.split(":");
//根据dirName执行对应的函数,传入this.vm主要是为了获取data上定义的值
compileUtil[dirName](node,value,this.vm,eventName);
//需要删除指令的属性
node.removeAttribute("v-"+directive);
}
})
}
删除后如下图:看到元素的属性v-被删除了
2.编译文本节点的函数compileText方法:
这里主要是针对<h2>{{person.name}} -- {{person.age}}</h2>如何赋值进行操作。
- 通过
/\{\{(.+?)\}\}/.test(value)正则表达式获取带有{{}}的内容,然后将这个value传递出去,调用的依然是compileUtil.text()的找个方法---这意味着compileUtil.text方法中又得加一层对于{{}}的处理
//compileText函数
compileText(node){
//主要是针对{{}}
const value = node.textContent;
if(/\{\{(.+?)\}\}/.test(value)){
compileUtil["text"](node,value,this.vm)
}
}
-
通过正则表达式获取到
value,然后传递给this.updater.textUpdater(node,value)进行渲染更新。attrValue.replace(/\{\{(.+?)\}\}/g,(...args)=>{ console.log("我是args",args) return this.getValue(args[1],vm) })
//compileUtil
const compileUtil = {
text(node,attrValue,vm){
let value ;
//这里需要对{{这种形式进行处理 <h2>{{person.name}} -- {{person.age}}</h2>
if(attrValue.indexOf('{{') !== -1)
{
value = attrValue.replace(/\{\{(.+?)\}\}/g,(...args)=>{
console.log("我是args",args)
return this.getValue(args[1],vm)
})
}else
{
value = this.getValue(attrValue,vm);
}
this.updater.textUpdater(node,value)
},
//...略
// 更新函数对象
updater:{
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
modelUpdater(node, value) {
node.value = value;
}
},
// 获取<div v-text='person.name'></div>中person.name以及其他形式的值
// 这里将vm.$data传入,第一次的随后prevValue的值就是vm.$data
//然后return 回去的prevValue将作为新的prevValue,之道获取到person.name的值为止。
getValue(attrValue,vm){
return attrValue.split(".").reduce((prevValue,currValue)=>{
console.log("我是currentValue",currValue)
return prevValue[currValue]
},vm.$data)
}
}
3.on方法的事件绑定处理函数:
- 通过
const handler = vm.$options.methods && vm.$options.methods[attrValue];找到方法 - 将找到的方法
handler绑定到node上,并将this指针方向归正
//on方法
on(node,attrValue,vm,eventName){
//获取到options中data的方法
const handler = vm.$options.methods && vm.$options.methods[attrValue];
node.addEventListener(eventName,handler.bind(vm),false);
},
4.关于“@”的处理方法:
- 通过
this.isEventName(name)来判断是否是以@开头的。 - 然后是一样的思路,
split以@符分割,取后面的作为eventName - 调用
compileUtil["on"](node,value,this.vm,eventName)
// compileElement方法
compileElement(node){
// 例如:<div v-text='msg'></div>
const attrs = node.attributes;
[...attrs].forEach(attr=>{
const {name,value} = attr;
//属性名字是否是一个指令:类似于v-html,v-model,v-on:click
if(this.isDirective(name)){
//这里值取directive的值(html,model,on),not不要
const [not,directive] = name.split("-");
//这里还需要在分割,主要是针对类似v-on:click的
const [dirName,eventName] = directive.split(":");
//根据dirName执行对应的函数,传入this.vm主要是为了获取data上定义的值
compileUtil[dirName](node,value,this.vm,eventName);
//需要删除指令的属性
node.removeAttribute("v-"+directive);
}else if(this.isEventName(name)){
let [not,eventName]= name.split('@');
compileUtil["on"](node,value,this.vm,eventName)
}
})
}