Vue 实现mvvm框架

4人阅读 评论(0) 收藏 举报
分类:

最近手痒,当然也是为了近阶段的跳槽做准备,利用周五时光,仿照vue用法,实现一下mvvm的双向绑定、数据代理、大胡子{{}}模板、指令v-on,v-bind等。当然由于时间紧迫,里面的编码细节没有做优化,还请各位看官多多包涵!看招:


实现原理

  • 数据的劫持观察(observe)
  • 观察者模式(watcher)
  • 使用es6的类class实现(当然,没有考虑到兼容性,只是为了实现而已)


代码:

  • 数据劫持
_observe(obj){
        // 递归遍历
        // let value;
        for (const key in obj) {
          let value;
          if (obj.hasOwnProperty(key)){
            // 利用原理 劫持数据---发布订阅

            value = obj[key];
            if (typeof value === 'object') {
              console.log('value', value)
              this._observe(value)
            }

            // 订阅(key)数据
            if (!this._binding[key]) {this._binding[key]= []};
            let binding = this._binding[key]
            // 重写getter, setter
            Object.defineProperty(obj, key, {
              enumerable: true,
              configurable: true,
              get() {
                return value
              },
              set(newVal) {
                if (value === newVal) return false;
                value = newVal
                console.log('newvalue', value)
                // 主要value更新,就发布通知(监听这个key的所有的)watcher更新(改变dom)
                binding.forEach(watcher => {
                  console.log('watcher', watcher)
                  watcher.update()
                });
              }
            })
          }
        }
      }
  • 实例代理数据
_proxyData(data, vm) {
        // data身上的所有属性全部挂载到vm实例上
        for (const key in data) {
          // let val = data[key];
          // ctx.key = val;
          Object.defineProperty(vm, key, {
            configurable: true,
            enumerable: true,
            get() {
              return data[key];
            },
            set(newVal) {
              data[key] = newVal;
              vm._observe(newVal)
            }
          })
        }
      }
  • 模板编译,添加发布订阅
_compile(root){
        // 获取所有节点
        let nodes = root.childNodes
        
        // 递归编译
        Array.from(nodes).forEach(node => {
          // 针对每一个节点进行处理

          // 元素节点
          if (node.nodeType === 1) {//只考虑绑定了一个指令
            // 获取节点的属性集合
            
            const attributes = Array.from(node.attributes);

            // 指令进行编译
            if (hasDirective(attributes, 'v-bind')) {
              const attrVal = getDirectiveValue(node, attributes, 'v-bind');
              const exp = getDirectiveParams(attributes, 'v-bind');
              // const 
              node.setAttribute(exp, this.$data[attrVal])
              this._binding[attrVal].push(new watcher({
                vm: this, 
                el: node,
                exp,
                attr: attrVal
              }))
            }
            if (hasDirective(attributes, 'v-on')) {
              const eventName = getDirectiveParams(attributes, 'v-on');
              node.addEventListener(eventName, (e) => {
                this.$methods[getDirectiveValue(node, attributes, 'v-on')].call(this)
              })
            }
            if (node.hasAttribute('v-model') && node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') {
              let attrVal = node.getAttribute('v-model');
              this._binding[attrVal].push(new Watcher({
                  vm: this,
                  el: node,
                  attr: attrVal,
                  name: 'v-model'
                }))
              node.addEventListener('input', e=> {
                this.$data[attrVal] = node.value;
              })
              node.value = this.$data[attrVal]
            }

            // 递归接着处理
            if (node.hasChildNodes()) {
              this._compile(node)
            }
          }

          // 文本节点
          if (node.nodeType === 3) {
            let text = node.textContent;

            let keyArr = [];
            // 获取{{变量}},用正则去匹配;watcher去观察{{变量}}(包裹元素),
            let newText = text.replace(/\{\{(\w+)\}\}/g, (match, p0)=> {
              keyArr = [...keyArr, p0];
              // 替换属性为真正的属性值
              return this.$data[p0]
            })
            node.textContent = newText;

            // 把整个文本节点进行监控{{v1}}-----{{v2}};添加到订阅到数组里等待通知
            keyArr.forEach(key => {
              // !this._binding[key] && (this._binding[key] = [])
              this._binding[key].push(new Watcher({
                vm: this,
                el: node,
                attr: text
              }))
            })
          }
        })
      }
  • 观察者实例
