vue双向绑定 - 模板的编译
1、vue实现双向绑定的必要方法
- 模板的编译。即:
{{}}
的取值编译,指令的编译 - 数据劫持,观察数据变化。即:表单值改变,需要更新model数据
- Watcher,利用发布订阅末模式联系以上两者
2、构建环境
- 先将上篇文章中的#app内容修改如下
<div id="app"> <input type="text" v-model="message"> <div>这是一个div{{message}}</div> <ul><li></li></ul> {{message}} </div>
- 新建mvvm.js(创建一个MVVM类),compile.js(实现编译过程)并引入至html,删除html中引入的vue依赖
3、完善mvvm.js内容
vue需要先实例化后才能使用,如Vue一样,那么mvvm里面也应该设定是一个类;参数为一个对象,包含el、data、created等。
注:constructor用来接收实例化时传递的参数
class MVVM{
constructor(options){
//传过来的数据都需要挂载在实例上,保证全局能调用
this.$el = options.el;
this.$data = options.data;
//如果有要编译的模板,即开始编译
if(this.$el){
new Compile(this.$el, this);
}
}
}
4、完善compile.js内容
Compile充当一个解析器,来做解析模板和绑定数据的工作,解析过程分为三步:
- 先把真实的DOM移入到内存中。利用
fragment
文档碎片,在内存中执行 - 提取想要的元素节点
v-model
和文本节点{{}}
- 把编译好的fragment塞回页面中
注:因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将根节点 el
转换成文档碎片 fragment
(内存中的DOM节点) 进行解析编译操作,解析完成,再将 fragment
添加回原来的真实dom节点中。
class Compile{
constructor(el, vm){
//el有两种类型:1、#app 2、DOM元素
this.el = this.isELementNode(el) ? el : document.querySelector(el);
//vm为实例化后的上下文环境this
this.vm = vm;
if(this.el){
let fragment = this.node2fragment(this.el);
this.compile(fragment);
this.el.appendChild(fragment);
}
}
//将el(#app)内的元素全部放到内存中,在内存中操作元素会优化性能
node2fragment(el){
let fragment = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild){
fragment.appendChild(firstChild);
}
return fragment;
}
/* 一些辅助的方法 */
//判断是否元素节点
isELementNode(node){
return node.nodeType == 1;
}
//判断是否指令
isDirective(name){
return name.includes('v-');
}
/* 一些核心的方法 */
compile(fragment){
//解析模板和数据绑定
}
}
完善compile方法:
注:fragment.childNodes只是拿到fragment下的 一级根节点
,需要 递归
找到所有的节点执行编译过程,如 <ul><li></li></ul>
,只能拿到 ul
;而创建文档碎片 fragment
的时候,只需要拿到 #app
下面的 一级根节点
,就可将根节点及下面的子节点一起append到 fragment
上
compile(fragment){
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach(node=>{
if(this.isELementNode(node)){
//编译元素
this.compileELement(node);
this.compile(node);
}else{
//编译文本
this.compileText(node);
}
})
}
4.1、编译元素
- v-model、v-text、v-html 这类vue指令的编译;
- 如果元素内不含指令,则忽略;
- 需要从实例化时传递的data数据里面拿到相应的数据后回填至表单。
注:attrs是 类数组对象
,例如input标签的attrs为 NamedNodeMap {0: type, 1: v-model, type: type, v-model: v-model, length: 2}
可用es6方法 Array.from
转化为数组后执行遍历;
compileELement(node){
let attrs = node.attributes;
Array.from(attrs).forEach(attr => {
let attrName = attr.name;
//判断属性名字是否包含 v-
if(this.isDirective(attrName)){
let expr = attr.value;
let [,type] = attrName.split('-');
CompileUtil[type](node, this.vm, expr);
}
})
}
CompileUtil为公有方法,需要考虑获取数据,文本赋值,输入框赋值等,见下方公共函数编写
4.2、编译文本
即:编辑{{}}
里面的东西
compileText(node){
let expr = node.textContent; //取文本中的内容
let reg = /\{\{([^}]+)\}\}/g;
if(reg.test(expr)){
CompileUtil['text'](node, this.vm, expr);
}
}
4.3、公共函数编写
reduce,为es5的方法,也是数组遍历的一种
注:text函数为将expr属性对应的数据更新至node节点中,node应该是带有 {{}}
的一串文本,故而需要getTextVal来替换 {{message}}
CompileUtil = {
getVal(vm, expr){ //取值,例如message.value,需要依次取出
expr = expr.split('.');
return expr.reduce((prev, next)=>{
return prev[next];
},vm.$data)
},
getTextVal(vm, expr){ //获取编译文本后的结果
return expr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{
return this.getVal(vm, arguments[1]);
});
},
text(node, vm, expr){
let updateFn = this.updater['textUpdater'];
let value = this.getTextVal(vm, expr)
updateFn && updateFn(node, value)
},
model(node, vm, expr){
let updateFn = this.updater['modelUpdater'];
updateFn && updateFn(node, this.getVal(vm, expr))
},
updater: {
//文本更新
textUpdater(node, value){
node.textContent = value;
},
//输入框更新
modelUpdater(node, value){
node.value = value;
}
}
}