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

搭建一个小型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”,即可领红包。谢谢支持

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容