美文网首页Vue.js专区
实现一个小型vue,探索原理

实现一个小型vue,探索原理

作者: Gary嘉骏 | 来源:发表于2017-09-09 22:49 被阅读0次

搭建一个小型vue,此处不用闭包,直观一点

测试例子

<div id="app">
    <h2 v-text='hello' v-show='1'></h2>
    <input type="text" v-model='counter'>
    <button v-on-click='add' type="button">add</button>
    <h2 v-if=isRender>hellow world</h2>
    <p v-text='counter'></p>
</div>
<script src='./mymvvm.js'></script>
<script>
    var vm = new MVVM({//自定义的一个名字
        el: 'app',//在id为app里面的所有元素
        data: {
            counter: 1,
            hello: 'ahahah!',
            isShow: true,
            isRender: false
        },
        methods: {
            add: function () {
                vm.counter += 1;
            }
        },
        // 钩子,初步渲染后就触发
        ready () {
            let self = this;
            self.hello = 'Ready, go!';
            setTimeout(function () {
                self.hello = 'Done!';
                self.isRender =true
            }, 3000)
        }
    })
</script>

在mymvvm内

  • 首先创建构造器MVVM
  • 定义指令对象的结构
class Directive { // 此使用typescript定义对象来说明
    el:Element //目标元素
    attr:{//元素属性
    name:string,
    value:string //绑定的对象名称
    },
    key:string, //绑定的对象名称
    dirname: string, //指令的名称
    definition: object, //指令的定义
    argument: string // 指令的参数
}
  • 定义指令的前缀 prefix;
  • 定义支持的指令名及其方法,创建对象Directives
  • 创建方法getDirSelectors由自定义的指令名称组成dom的选择器
  • 由选择器获得所有需要绑定的dom元素
  • 对以上dom元素及根元素通过processNode方法绑定指令方法,parseDireactive获得指令对象
  • 对指令与元素进行绑定的具体方法bindDirective,使用中转桥梁 bindings
  • 定义钩子函数(defineProperty),双向数据绑定bindAccessors,根据bindings去及时更新视图
  • MVVM对象继承opts.dataopts.methods的属性(简单合并实现,会有重名覆盖问题),就会触发钩子函数并更新视图
  • 触发生命周期ready函数

具体代码

var prefix ='v';

var Directives = {
  /**
   * 更改目标元素的文本内容
   * @param  {Element} el    目标元素
   * @param  {string} value 新的文本内容
   */
  text: function(el,value) {
    el.textContent = value || '';
  },
  /**
   * 更改目标dom元素是否隐藏
   * @param  {Element} el    目标元素
   * @param  {object} value 被用来作判定的值
   */
  show: function(el,value) {
    el.style.display = value? '':'none';
  },
  /**
   * 更改目标dom元素是否插入到dom树里
   * @param  {Element} el     目标元素
   * @param  {any} value     新的数值
   * @param  {string} dirAgr 指令的参数,如v-on-click,click就是参数
   * @param  {object} dir    对应指令的对象
   * @param  {object} vm     mvvm new出来的对象
   */
  if: function(el,value,dirAgr,dir,vm) {
    var idx = el.idx;
    if(!value) {
      el.remove();
    } else {
      var els = vm.$els;
      var length = els.length;
      var root = vm.$el;
      while(idx<length) {
        // 获得后一个元素
        var nextEl = els[idx+1];
        // 到最后一个直接append,终止while
        if(idx === length-1) {
          root.append(el);
          break;
        } else {
          // 判定后一个dom是否也被remove掉了,没有则在它前面插入el,终止while
          if(nextEl.parentNode === root) {
            root.insertBefore(el, nextEl);
            break;
          }
        }
        idx++;
      }
    }
  },
  /**
   * 双向数据绑定
   * @param  {Element} el     目标元素
   * @param  {any} value     新的数值
   * @param  {string} dirAgr 指令的参数,如v-on-click,click就是参数
   * @param  {object} dir    对应指令的对象
   * @param  {object} vm     mvvm new出来的对象
   * @param  {string} key    监听的变量名
   */
  model: function(el,value,dirAgr,dir,vm,key) {
    var eventName = 'keyup';
    el.value = value || '';
    // 绑定方法时把方法与元素变量建立连接,方便后期remove
    if(el.handlers && el.handlers[eventName]) {
      el.removeEventListener(eventName,el.handlers[eventName]);
    } else {
      el.handlers = {};
    }

    el.handlers[eventName] = function(e) {
      vm[key] = e.target.value;
    };

    el.addEventListener(eventName,el.handlers[eventName]);
  },
  on: {
    /**
     * 绑定方法
     * @param  {Element} el         目标元素
     * @param  {function} handler   绑定的方法
     * @param  {string} eventName  事件名称(指令on的参数)
     * @param  {Direactive} directive 指令对象
     */
    update: function(el,handler,eventName,directive) {
      if(!directive.handlers){
          directive.handlers = {};
      }
      var handlers = directive.handlers;

      //把之前的取消
      if(handlers[eventName]) {
        el.removeEventListener(eventName,handlers[eventName]);
      }

      if(handlers) {
        handler = handler.bind(el);
        el.addEventListener(eventName,handler);
        handlers[eventName] = handler;
      }
    }
  }
};

