ES6高阶--实现MVVM框架

197 阅读1分钟

1  利用defineProperty实现数据劫持

<!DOCTYPE html>
<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>Document</title>
</head>
<body>
  <input id='input' />
  <span id="span"></span>
</body>
<script type="text/javascript">
  // 数据
  const data = {
    text: 'default'
  };
  const input = document.getElementById('input');
  const span = document.getElementById('span');
  // 数据劫持
  Object.defineProperty(data, 'text', {
  // 数据变化 --> 修改视图
    set(newVal) {
      input.value = newVal;
      span.innerHTML = newVal;
    }
  });
  // 视图更改 --> 数据变化
  input.addEventListener('keyup', function(e) {
    data.text = e.target.value;
  });
</script>
</html>

2  利用ES6中的proxy实现数据劫持

<!DOCTYPE html>
<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>Document</title>
</head>
<body>
  <input id='input' />
  <span id="span"></span>
</body>
<script type="text/javascript">
// 数据
  const data = {
    text: 'default'
  };
  const input = document.getElementById('input');
  const span = document.getElementById('span');
  // 数据劫持
  const handler = {
    set(target, key, value) {
      target[key] = value;
      // 数据变化 --> 修改视图
      input.value = value;
      span.innerHTML = value;
      return value;
    }
  };
  const proxy = new Proxy(data, handler);

  // 视图更改 --> 数据变化
  input.addEventListener('keyup', function(e) {
    proxy.text = e.target.value;
  });

</script>
</html>

3 Proxy和Object.defineProperty区别

  1. roxy使用比Object.defineProperty方便

  2. Proxy代理整个对象,Object.defineProperty只代理对象上的某个属性

  3. 如果对象内部要全部递归代理,则Proxy可以只在调用时递归,而Object.defineProperty需要在一开始就全部递归,Proxy性能优于Object.defineProperty;

  4. 对象上定义新属性时,Proxy可以监听到,Object.defineProperty监听不到;

  5. 数组新增删除修改时,Proxy可以监听到,Object.defineProperty监听不到;

  6. Proxy不兼容IE, Object.defineProperty不兼容IE8及以下;

  7. Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改;

如下代码所示

let target = {};
let p = new Proxy(target, {});

p.a = 37;   // 操作转发到目标

console.log(target.a);    // 37. 操作已经被正确地转发

target.a=4;
console.log(p.a)//4

console.log(target==p)//false

4   mvvm框架实现

下面demo实现了一个mvvm框架,基于EventTarget实现;EventTarget是一个DOM接口,由可以接收事件,并且可以创建侦听器的对象实现;

4.1 Object.defineProperty实现数据劫持

index.html代码

<!DOCTYPE html>
<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>Document</title>
</head>
<body>
  <div id="app">
    {{message}}
    <div>
      111
      <div>
        {{message}}
      </div>
    </div>
    <div v-html="htmlData"></div>
    <input v-model="modelData" /> {{modelData}}
  </div>
</body>
<script>

</script>
</html>

webpack.config.js简单配置

let path = require('path');
let HtmlWebpackPlugin = require('html-webpack-plugin');
let Webpack = require('webpack');

module.exports = {
  mode: 'production',
  entry: {
    index: './src/index.js'
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: { // 用babel-loader需要把es6-es5
            presets: [
              '@babel/preset-env'
            ]
          }
        },
      },
      {
        test: /\.css$/,
        use: [
          'css-loader',
          'postcss-loader',
        ]
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './index.html',
      filename: 'index.html',
    }),
  ]
}

index.js代码

import Kvue from './Kvue.js';

let vm = new Kvue({
  el: '#app',
  data: {
    message: '测试数据',
    htmlData: 'html数据',
    modelData: '绑定的数据'
  }
})

setTimeout(() => {
  console.log("setTimeout...");
  vm.$options.data.message = "修改的数据";
}, 1000);

Kvue.js代码

export default class Kvue extends EventTarget {
  constructor(options) {
    super();
    this.$options = options;
    this.compile();
    this.observe(this.$options.data);
  }

  observe(data) {
    let keys = Object.keys(data);
    keys.forEach(key => {
      this.defineReact(data, key, data[key]);
    })
  }

  defineReact(data, key, value) {
    let _this = this;
    Object.defineProperty(data, key, {
      configurable: true,
      enumerable: true,
      get() {
        console.log("get...");
        return value;
      },
      set(newVal) {
        console.log('set...');
        let event = new CustomEvent(key, {
          detail: newVal
        });
        _this.dispatchEvent(event);
        value = newVal;
      }
    });
  }

  compile() {
    let el = document.querySelector(this.$options.el);
    this.compileNode(el);
  }

  compileNode(el) {
    let childNodes = el.childNodes;
    console.log(childNodes)
    childNodes.forEach(node => {
      if(node.nodeType === 1) {
        // 标签
        let attrs = node.attributes;
        [...attrs].forEach(attr => {
          let attrName = attr.name;
          let attrValue = attr.value;
          if(attrName.indexOf("v-") === 0) {
            attrName = attrName.substr(2);
            if(attrName === 'html') {
              node.innerHTML = this.$options.data[attrValue];
            } else if(attrName === 'model') {
              node.value = this.$options.data[attrValue];
              node.addEventListener("input", e => {
                this.$options.data[attrValue] = e.target.value;
              })
            }
          }
        })
        if(node.childNodes.length > 0) {
          this.compileNode(node);
        }
      } else if(node.nodeType === 3) {
        // 文本节点
        let reg = /\{\{\s*(\S+)\s*\}\}/g;
        let textContent = node.textContent;
        if(reg.test(textContent)) {
          // console.log('存在双花括号');
          // console.log(RegExp.$2);
          let $1 = RegExp.$1;
          // node.textContent = this.$options.data[$1];
          node.textContent = node.textContent.replace(reg, this.$options.data[$1]);
          this.addEventListener($1,  e => {
            // console.log('触发了修改。。。');
            // console.log(e.detail);
            // 重新渲染视图
            let oldVal = this.$options.data[$1];
            let reg = new RegExp(oldVal);
            node.textContent = node.textContent.replace(reg, e.detail);
          })
        }
      }
    })
  }
}

4.2 Proxy实现数据劫持

Kvue.js代码里面observe修改如下:

observe(data) {
    let _this = this;
    this.$options.data = new Proxy(data, {
      get(target, key) {
        return target[key];
      },
      set(target, key, newVal) {
        let event = new CustomEvent(key, {
          detail: newVal
        });
        _this.dispatchEvent(event);
        target[key] = newVal;
        return true;
      }
    })
  }