Vue由于其高效的性能和灵活入门简单、轻量的特点下变得火热。在当今前端越来越普遍的使用,今天来动手写一下Vue
主要实现
1.构建Vue实例查找指令和模板
2.数据驱动界面更新
3.界面驱动数据更新
4.事件相关指令
5.Vuex基本实现
6.Vue-Router基本实现
一、构建Vue实例
大家至此应该已经在项目中使用Vue,在此就不过多讲解Vue使用,直接从0开始尽量模仿Vue的语法,首先我们看一下简单的Vue案例
<body>
<div id="app">
<input type="text" v-model="name">
<p>{{ name }}</p>
<p>{{age}}</p>
<ul>
<li>6</li>
<li>6</li>
<li>6</li>
</ul>
</div>
<!--1.下载导入Vue.js-->
<script src="js/vue.js"></script>
<script>
// 2.创建一个Vue的实例对象
let vue = new Vue({
// 3.告诉Vue的实例对象, 将来需要控制界面上的哪个区域
el: '#app',
// 4.告诉Vue的实例对象, 被控制区域的数据是什么
data: {
name: "张三",
age : '18'
}
});
console.log(vue.$el);
console.log(vue.$data);
</script>
</body>
-
通过观察我们得知,要想使用Vue必须先创建Vue的实例, 创建Vue的实例通过new来创建, 所以说明Vue是一个类
所以我们要想使用自己的Vue,就必须定义一个名称叫做Vue的类 -
只要创建好了Vue的实例, Vue就会根据指定的区域和数据, 去编译渲染这个区域
所以我们需要在自己编写的Vue实例中拿到数据和控制区域, 去编译渲染这个区域
注意点: 创建Vue实例的时候指定的控制区域可以是一个ID名称, 也可以是一个Dom元素
注意点: Vue实例会将传递的控制区域和数据都绑定到创建出来的实例对象上
$el/$data
-
来到我们自己创建的js文件,简单命名为lue.js
Lue.js文件
class Lue {
constructor(options){
// 1.保存创建时候传递过来的数据
if(this.isElement(options.el)){
this.$el = options.el;
}else{
this.$el = document.querySelector(options.el);
}
this.$data = options.data;
// 2.根据指定的区域和数据去编译渲染界面
if(this.$el){
new Compiler(this)
}
}
// 判断是否是一个元素
isElement(node){
return node.nodeType === 1;
}
}
class Compiler {
constructor(vm){
this.vm = vm;
}
}
首先在第一步判断传递的数据是否是元素,如果是则直接赋值给this.$el
,不是则通过选择器查找一下再赋值。然后再把外面传递的data赋值给this.$data
。
接下来在根据指定数据,指定的区域,去编译渲染到界面中,专门定义一个Compiler类用于编译渲染。
接下来就可以使用了,只需要导入我们刚刚创建的Lue.js文件,将Vue替换Lue即可
let vue = new Lue({
el: '#app',
data: {
name: "张三",
age : '18'
}
});
console.log(vue.$el);
console.log(vue.$data);
二、提取元素到内存
在后面实现数据驱动界面时,数据发生改变,就替换元素。在js每次操作dom时都会对DOM进行一次重排,所谓重排也就是当元素的大小,位置结构发生变化的时候,就会引起浏览器对当前页面的结构进行一次重新的计算,这是非常耗费浏览器性能的。
虚拟DOM的出现很好的解决了这一问题,而js中的文档碎片就类似于虚拟DOM
可以使用文档碎片(Fragment),
类似于虚拟DOM的思想,在内存中先开辟出一块地方,对于所有节点的操作都在内存中先完成,完成后再一次更新到页面中,优化了浏览器的性能。
Compiler编译渲染的类
class Compiler {
constructor(vm){
this.vm = vm;
// 1.将网页上的元素放到内存中
let fragment = this.node2fragment(this.vm.$el);
console.log(fragment);
// 2.利用指定的数据编译内存中的元素
// 3.将编译好的内容重新渲染会网页上
}
node2fragment(app){
// 1.创建一个空的文档碎片对象
let fragment = document.createDocumentFragment();
// 2.编译循环取到每一个元素
let node = app.firstChild;
while (node){
// 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
fragment.appendChild(node);
node = app.firstChild;
}
// 3.返回存储了所有元素的文档碎片对象
return fragment;
}
}
三、编译指令和模板数据
将网页中的元素放到内存中后,接下就是利用指定的数据编译内存中的元素。
class Compiler {
constructor(vm){
this.vm = vm;
// 1.将网页上的元素放到内存中
let fragment = this.node2fragment(this.vm.$el);
// 2.利用指定的数据编译内存中的元素
this.buildTemplate(fragment);
// 3.将编译好的内容重新渲染会网页上
this.vm.$el.appendChild(fragment);
}
node2fragment(app){
// 1.创建一个空的文档碎片对象
let fragment = document.createDocumentFragment();
// 2.编译循环取到每一个元素
let node = app.firstChild;
while (node){
// 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
fragment.appendChild(node);
node = app.firstChild;
}
// 3.返回存储了所有元素的文档碎片对象
return fragment;
}
buildTemplate(fragment){
//为了实现遍历,通过解构赋值将伪数组转成数组
let nodeList = [...fragment.childNodes];
nodeList.forEach(node=>{
// 需要判断当前遍历到的节点是一个元素还是一个文本
// 如果是一个元素, 我们需要判断有没有v-model属性
// 如果是一个文本, 我们需要判断有没有{{}}的内容
if(this.vm.isElement(node)){
// 是一个元素
this.buildElement(node);
// 处理子元素(处理后代)
this.buildTemplate(node);
}else{
// 不是一个元素
this.buildText(node);
}
})
}
buildElement(node){
let attrs = [...node.attributes];
attrs.forEach(attr => {
let {name, value} = attr; // v-model="name" / name:v-model / value:name
if(name.startsWith('v-')){ // v-model / v-html / v-text / v-xxx
let [_, directive] = name.split('-'); // v-model -> [v, model]
CompilerUtil[directive](node, value, this.vm);
}
})
}
buildText(node){
let content = node.textContent;
let reg = /\{\{.+?\}\}/gi;
if(reg.test(content)){
console.log('是{{}}的文本, 需要我们处理', content);
}
}
}
buildTemplate方法中,通过fragment.childNodes可以拿到内存中所有的子节点,然后遍历判断是否时元素还是文本。
但是子节点中可能包含多个子节点,所以需要再次递归遍历下
// 处理子元素(处理后代) this.buildTemplate(node);
如果是元素则来到buildElement方法中。在遍历元素的属性,name为属性名,value为属性值。再判断是否包含v-,包含的话代表是vue指令,取出指令赋值给directive。
CompilerUtil[directive](node, value, this.vm);
,调用工具对象的方法给节点赋值。
接着看工具CompilerUtil的实现
let CompilerUtil = {
//根据属性名称获取值 vm:vue实例对象,value : 指令的值,(v-model = "name",值就是name)
getValue(vm, value){
// time.h --> [time, h] ,https://www.jianshu.com/p/e375ba1cfc47
return value.split('.').reduce((data, currentKey) => {
// 第一次执行: data=$data, currentKey=time
// 第二次执行: data=time, currentKey=h
return data[currentKey];
}, vm.$data);
},
/*
node : 当前节点
value : 指令的值,(v-model = "name",值name)
vm : lue实例对象
*/
model: function (node, value, vm) { // value=time.h
/*node.value = vm.$data[value]; // vm.$data[time.h] --> vm.$data[time] --> time[h]*/
let val = this.getValue(vm, value);
node.value = val;
},
html: function (node, value, vm) {
let val = this.getValue(vm, value);
node.innerHTML = val;
},
text: function (node, value, vm) {
let val = this.getValue(vm, value);
node.innerText = val;
}
}
在getValue(vm, value)方法中又有注意点,此时我们修改下html代码
<body>
<div id="app">
<input type="text" v-model="name">
<input type="text" v-model="time.h">
<input type="text" v-model="time.m">
<input type="text" v-model="time.s">
<div v-html="html">abc</div>
<div v-text="text">123</div>
<p>{{ name }}</p>
<p>{{age}}</p>
<p>{{time.h}}</p>
<p>{{name}}-{{age}}</p>
<ul>
<li>6</li>
<li>6</li>
<li>6</li>
</ul>
</div>
<script>
// 2.创建一个Vue的实例对象
// let vue = new Vue({
let vue = new Lue({
// 3.告诉Vue的实例对象, 将来需要控制界面上的哪个区域
el: '#app',
// el: document.querySelector('#app'),
// 4.告诉Vue的实例对象, 被控制区域的数据是什么
data: {
name: "张三",
age: 18,
time: {
h: 11,
m: 12,
s: 13
},
html: `<div>我是div</div>`,
text: `<div>我是div</div>`
}
});
</script>
</body>
在data中可能有多层数据,也就是对象类似的。
<input type="text" v-model="time.h">
<input type="text" v-model="time.m">
<input type="text" v-model="time.s">
time: {
h: 11,
m: 12,
s: 13
},
所以在CompilerUtil的model方法中仅仅使用 node.value = vm.$data[value]
不行的,所以抽取一个getValue方法,通过reduce遍历,取出data.time.h的值
//根据属性名称获取值 vm:vue实例对象,value : 指令的值,(v-model = "name",值就是name)
getValue(vm, value){
// time.h --> [time, h] ,https://www.jianshu.com/p/e375ba1cfc47
return value.split('.').reduce((data, currentKey) => {
// 第一次执行: data=$data, currentKey=time
// 第二次执行: data=time, currentKey=h
return data[currentKey];
}, vm.$data);
},
元素中带有v-model指令就能够实现绑定数据了。接下来在看插值语法绑定模板数据。
buildText(node){
let content = node.textContent;
//看看是否匹配{{name}}(插值语法)
let reg = /\{\{.+?\}\}/gi;
if(reg.test(content)){
CompilerUtil['content'](node, content, this.vm);
}
}
通过正则表达式匹配{{}},匹配到后再调用CompilerUtil工具对象的content方法。
content: function (node, value, vm) {
// console.log(value); // {{ name }} -> name -> $data[name]
let val = this.getContent(vm, value);
node.textContent = val;
}
getContent(vm, value){
// console.log(value); // {{name}}-{{age}} ->张三-{{age}} -> 张三-18
// (.+?) 取出值
let reg = /\{\{(.+?)\}\}/gi;
let val = value.replace(reg, (...args) => {
// 第一次执行 args[1] = name
// 第二次执行 args[1] = age
// console.log(args);
return this.getValue(vm, args[1]); // 张三, 18
});
// console.log(val);
return val;
},
注意点:可能数据是{{name}--{{age}},那么则用replace方法依次查找替换。
附上本章节lue.js代码
let CompilerUtil = {
/*
node : 当前接单
value : 指令的值,(v-model = "name",值name)
vm : Lue实例对象
*/
//根据属性名称获取值 vm:vue实例对象,value : 指令的值,(v-model = "name",值就是name)
getValue(vm, value){
// time.h --> [time, h]
return value.split('.').reduce((data, currentKey) => {
// 第一次执行: data=$data, currentKey=time
// 第二次执行: data=time, currentKey=h
return data[currentKey.trim()];
}, vm.$data);
},
getContent(vm, value){
// console.log(value); // {{name}}-{{age}} -> 张三-{{age}} -> 张三-18
// (.+?) 取出值
let reg = /\{\{(.+?)\}\}/gi;
let val = value.replace(reg, (...args) => {
// 第一次执行 args[1] = name
// 第二次执行 args[1] = age
// console.log(args);
return this.getValue(vm, args[1]); // 张三, 18
});
// console.log(val);
return val;
},
model: function (node, value, vm) {
let val = this.getValue(vm, value);
node.value = val;
},
html: function (node, value, vm) {
let val = this.getValue(vm, value);
node.innerHTML = val;
},
text: function (node, value, vm) {
let val = this.getValue(vm, value);
node.innerText = val;
},
content: function (node, value, vm) {
// console.log(value); // {{ name }} -> name -> $data[name]
let val = this.getContent(vm, value);
node.textContent = val;
}
}
class Lue {
constructor(options){
// 1.保存创建时候传递过来的数据
if(this.isElement(options.el)){
this.$el = options.el;
}else{
this.$el = document.querySelector(options.el);
}
this.$data = options.data;
// 2.根据指定的区域和数据去编译渲染界面
if(this.$el){
new Compiler(this);
}
}
// 判断是否是一个元素
isElement(node){
return node.nodeType === 1;
}
}
class Compiler {
constructor(vm){
this.vm = vm;
// 1.将网页上的元素放到内存中
let fragment = this.node2fragment(this.vm.$el);
// 2.利用指定的数据编译内存中的元素
this.buildTemplate(fragment);
// 3.将编译好的内容重新渲染会网页上
this.vm.$el.appendChild(fragment);
}
node2fragment(app){
// 1.创建一个空的文档碎片对象
let fragment = document.createDocumentFragment();
// 2.编译循环取到每一个元素
let node = app.firstChild;
while (node){
// 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
fragment.appendChild(node);
node = app.firstChild;
}
// 3.返回存储了所有元素的文档碎片对象
return fragment;
}
buildTemplate(fragment){
//为了实现遍历,通过结构赋值将伪数组变为数组
let nodeList = [...fragment.childNodes];
nodeList.forEach(node=>{
// 需要判断当前遍历到的节点是一个元素还是一个文本
if(this.vm.isElement(node)){
// 是一个元素
this.buildElement(node);
// 递归,处理子元素(处理后代)
this.buildTemplate(node);
}else{
// 不是一个元素
this.buildText(node);
}
})
}
buildElement(node){
let attrs = [...node.attributes];
attrs.forEach(attr => {
let {name, value} = attr; // v-model="name" / name:v-model / value:name
if(name.startsWith('v-')){ // v-model / v-html / v-text / v-xxx
let [_, directive] = name.split('-'); // v-model -> [v, model]
CompilerUtil[directive](node, value, this.vm);
}
})
}
buildText(node){
let content = node.textContent;
//看看是否匹配{{name}}(插值语法),'.+?' :至少一位数,'g':全局匹配,'i':是忽略大小写
let reg = /\{\{.+?\}\}/gi;
if(reg.test(content)){
CompilerUtil['content'](node, content, this.vm);
}
}
}
本章节就暂时结束,下一章讲解利用 Object.defineProperty,和发布订阅者模式实现数据驱动界面更新
文章主要代码参考李南江web前端课程
其他参考链接:
https://www.cnblogs.com/suihang/p/9491359.html
https://segmentfault.com/a/1190000016434836
网友评论