function MVVM(opts) {
  if(!opts.el) throw new Error('未提供目标dom的id');
  var self = this;
  var root = this.$el = document.getElementById(opts.el);
  var _data = this.$data = Object.assign(opts.data,opts.methods);
  // 获取有有效的指令的元素,getDirSelectors:返回所有的选择器
  var els = this.$els = root.querySelectorAll(getDirSelectors(Directives));
  var bindings = {}; // 指令与data关联的桥梁,记录绑定属性共绑定了多少个指令
  this._bindings = bindings;
  [].forEach.call(els,processNode);
  processNode(root);

  self = Object.assign(self,_data);

  // 触发生命周期ready
  if(opts.ready && typeof opts.ready == 'function') {
    this.ready = opts.ready;
    this.ready();
  }
  function processNode (el,idx) {
    // 记录所在位置,方便重新插入(if指令)
    el.idx = idx;
    // 对每个指令分别操作
    getAttributes(el.attributes).forEach(function(attr) {
      var directive = parseDireactive(attr,el);//获得指令对象
      if(directive) {
        bindDirective(self,el,bindings,directive);
      }
    });
  }
}

/**
 * 对vm的属性绑定指令
 * @param  {mvvm} vm         mvvm对象
 * @param  {Element} el         目标对象
 * @param  {object} bindings   绑定的桥梁对象
 * @param  {Direactive} directive 指令对象
 */
function bindDirective(vm,el,bindings,directive) {
  el.removeAttribute(directive.attr.name);
  var key = directive.key;
  // 筛选属性,对于非show 或者 if 的指令,绑定的是没有提供的属性则报错,否则进行判断;
  if(!vm.$data.hasOwnProperty(key)) {
    var booleanDirective =['show','if'];
    if((booleanDirective.indexOf( directive.dirname))>-1) {
      // 把key当表达式javascript运行使用,并覆盖key;
      try {
        eval('key='+key);
      } catch(e) {
        console.log(e);
      }
      // 更新视图
      directive.update(directive.el,key);
      // 返回,不对此属性进行双向绑定
      return;
    } else {
      throw new Error(key+'未定义');
    }
  }
  var binding = bindings[key];
  if(!binding) {
    bindings[key]=binding={
      value:'',
      directives:[]
    };
  }
  // 对属性增加指令对象
  binding.directives.push(directive);
  if(!vm.hasOwnProperty(key)) {// 判定属性是否存在这个对象
    // binding 与 bindings 相等,所以bindings改变,set里的binding页改变
    bindAccessors(vm,key,binding);//双向绑定,定义钩子函数
  }
}

/**
 * 在defineProperty插入钩子函数,根据指令更新视图
 * @param  {Element} el         目标dom对象
 * @param  {string} key     需要观察的属性名称
 * @param  {object} bindings   绑定的桥梁对象
 */
function bindAccessors(vm,key,binding) {
  Object.defineProperty(vm,key,{
    get:function(){
      return binding.value;
    },
    set:function(nValue) {
      binding.value = nValue;
      binding.directives.forEach(function(directive) {
        directive.update(directive.el,nValue,directive.argument,directive,vm,key);
      });
    }
  });
}

/**
 * 由dom元素属性生成Directive对象
 * @param  {object} attr dom元素属性
 * @param  {Element} el  目标dom对象
 * @return {Directive}   自定义对象
 */
function parseDireactive(attr,el) {
  // 前缀不对返回
  if(attr.name.indexOf(prefix) === -1) {
    return;
  }

  var directiveStr = attr.name.slice(prefix.length+1);
      argIndex = directiveStr.indexOf('-');
      directiveName = argIndex === -1? directiveStr: directiveStr.slice(0,argIndex);
      directiveDef = Directives[directiveName];//指令的定义
      // 获得on的参数
      arg = argIndex === -1? null : directiveStr.slice(argIndex+1);
      key = attr.value;
      return directiveDef? {
        attr:attr,key:key,el:el,dirname:directiveName,definition:directiveDef,argument:arg,
        update:typeof directiveDef == 'function'? directiveDef:directiveDef.update
      }: null;
}
function getAttributes(attributes) {
  return [].map.call(attributes, function(attr) {
    return {
      name:attr.name,
      value:attr.value
    };
  });
}

function getDirSelectors(directives) {
  var eventArr = ['click','change','blur']; // 定义指令on支持的参数

  return Object.keys(directives).map(function(directive) {
    return '[' + prefix + '-' + directive +']';
  }).join() + ',' + eventArr.map(function(eventName) {
    return '[' + prefix + "-on-" + eventName + ']';
  });
}

Done!

如果觉得文章对你有点用的话,麻烦拿出手机,这里有一个你我都有的小福利(每天一次): 打开支付宝首页搜索“8601304”,即可领红包。谢谢支持

相关文章

网友评论

    本文标题:实现一个小型vue,探索原理

    本文链接:https://www.haomeiwen.com/subject/jnnajxtx.html