vue手写源码详解二
本章讲解如何根据指定的数据渲染控制区域
我们可以通过获取元素来判断有没有用v-model或者{{}},同时可以用指定的数据来替换,但这样做有一个弊端如数据发生变化那么页面就需要从新渲染,这样高频的渲染显然是我们不想看到的,网页卡顿的同时给用户也造成了很差的体验
那么有什么办法可以去规避这个问题那,其实可以通过把控制区域内的元素放入内存中,在内存中把所有的数据替换好,这样一次性即可渲染到网页上
那么我们需要的处理步骤分为三步
第一步:将网页上的元素放入内存中
如何把网页内的元素放入内存中那,使用DocumentFragment(文档碎片)这个方法可以完成,感兴趣可以百度看看
class Nue {
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);
console.log(fragment);
// 2.利用指定的数据编译内存中的元素
// 3.将编译好的内容重新渲染会网页上
}
node2fragment(app){
// 1.创建一个空的文档碎片对象
let fragment = document.createDocumentFragment();
// 2.编译循环取到每一个元素
let node = app.firstChild;
//当node元素为undefined就不会走到下发循环内
while (node){
// 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
fragment.appendChild(node);
//每次获取第二个依次类推,做了个循环
//借用变量提升重新赋值上面的node
node = app.firstChild;
}
// 3.返回存储了所有元素的文档碎片对象
return fragment;
}
}
这时利用数据已经在内存中处理完毕,那么我们怎么根据指定的数据渲染那
第二步:利用指定的数据编译内存中的元素
class Nue {
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.将编译好的内容重新渲染会网页上
}
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){
// 内存中的数据使用childNodes取出是类数组,需要三点语法解析
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){
// attributes 属性返回指定节点的属性集合
// 因为返回的是类数组不是数组,所以用三点语法将其变为真正的数组,方便我们去遍历
let attrs = [...node.attributes];
console.log('attrs',attrs)
// 遍历属性
attrs.forEach(attr => {
// name是key,value是值
let {name, value} = attr;
// 通过startsWith获取以v-开头的属性
if(name.startsWith('v-')){
console.log('是Vue的指令, 需要我们处理', name);
}
})
}
// 处理模板内容
buildText(node){
// 获取模板内的,内容
let content = node.textContent;
// 正则匹配{{}}花括号有特殊含义需要\去转义
let reg = /\{\{.+?\}\}/gi;
// 匹配内容
if(reg.test(content)){
console.log('是{{}}的文本, 需要我们处理', content);
}
}
}
第三步:将编译好的内容重新渲染回网页上
3.1替换指令中的数据
这一步将根据指令渲染到页面,此时我们为了处理复杂的逻辑,做了一个全局指令来协助完成编译
建议复制html内容和js运行理解下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>01-Vue基本模板</title>
<!--1.下载导入Vue.js-->
<!-- <script src="js/vue.js"></script>-->
<script src="js/nue.js"></script>
</head>
<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>
<ul>
<li>6</li>
<li>6</li>
<li>6</li>
</ul>
</div>
<script>
// 2.创建一个Vue的实例对象
// let vue = new Vue({
let vue = new Nue({
// 3.告诉Vue的实例对象, 将来需要控制界面上的哪个区域
el: '#app',
// el: document.querySelector('#app'),
// 4.告诉Vue的实例对象, 被控制区域的数据是什么
data: {
name: "name内容",
age: 33,
time: {
h: "我是对象内容1",
m: "我是对象内容2",
s: "我是对象内容3"
},
html: `<div>我是div</div>`,
text: `<div>我是div</div>`
}
});
</script>
</body>
</html>
// 处理指令工具类
let CompilerUtil = {
// 处理复杂数据例如对象,根据点语法截取循环
getValue(vm, value){
// time.h --> [time, h]
return value.split('.').reduce((data, currentKey) => {
// 第一次执行: data=$data, currentKey=time
// 第二次执行: data=time, currentKey=h
return data[currentKey];
}, vm.$data);
},
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;
}
}
class Nue {
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=>{
// 需要判断当前遍历到的节点是一个元素还是一个文本
// 如果是一个元素, 我们需要判断有没有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);
}
}
}
3.2编译模板数据
let CompilerUtil = {
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}} -> 李南江-33
let reg = /\{\{(.+?)\}\}/gi;
let val = value.replace(reg, (...args) => {
// 第一次执行 args[1] = name
// 第二次执行 args[1] = age
// console.log(args);
return this.getValue(vm, args[1]); // 李南江, 33
});
// 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 Nue {
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;
let reg = /\{\{.+?\}\}/gi;
if(reg.test(content)){
// 处理模板数据
CompilerUtil['content'](node, content, this.vm);
}
}
}