class Watcher {
      constructor({
        vm,
        name,
        el,
        exp,
        attr,
      }) {
        this.vm = vm;
        this.el = el;
        this.name = name;
        this.exp = exp;
        this.attr = attr;
      }
      // 更新text,或更新属性
      update() {
        // 改变节点的属性
        if (this.el.nodeType === 1) {
          // this.el.value = this.vm.$data[this.exp]
          if (this.name === 'v-model') {
            console.log('value', this.el)
            this.el.value = this.vm.$data[this.attr]
          }
          this.el[this.attr] = this.vm.$data[this.exp]
        }
        // 文本节点
        else {
          let text = this.attr;
          // 获取{{变量}},用正则去匹配;watcher去观察{{变量}}(包裹元素),
          let newText = text.replace(/\{\{(\w+)\}\}/g, (match, p0)=> {
            // 替换属性为真正的属性值
            return this.vm.$data[p0]
          })
          this.el.textContent = newText;
        }
      }
    }
  • 整体代码
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>vue实现</title>
  <style>
  </style>
</head>
<body>
  <div id="app">
    <!-- <form> -->
    <h2>{{title}}</h2>
    <p>{{text}}</p>
    <input type="text" v-model="number">
    <button v-on:click="increase">增加</button>
    <button v-on:click="decrease">减少</button>
    <!-- </form> -->
    <p>我点的时候就会变化{{number}}---{{number}}</h2>
  </div>
  <script>
    // 构造watcher类,用来观察数据变化来(本质更新dom的指令属性或innertext)
    /**
     * vm: vue实例 
     * name: 指令名
     * el: 节点
     * exp: 指令对应的参数
     * attr: 指令值(绑定的属性名)
    **/ 
    class Watcher {
      constructor({
        vm,
        name,
        el,
        exp,
        attr,
      }) {
        this.vm = vm;
        this.el = el;
        this.name = name;
        this.exp = exp;
        this.attr = attr;
      }
      // 更新text,或更新属性
      update() {
        // 改变节点的属性
        if (this.el.nodeType === 1) {
          // this.el.value = this.vm.$data[this.exp]
          if (this.name === 'v-model') {
            console.log('value', this.el)
            this.el.value = this.vm.$data[this.attr]
          }
          this.el[this.attr] = this.vm.$data[this.exp]
        }
        // 文本节点
        else {
          let text = this.attr;
          // 获取{{变量}},用正则去匹配;watcher去观察{{变量}}(包裹元素),
          let newText = text.replace(/\{\{(\w+)\}\}/g, (match, p0)=> {
            // 替换属性为真正的属性值
            return this.vm.$data[p0]
          })
          this.el.textContent = newText;
        }
      }
    }


    function hasDirective(attrs, dir) {
      return attrs.some(attr => attr.name.indexOf(dir) !== -1)
    }
    function getDirectiveParams(attrs, dir) {
      dir = attrs.find(attr => attr.name.indexOf(dir) !== -1).name
      return dir.split(':')[1] ? dir.split(':')[1].split('.')[0] : '';
    }
    function getDirectiveValue(node, attrs, dir) {
      return attrs.find(attr => attr.name.indexOf(dir) !== -1).value;
    }

    class DuVue {
      constructor(options) {
        this._init(options);
      }
      _init(options) {
        this.$options = options
        this.$data = options.data
        this.$methods = options.methods
        this.$el = document.querySelector(options.el)

        this._binding = {}
        this._observe(this.$data)
        // 代理所有数据
        this._proxyData(this.$data, this)

        this._compile(this.$el)
      }
      _observe(obj){
        // 递归遍历
        // let value;
        for (const key in obj) {
          let value;
          if (obj.hasOwnProperty(key)){
            // 利用原理 劫持数据---发布订阅

            value = obj[key];
            if (typeof value === 'object') {
              console.log('value', value)
              this._observe(value)
            }

            // 订阅(key)数据
            if (!this._binding[key]) {this._binding[key]= []};
            let binding = this._binding[key]
            // 重写getter, setter
            Object.defineProperty(obj, key, {
              enumerable: true,
              configurable: true,
              get() {
                return value
              },
              set(newVal) {
                if (value === newVal) return false;
                value = newVal
                console.log('newvalue', value)
                // 主要value更新,就发布通知(监听这个key的所有的)watcher更新(改变dom)
                binding.forEach(watcher => {
                  console.log('watcher', watcher)
                  watcher.update()
                });
              }
            })
          }
        }
      }
      // 实例代理数据
      _proxyData(data, vm) {
        // data身上的所有属性全部挂载到vm实例上
        for (const key in data) {
          // let val = data[key];
          // ctx.key = val;
          Object.defineProperty(vm, key, {
            configurable: true,
            enumerable: true,
            get() {
              return data[key];
            },
            set(newVal) {
              data[key] = newVal;
              vm._observe(newVal)
            }
          })
        }
      }
      _compile(root){
        // 获取所有节点
        let nodes = root.childNodes
        
        // 递归编译
        Array.from(nodes).forEach(node => {
          // 针对每一个节点进行处理

          // 元素节点
          if (node.nodeType === 1) {//只考虑绑定了一个指令
            // 获取节点的属性集合
            
            const attributes = Array.from(node.attributes);

            // 指令进行编译
            if (hasDirective(attributes, 'v-bind')) {
              const attrVal = getDirectiveValue(node, attributes, 'v-bind');
              const exp = getDirectiveParams(attributes, 'v-bind');
              // const 
              node.setAttribute(exp, this.$data[attrVal])
              this._binding[attrVal].push(new watcher({
                vm: this, 
                el: node,
                exp,
                attr: attrVal
              }))
            }
            if (hasDirective(attributes, 'v-on')) {
              const eventName = getDirectiveParams(attributes, 'v-on');
              node.addEventListener(eventName, (e) => {
                this.$methods[getDirectiveValue(node, attributes, 'v-on')].call(this)
              })
            }
            if (node.hasAttribute('v-model') && node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') {
              let attrVal = node.getAttribute('v-model');
              this._binding[attrVal].push(new Watcher({
                  vm: this,
                  el: node,
                  attr: attrVal,
                  name: 'v-model'
                }))
              node.addEventListener('input', e=> {
                this.$data[attrVal] = node.value;
              })
              node.value = this.$data[attrVal]
            }

            // 递归接着处理
            if (node.hasChildNodes()) {
              this._compile(node)
            }
          }

          // 文本节点
          if (node.nodeType === 3) {
            let text = node.textContent;

            let keyArr = [];
            // 获取{{变量}},用正则去匹配;watcher去观察{{变量}}(包裹元素),
            let newText = text.replace(/\{\{(\w+)\}\}/g, (match, p0)=> {
              keyArr = [...keyArr, p0];
              // 替换属性为真正的属性值
              return this.$data[p0]
            })
            node.textContent = newText;

            // 把整个文本节点进行监控{{v1}}-----{{v2}};添加到订阅到数组里等待通知
            keyArr.forEach(key => {
              // !this._binding[key] && (this._binding[key] = [])
              this._binding[key].push(new Watcher({
                vm: this,
                el: node,
                attr: text
              }))
            })
          }
        })
      }
    }
    window.onload = function(){
      var duVue = new DuVue({
        el: '#app',
        data: {
          number: 0,
          title: '手写vue',
          text: '用到es6 class',
          obj: {a:1}
        },
        methods: {
          increase() {
            console.log('click-increase')
            this.number++
          },
          decrease() {
            this.number--
          }
        }
      })
      console.log(duVue)
    }
  </script>
