v-if & v-show原理解析及最简模型实现
前期准备
视图层准备了四个盒子,相同宽高,并且给他们各自不同的颜色以区分,并且全部设置为左浮动,这样能观察到v-if
显隐的排序问题。
<!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>Document</title>
<style>
.box1 {
float: left;
width: 100px;
height: 120px;
background-color: red;
}
.box2 {
float: left;
width: 100px;
height: 120px;
background-color: green;
}
.box3 {
float: left;
width: 100px;
height: 120px;
background-color: blue;
}
.box4 {
float: left;
width: 100px;
height: 120px;
background-color: gray;
}
.box-container {
overflow: hidden;
}
</style>
</head>
<body>
<div class="wrapper">
<div class="box-container">
<div class="box1" v-if="boxView1">box1</div>
<div class="box2" v-show="boxView2">box2</div>
<div class="box3" v-show="boxView3">box3</div>
<div class="box4" v-if="boxView4">box4</div>
</div>
<div class="control">
<button @click="changeView1">change view 1</button>
<button @click="changeView2">change view 2</button>
<button @click="changeView3">change view 3</button>
<button @click="changeView4">change view 4</button>
</div>
</div>
<script src="./index.js"></script>
<script>
const app = new HyVue({
el: '.wrapper',
data: {
boxView1: false,
boxView2: false,
boxView3: false,
boxView4: false,
},
methods: {
changeView1() {
this.boxView1 = !this.boxView1;
console.log('changeView1');
},
changeView2() {
this.boxView2 = !this.boxView2;
console.log('changeView2');
},
changeView3() {
this.boxView3 = !this.boxView3;
console.log('changeView3');
},
changeView4() {
this.boxView4 = !this.boxView4;
console.log('changeView4');
},
},
});
</script>
</body>
</html>
js部分
class初始化
js部分定义一个类,在使用new调用该类进行初始化,将传入的options解构出挂载元素el
,数据层data
,事件层methods
class HyVue {
constructor(options) {
const { el, data, methods } = options;
this.el = document.querySelector(el);
this.data = data;
this.methods = methods;
}
}
创建视图和数据的关联
视图要进行变化,必须拿到相关的元素做处理,而且元素和数据肯定要关联起来,这样在找到元素时也能找到数据,同时找到数据也能找到元素。此时可使用Map来做映射处理,所以我们定义一个showPool为视图池,eventPool为事件池。将data和对应的dom元素关联并添加到showPool;将event和对应的dom元素关联并添加到eventPool。
class HyVue {
constructor(options) {
const { el, data, methods } = options;
this.el = document.querySelector(el);
this.data = data;
this.methods = methods;
this.showPool = new Map();
this.eventPool = new Map();
}
初始化数据(将data中的数据代理到实例的this)
这个步骤将用defineProperty将数据代理到this,这样在绑定的方法中用this访问时可以直接访问到data而不用再进到data对象去访问。在vue2中我们可以使用this直接访问到data的数据也是这个原理。
class HyVue {
constructor(options) {
const { el, data, methods } = options;
this.el = document.querySelector(el);
this.data = data;
this.methods = methods;
this.showPool = new Map();
this.eventPool = new Map();
this.init();
}
init() {
//初始化数据,将data中的数据代理到this中
this.initData();
}
initData() {
const dataKey = Object.keys(this.data);
for (const key of dataKey) {
Object.defineProperty(this, key, {
get: function () {
return this.data[key];
},
set: function (newValue) {
this.data[key] = newValue;
},
});
}
}
初始化dom
这一步为关键步骤,我们需要在视图池和事件池中添加关联,以在后续可以找到某个绑定的dom元素所对应的data或方法。
class HyVue {
constructor(options) {
const { el, data, methods } = options;
this.el = document.querySelector(el);
this.data = data;
this.methods = methods;
this.showPool = new Map();
this.eventPool = new Map();
this.init();
}
init() {
//初始化数据,将data中的数据代理到this中
this.initData();
//初始化dom 往视图池和事件池中添加关联
this.initDom();
//初始化视图,根据视图池中的关联,决定元素的显隐,
this.initView(this.showPool);
//初始化方法,为事件池中关联元素绑定事件
this.initMethods();
}
initData() {
// ...略
}
initDom() {
const traverseNode = (node) => {
if (!node.childNodes.length) return;
const nodes = node.childNodes;
for (const dom of nodes) {
//此时nodeType===1为元素节点
if (dom.nodeType === 1) {
//找到有以下属性的dom元素
const v_if = dom.getAttribute('v-if');
const v_show = dom.getAttribute('v-show');
const v_event = dom.getAttribute('@click');
if (v_if) {
//创建v_if关联
this.showPool.set(dom, {
type: 'v-if',
show: this[v_if],
dataName: v_if,
});
} else if (v_show) {
//创建v_show关联
this.showPool.set(dom, {
type: 'v-show',
show: this[v_show],
dataName: v_show,
});
}
if (v_event) {
//创建事件关联
this.eventPool.set(dom, {
method: this.methods[v_event],
dataName: v_event,
event: 'click',
});
}
//递归查询
traverseNode(dom);
}
}
};
traverseNode(this.el);
}
初始化视图
此时视图层和数据层的关联已经创建完了,下一步是不是应该将视图和数据联系起来了?如果dom元素属性为v-if,那么其绑定的data为false时,将不对其进行渲染,重点来了我们应该如何隐藏该元素? 第一种办法直截了当,把该元素删掉,后续显示的时候再创建。哈哈,很明显这种办法行不通,因为你不知道创建该元素后应该放在那个位置,appendChild?那么创建的元素会在所有子元素的尾部,这样dom结构就乱了;insertChild?此时的位置是未知的,如果要知道在哪个元素的前面,这样需要重复做复杂的记录操作。所以有一个解决办法,vue也是使用了这个办法,使用注释来替换该元素,然后记录该注释,之后如果要显示该元素,那么就用该dom节点来替换注释。
class HyVue {
constructor(options) {
const { el, data, methods } = options;
this.el = document.querySelector(el);
this.data = data;
this.methods = methods;
this.showPool = new Map();
this.eventPool = new Map();
this.init();
}
init() {
//初始化数据,将data中的数据代理到this中
this.initData();
//初始化dom 往视图池和事件池中添加关联
this.initDom();
//初始化视图,根据视图池中的关联,决定元素的显隐,
this.initView(this.showPool);
//初始化方法,为事件池中关联元素绑定事件
this.initMethods();
}
initData() {
// ... 略
}
initDom() {
// ... 略
}
initView(data, showPool) {
//同时负责视图层的更新
this.domChange(null, this.showPool);
}
domChange(data, showPool) {
if (!data) {
//初始化
showPool.forEach((view, dom) => {
switch (view.type) {
case 'v-if':
view.comment = document.createComment(view.dataName || 'unknown');
view.show ? '' : dom.parentNode.replaceChild(view.comment, dom);
break;
case 'v-show':
view.show
? (dom.style.display = 'block')
: (dom.style.display = 'none');
break;
}
});
}
}
初始化方法
这个没什么好说的,把事件池eventPool遍历一遍,使用dom.addEventListener绑定事件,但是有一点要注意,此时所绑定的事件的this是指向该元素的,所以需要用Func.bind(this)
显式绑定this到实例,这样才能访问实例里的data。
class HyVue {
constructor(options) {
const { el, data, methods } = options;
this.el = document.querySelector(el);
this.data = data;
this.methods = methods;
this.showPool = new Map();
this.eventPool = new Map();
this.init();
}
init() {
//初始化数据,将data中的数据代理到this中
this.initData();
//初始化dom 往视图池和事件池中添加关联
this.initDom();
//初始化视图,根据视图池中的关联,决定元素的显隐,
this.initView(this.showPool);
//初始化方法,为事件池中关联元素绑定事件
this.initMethods();
}
initData() {
// ... 略
}
initDom() {
// ... 略
}
initView(data, showPool) {
// ... 略
}
domChange(showPool) {
// ... 略
}
initMethods() {
this.eventPool.forEach((data, dom) => {
dom.addEventListener(data.event, data.method.bind(this));
});
}
}
执行方法,触发视图变化
执行方法后,我们需要知道此时哪个数据变化了,而且要把变化的值给到对应的dom处理逻辑去处理。此时defineProperty的set方法就派上用场了,set方法可以监听到数据变化,我们可以在此方法中去触发视图的更新。又因为初始化视图和更新视图的逻辑差不多,就在domChange方法中处理了,通过data来判断此时是初始化还是更新操作。遍历视图池,找到该变量key值相等的dom元素进行处理。
完整代码:
javascript
class HyVue {
constructor(options) {
const { el, data, methods } = options;
this.el = document.querySelector(el);
this.data = data;
this.methods = methods;
this.showPool = new Map();
this.eventPool = new Map();
this.init();
}
init() {
//初始化数据,将data中的数据代理到this中
this.initData();
//初始化dom 往视图池和事件池中添加关联
this.initDom();
//初始化视图,根据视图池中的关联,决定元素的显隐,
this.initView(this.showPool);
//初始化方法,为事件池中关联元素绑定事件
this.initMethods();
}
initData() {
const dataKey = Object.keys(this.data);
for (const key of dataKey) {
Object.defineProperty(this, key, {
get: function () {
return this.data[key];
},
set: function (newValue) {
this.data[key] = newValue;
this.domChange(
{
key,
newValue,
},
this.showPool
);
},
});
}
}
initDom() {
const traverseNode = (node) => {
if (!node.childNodes.length) return;
const nodes = node.childNodes;
for (const dom of nodes) {
if (dom.nodeType === 1) {
const v_if = dom.getAttribute('v-if');
const v_show = dom.getAttribute('v-show');
const v_event = dom.getAttribute('@click');
if (v_if) {
this.showPool.set(dom, {
type: 'v-if',
show: this[v_if],
dataName: v_if,
});
} else if (v_show) {
this.showPool.set(dom, {
type: 'v-show',
show: this[v_show],
dataName: v_show,
});
}
if (v_event) {
this.eventPool.set(dom, {
method: this.methods[v_event],
dataName: v_event,
event: 'click',
});
}
traverseNode(dom);
}
}
};
traverseNode(this.el);
}
initView(data, showPool) {
//同时负责视图层的更新
this.domChange(null, this.showPool);
}
domChange(data, showPool) {
if (!data) {
showPool.forEach((view, dom) => {
switch (view.type) {
case 'v-if':
view.comment = document.createComment(view.dataName || 'unknown');
view.show ? '' : dom.parentNode.replaceChild(view.comment, dom);
break;
case 'v-show':
view.show
? (dom.style.display = 'block')
: (dom.style.display = 'none');
break;
}
});
} else {
showPool.forEach((view, dom) => {
if (view.dataName === data.key) {
switch (view.type) {
case 'v-if':
data.newValue
? view.comment.parentNode.replaceChild(dom, view.comment)
: dom.parentNode.replaceChild(view.comment, dom);
break;
case 'v-show':
data.newValue
? (dom.style.display = 'block')
: (dom.style.display = 'none');
break;
}
view.show = data.newValue;
}
});
}
}
initMethods() {
this.eventPool.forEach((data, dom) => {
dom.addEventListener(data.event, data.method.bind(this));
});
}
}