在 手写手写v-if 和 v-show(1) - 掘金 (juejin.cn)-创建代码结构中,整体的代码结构,以及我们要做的事情以及表述完了. 接下来就是一一实现每一个方法了.
一、实现响应式数据
观察:
- 在
data
返回函数的数据中, 我们可以在别的地方直接使用this
来进行调用 - 如果变量是基本数据类型, 变量一旦发生了变化,视图就发生了变化
会有如下代码
Vue.prototype._init = function (vm, template, methods) {
initData(vm);
};
function initData(vm) {
var _data = vm.$data;
for (var key in _data) {
(function (key) {
Object.definedPrototype(vm, key, {
get: function () {
return _data[key];
},
set: function (newVal) {
_data[key] = newVal;
},
});
})(key);
}
}
如此,把data
中返回的数据就可以直接通过 vue 的实例来直接访问到了. 这样实现了第一点.至于第二点, 就需要在set
的方法的时候,调用了视图更新的逻辑.
set: function(newVal) {
_data[key] = newVal
update(vm, key)
}
update
方法需要当前vue
的实例, 以及当前修改的变量.
二、template 模版数据处理
在template
中存在如下数据类型:
- 需要转化为真实 DOM 的节点数据
- v-if 和 v-show 和对应变量的
attribute
参数 @click
和对应方法的 的attribute
参数
三个不同类型的数据都是我们需要处理的. 由于第 2 点和第 3 点需要用在别的地方,所以我们需要想办法将他们给存储起来.
为了方便存储数据,我们使用可以以对象为键名的Map
来进行存储
var showPool = new Map();
var eventPool = new Map();
function initPool(vm, container, methods) {
var _allNodes = container.getElementsByTagName("*");
for (var i = 0; i < _allNodes.length; i++) {
var node = _allNodes[i];
var vIfData = node.getAttribute("v-if");
var vShowData = node.getAttribute("v-show");
var eventData = node.getAttribute("@click");
if (vIfData) {
showPool.set(node, {
type: "if",
prop: vIfData,
});
} else if (vShowData) {
showPool.set(node, {
tyep: "show",
prop: vShowData,
});
}
if (eventData) {
eventPool.set(node, methods[eventData]);
}
}
}
上面的
eventData
没有使用else if
是因为同一个节点上面是有可能同时存在v-if/v-show
和@click
同时存在的
自此,我们需要进一步处理的数据都存储起来. 但是还有一个问题,我处理从template
中拿到了想要的数据之后, 然后一个问题需要处理.这些attribute
里面的对应的变量以及拿到了. 这个时候就需要我们把这些对于 html 解析无效的东西.
在循环中,进入判断之后,就将其删除掉
node.removeAttribute("v-if");
node.removeAttribute("v-show");
node.removeAttribute("@click");
三、事件绑定
vue 中的事件处理一样是不需要使用this.methods.clickShow1()
这样的方式,直接通过调用 vue 的实例化对象就可以调用.
function bindEvent(vm, eventPool) {
for (var [dom, handle] of eventPool) {
vm[handle] = handle;
dom.addEventListenner("click", vm[handle].bind(vm), false);
}
}
其中bind(vm)
为了确保在 vue 中能够指向 vue 实例.
四、界面渲染
需要的各种数据已经齐备了. 这个事件就需要使用这些数据来渲染界面了.
function render(vm, container, showPool ) {
var _data = vm.$date
var _el = vm.$el
for (var [dom, info] of showPool) {
switch info.type {
case 'if':
info.comment = document.createComment(["v-if"])
!_data[info.prop] && dom.parentNode.replaceChild(info.comment, dom)
break
case 'show':
!_data[info.prop] && dom.style.display = 'none'
break
default:
break
}
}
_el.append(container)
}
此时看界面,不出意外,应该已经渲染完毕了.
使用
_data[info.prop]
来进行判断,而不是直接使用vm[info.prop]
. 就是为了让变量不污染全局. 区别于在实例中直接this.xxx
来注册的变量
五、更新数据
响应式的数据变化了之后,需要重新渲染对应的界面.通常这个发生在initData
方法的set
中.
// initData()
Object.definedPrototype(vm, key, {
get: function () {
return _data[key];
},
set: function (newVal) {
_data[key] = newVal;
update(vm, key, showPool);
},
});
function update(vm, key, showPool) {
const _data = vm.$data; // 这样写是为了,让你明白这变量是在data中定义的,还是直接绑定到vue实例上面的
for (var [dom, info] of showPool) {
if (info.prop === key) {
switch (info.type) {
case "if":
!_data[key]
? dom.parentNode.replaceChild(info.comment, dom)
: info.comment.parentNode.replaceChild(dom, info.comment);
break;
case "show":
let displayValue = _data[info.prop] ? "inline" : "none";
dom.style.display = displayValue;
break;
default:
break;
}
}
}
}
值得注意的是,在if的判断中, 当我们使用了注释标签对节点进行替换之后,就不存在原来的dom节点了.但是info.comment
一直都没有被删除,所以是直接将info.comment
替换成存储的dom节点.
涉及到的知识点
Node.parentNode()
Node.replaceChild()
Object.definedPrototype
Map
集合的使用