全手打原创,转载请标明出处:https://www.cnblogs.com/dreamsqin/p/15175740.html, 多谢,=。=~(如果对你有帮助的话请帮我点个赞啦)
作为一个Web前端开发人员,使用Vue框架进行项目开发已经有一阵子,掐指一算,是时候认真探索一下Vue的底层了,以前的了解比较偏理论,这一次打算在弄清基本原理的前提下自己手写Vue中的核心部分,也许这样我才敢说自己“深入理解”了Vue。上篇完成了编译器的第一项目标:插值文本编译和依赖收集,编译后
template
中的插值变量会替换为实际值展示,当用户修改data
属性中的值时视图也会同步更新,接下来是另外一块核心部分,也就是分流处理的另一分支,节点编译,本篇涉及节点属性遍历、指令解析和事件处理~
先上一张我们在《Vue底层学习4——编译器框架搭建》中抛出的编译器流程图,本篇会严格按照这个流程进行编译器的功能编码,=。=我确实比较喜欢看图说话:
属性遍历
之前在compile
的编译函数中,对Dom子节点遍历时如果子节点类型是Element
,我们预留了一个编译节点的分支,接下来的重点就是在该分支中进行节点属性的遍历,找到v-
、@
开头的属性并实现各自的处理流程。
/*** compile.js ***/
// new Compile(el, vm)
class Compile{
constructor(el, vm) {...}
// 提取指定Dom节点中的代码片段
node2Fragment(el) {...}
// 编译过程
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
// 类型判断
if (this.isElement(node)) {
// 节点属性遍历
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach(attr => {
// 属性名
const attrName = attr.name;
// 属性值
const exp = attr.value;
if(this.isDirective(attrName)) {
// 如果是v-开头的指令
}
if (this.isEvent(attrName)) {
// 如果是@开头的事件
}
})
} else if(this.isInterpolation(node)) {
// 编译插值文本
this.compileText(node);
}
// 递归子节点
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
})
}
// 是否是节点
isElement(node) {...}
// 是否是插值文本
isInterpolation(node) {...}
// 更新函数
update(node, vm, exp, dir) {...}
// 插值文本更新
textUpdater(node, value) {...}
// 插值文本编译
compileText(node) {...}
}
通过isDirective
和isEvent
方法分别判断是否是v-
、@
开头的属性:
// 是否是指令
isDirective(name) {
return name.indexOf('v-') === 0;
}
// 是否是事件
isEvent(name) {
return name.indexOf('@') === 0;
}
指令解析
对于v-
开头的指令我们根据指令名称标识抽象出需要调用的解析方法:
if(this.isDirective(attrName)) {
// 如果是v-开头的指令
const dir = attrName.substring(2);
this[dir] && this[dir](node, this.$vm, exp);
}
针对不同类型的指令调用不同的方法,实现对应的功能:
v-text
text(node, vm, exp) {
this.update(node, vm, exp, 'text');
}
// 与插值文本更新调用的是同一个函数
textUpdater(node, value) {
node.textContent = value;
}
运行结果如下,可以看到通过v-text
指令实现了name
插值,视图中新增了一行name
的展示,与此同时,created
中对name
的修改也同样生效:
v-html
html(node, vm, exp) {
this.update(node, vm, exp, 'html');
}
htmlUpdater(node, value) {
node.innerHTML = value;
}
运行结果如下,可以看到html
属性的button
被插入到了v-html
所在到Dom节点中:
v-model
该指令是Vue双向绑定的关键体现,可以理解为它是:value
和@input
的结合体:
// 指令v-model,双向绑定
model(node, vm, exp) {
// 指定input的value属性(值对视图的影响——MV)
this.update(node, vm, exp, 'model');
// input事件监听并修改数据模型中的值(视图对值对影响——VM)
node.addEventListener('input', e => {
vm[exp] = e.target.value;
})
}
modelUpdater(node, value) {
node.value = value;
}
运行结果如下,name
属性的值会以input
当前的value
值进行展示,与此同时,input
中value
的修改都会同步到一切使用name
属性的视图中,这就实现了双向绑定:
事件处理
对于@
开头的事件我们根据事件名称抽象出需要调用的处理方法:
if (this.isEvent(attrName)) {
// 如果是@开头的事件
const dir = attrName.substring(1);
this.eventHandler(node, this.$vm, exp, dir);
}
具体的事件处理放到eventHandler
中实现,实际就是给指定的节点绑定对应的事件监听,并且将回调函数的this
指向当前的Vue实例,原因跟之前一样,这样就可以在回调函数中通过this
轻松的获取到挂载到Vue实例上的属性:
// 事件处理
eventHandler(node, vm, exp, dir) {
// 事件触发后需要执行的函数
const fn = vm.$options.methods && vm.$options.methods[exp];
if(dir && fn) {
node.addEventListener(dir, fn.bind(vm));
}
}
运行结果如下,点击“改名儿”按钮时,name
和location
的值被修改,视图也同步更新:
总结
手撸Vue核心代码就这样悄悄的结束了,心情舒畅,好像打通了任督二脉~最后做个小小的总结,整体下来我觉得Vue核心代码中最重要的部分就是编译,编译可以实现依赖收集,这样可以建立视图和数据模型之间的联系,当数据模型变更时,可以通知依赖对应数据的视图进行更新,也就是我们常说的数据驱动视图。而Vue中著名的双向绑定其实是通过v-model
指令实现,在编译时解析该指令,为所属节点添加事件监听,事件触发时就可以即时修改绑定的值,绑定值的修改又会触发所有依赖的视图更新。
参考资料
1、Vue源码:https://github.com/vuejs/vue;