</body>
</html>
查看评论

实现一个类 Vue 的 MVVM 框架

Vue 一个 MVVM 框架、一个响应式的组件系统,通过把页面抽象成一个个组件来增加复用性、降低复杂性 主要特色就是数据操纵视图变化,一旦数据变化自动更新所有关联组件~ 所以它的一大特性就是一个数...
  • aabv54321
  • aabv54321
  • 2017-01-27 19:43:01
  • 411

vue,angular,avalon这三种MVVM框架之间有什么优缺点?

Vue.jsVue.js@尤雨溪老师写的一个用于创建 web 交互界面的库,是一个精简的 MVVM。从技术角度讲,Vue.js 专注于 MVVM 模型的ViewModel层。它通过双向数据绑定把Vie...
  • zhouwangling_
  • zhouwangling_
  • 2016-11-03 00:27:30
  • 3185

浅谈对Vue.js的MVVM的理解

Vue.js  最近很火  这两天看了代码以及项目 本地运行了一下  感觉主要还是用来做SPA单页应用   谈到整体的架构 肯定要跟MVC做对比  典型的MVC结构  就是  M-...
  • u013321789
  • u013321789
  • 2017-07-20 00:55:02
  • 2164

剖析Vue实现原理 - 如何实现双向绑定mvvm

剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么? 1、了解vue的双向数据绑定原理以及核心代码模块 2、缓解好奇心的同时了解如何实现双向绑定 为了便于说明原理与实现,本...
  • zt_vicky
  • zt_vicky
  • 2017-03-14 11:07:34
  • 835

怎么理解VUE,VUE的数据驱动原理是什么,解释MVVM框架

一:Vue是什么,怎么理解Vue Vue是一个基于MVVM模式数据驱动页面的框架,它将数据绑定在视图上。属于实现单页面应用的技术。.总结起来的几大特点: (1) 简洁 (2) 轻量 (3)快速...
  • AN0692
  • AN0692
  • 2018-01-30 18:06:55
  • 499

自己实现MVVM(Vue源码解析)

前言本文会带大家手动实现一个双向绑定过程(仅仅涵盖一些简单的指令解析,如:v-text,v-model,插值),当然借鉴的是Vue1的源码,相信大家在阅读完本文后对Vue1会有一个更好的理解,源代码放...
  • zp1996323
  • zp1996323
  • 2016-12-10 14:26:57
  • 2413

初试MVVM框架之Vue.js - 列表渲染篇【南大软院大神养成计划】

初试MVVM框架之Vue.js - 列表渲染篇
  • github_32919701
  • github_32919701
  • 2015-11-18 21:10:44
  • 2953

vue学习之路之MVC,MVP,MVVM之间的区别和联系

最开始是MVC模式,这里M代表的是model负责提供数据,V是视图view,C 代表控制器,他们之间是单向通信,V和M之间的通信是通过C来作为桥梁的,也就是说V和M并不是直接通信; 再后来的是M...
  • codesWay
  • codesWay
  • 2017-03-03 22:40:14
  • 3686

轻量级MVVM框架Vue.js快速上手

课程主要分为两部分完成。第一部分:掌握Vue.js设计规范的语法。 第二部分:通过一些实际的前端案例来强化同学们对该技术的灵活运用。 实践阶段由浅入深分为四个案例:动态评分、图片轮播、OLTP管理界面、聊天室。涉及Vue和HTML5技术的整合,而OLTP管理界面应用在很多系统之内,是很常用的案例,里面包括CRUD的操作、数据分页、Ajax等等。通过这些案例,最终强化学生驾驭Vue.js的水平。
  • 2017年11月14日 10:33

JS组件系列——又一款MVVM组件:Vue(一:30分钟搞定前端增删改查)

阅读目录 一、MVVM大比拼二、Vue常用网址三、Vue基础入门 1、MVVM图例2、第一个Vue实例3、双向绑定 四、常用指令 1、v-text、v-html指令2、v-mode...
  • doc_wei
  • doc_wei
  • 2016-12-21 12:57:22
  • 2943
    个人资料
    持之以恒
    等级:
    访问量: 3万+
    积分: 944
    排名: 5万+
    最新评论
  • vue-router

    xuanwuziyou: 博主您好!我做了一个简单的vue-cli的项目的demo, 第一次进入登录页,url是:http:...