实现vue的双向绑定(二)

vue双向绑定 - 模板的编译

1、vue实现双向绑定的必要方法

  1. 模板的编译。即:{{}}的取值编译,指令的编译
  2. 数据劫持,观察数据变化。即:表单值改变,需要更新model数据
  3. Watcher,利用发布订阅末模式联系以上两者

2、构建环境

  1. 先将上篇文章中的#app内容修改如下
    <div id="app">
       <input type="text" v-model="message">
       <div>这是一个div{{message}}</div>
       <ul><li></li></ul>
       {{message}}
    </div>
    
  2. 新建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充当一个解析器,来做解析模板和绑定数据的工作,解析过程分为三步:

  1. 先把真实的DOM移入到内存中。利用 fragment 文档碎片,在内存中执行
  2. 提取想要的元素节点 v-model 和文本节点 {{}}
  3. 把编译好的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、编译元素

  1. v-model、v-text、v-html 这类vue指令的编译;
  2. 如果元素内不含指令,则忽略;
  3. 需要从实例化时传递的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;
        }
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值