( 第四篇 )仿写'Vue生态'系列___"Proxy双向绑定与封装请求"

( 第四篇 )仿写'Vue生态'系列___"Proxy双向绑定与封装请求"

本次任务 前端

  1. vue3.0使用了Proxy进行数据的劫持, 那固然就有必要研究并实战一下这方面知识了.
  2. 对Reflect进行解读, 并将Object的操做少部分改成Reflect的形式.
  3. 异步不能总用定时器模拟, 本次本身封装一个简易的'axios'.
  4. 有了请求固然须要服务器, 用koa启动一个简易的服务.

一. Proxy

vue3.0选择了这个属性, 虽然也会提供兼容版本, 但基本也算是跟老版ie说再见了, Proxy会解决以前没法监听数组的修改这个痛点, 也算是我辈前端的福音了.
使用方面会有很大不一样, defineProperty是监控一个对象, 而Proxy是返回一个新对象, 这就须要我彻底重写Observer模块了, 话很少说先把基本功能演示一下.
由下面的代码可知:vue

  1. Proxy能够代理数组.
  2. 代理并不会改变原数据的类型, Array仍是Array.
  3. 修改length属性会触发set, 浏览器认为length固然是属性, 修改他固然要触发set.
  4. 像是push, pop这种操做也是会触发set的, 并且不止一次, 能够借此看出这些方法的实现原理.
let ary = [1, 2, 3, 4];
  let proxy = new Proxy(ary, {
    get(target, key) {
      return target[key];
    },
    set(target, key, value) {
        console.log('我被触发了');
      return value;
    }
  });
  console.log(Array.isArray(proxy)); // true
  proxy.length = 1; // 我被触发了

我以前写的劫持模块就须要完全改版了
cc_vue/src/Observer.js
改变$data指向我选择在这里作, 为了保持主函数的纯净.ios

// 数据劫持
import { Dep } from './Watch';
let toString = Object.prototype.toString;
class Observer {
  constructor(vm, data) {
    // 因为Proxy的机制是返回一个代理对象, 那咱们就须要更改实例上的$data的指向了
    vm.$data = this.observer(data);
  }
}

export default Observer;

observer
对象与数组是两种循环的方式, 每次递归的解析里面的元素, 最后整个对象彻底由Proxy组成.nginx

observer(data) {
    let type = toString.call(data),
        $data = this.defineReactive(data);
    if (type === '[object Object]') {
      for (let item in data) {
        data[item] = this.defineReactive(data[item]);
      }
    } else if (type === '[object Array]') {
      let len = data.length;
      for (let i; i < len; i++) {
        data[i] = this.defineReactive(data[i]);
      }
    }
    return $data;
  }

defineReactive
遇到基本类型我会直接return;
代理基本类型还会报错😯;git

defineReactive(data) {
    let type = toString.call(data);
    if (type !== '[object Object]' && type !== '[object Array]') return data;
    let dep = new Dep(),
        _this = this;
    return new Proxy(data, {
      get(target, key) {
        Dep.target && dep.addSub(Dep.target);
        return target[key];
      },
      set(target, key, value) {
        if (target[key] !== value) {
        // 万一用户付给了一个新的对象, 就须要从新生成监听元素了.
          target[key] = _this.observer(value);
          dep.notify();
        }
        return value;
      }
    });
  }

Observer模块改装完毕
如今vm上面的data已是Proxy代理的data了, 也挺费性能的, 因此说用vue开发的时候, 尽可能不要弄太多数据在data身上.github

二. Reflect

这个属性也蛮有趣的, 它的出现很符合设计模式, 数据就是要有一套专用的处理方法, 并且函数式处理更符合js的设计理念.web

  1. 静态方法 Reflect.defineProperty() 基本等同于 Object.defineProperty() 方法,惟一不一样是返回 Boolean 值, 这样就不用担忧defineProperty时的报错了.
  2. Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。

下面把经常使用的方法演示一下
操做成功或失败会返回布尔值ajax

let obj = {name:'lulu'};
  console.log(Reflect.get(obj,'name')) // name
  console.log(Reflect.has(obj,'name')) // true
  console.log(Reflect.has(obj,'name1')) // false
  console.log(Reflect.set(obj,'age',24)) // true
  console.log(Reflect.get(obj,'age')) // 24

把个人代码稍微改装一下
cc_vue/src/index.js数据库

proxyVm(data = {}, target = this) {
    for (let key in data) {
      Reflect.defineProperty(target, key, {
        enumerable: true, // 描述属性是否会出如今for in 或者 Object.keys()的遍历中
        configurable: true, // 描述属性是否配置,以及能否删除
        get() {
          return Reflect.get(data,key)
        },
        set(newVal) {
          if (newVal !== data[key]) {
            Reflect.set(data,key,newVal)
          }
        }
      });
    }
  }

三. 封装简易的"axios"

我见过不少人离开axios或者jq中的ajax就无法作项目了, 其实彻底能够本身封装一个, 原理都差很少, 并且如今也能够用'feach'弄, 条件容许的状况下真的不必定非要依赖插件.
独立的文件夹负责网络相关的事宜;
cc_vue/use/httpnpm

class C_http {
  constructor() {
    // 请求可能不少, 而且须要互不干涉, 因此决定每一个类生成一个独立的请求
    let request = new XMLHttpRequest();
    request.responseType = 'json';
    this.request = request;
  }
}

编写插件的时候, 先要考虑用户会怎么用它

  1. 用户指定请求的方法, 本次只作post与get.
  2. 能够配置请求地址.
  3. 能够传参, 固然post与get处理参数确定不同.
  4. 返回值咱们用Promise的形式返回给用户.
http.get('http:xxx.com', { name: 'lulu'}).then(data => {});
  http.post('http:xxx.com', { name: 'lulu'}).then(data => {});

get与post方法其实不用每次都初始化, 咱们直接写在外面
处理好参数直接调用open方法, 进入open状态某些参数才能设置;
在有参数的状况下为连接添加'?';
参数品在连接后面, 我以前遇到一个bug, 拼接参数的时候若是结尾是'&'部分手机出现跳转错误, 因此为了防止特殊状况的发生, 咱们要判断一下干掉结尾的'&';

function get(path, data) {
  let c_http = new C_http();
  let str = '?';
  for (let i in data) {
    str += `${i}=${data[i]}&`;
  }
  if (str.charAt(str.length - 1) === '&') {
    str = str.slice(0, -1);
  }
  path = str === '?' ? path : `${path}${str}`;
  c_http.request.open('GET', path);
  return c_http.handleReadyStateChange();
}

post
这个就很好说了, .data是请求自带的.

function post(path, data) {
  let c_http = new C_http();
  c_http.request.open('POST', path);
  c_http.data = data;
  return c_http.handleReadyStateChange();
}

handleReadyStateChange

handleReadyStateChange() { 
    // 这个须要在open以后写
    // 设置数据类型
    this.request.setRequestHeader(
      'content-type',
      'application/json;charset=utf-8'
    );
    // 如今前端全部返回都是Promise化;
    return new Promise((resolve) => {
      this.request.onreadystatechange = () => {
        // 0    UNSENT    代理被建立,但还没有调用 open() 方法。
        // 1    OPENED    open() 方法已经被调用。
        // 2    HEADERS_RECEIVED    send() 方法已经被调用,而且头部和状态已经可得到。
        // 3    LOADING    下载中; responseText 属性已经包含部分数据。
        // 4    DONE    下载操做已完成。
        if (this.request.readyState === 4) {
        // 这里由于是独立开发, 就直接写200了, 具体项目里面会比较复杂
          if (this.request.status === 200) {
           // 返回值都在response变量里面
            resolve(this.request.response);
          }
        }
      };
      // 真正的发送事件.
      this.send();
    });
  }

send

send() {
// 数据必定要JSON处理一下
    this.request.send(JSON.stringify(this.data));
  }

不少人提到 "拦截器" 会感受很高大上, 其实真的没啥
简易的拦截器"interceptors"👇

// 1: 使用对象不使用[]是由于能够高效的删除拦截器
const interceptorsList = {};
// 2: 每次发送数据以前执行全部拦截器, 别忘了把请求源传进去.
  send() {
    for (let i in interceptorsList) {
      interceptorsList[i](this);
    }
    this.request.send(JSON.stringify(this.data));
  }
// 3: 添加与删除拦截器的方法, 没啥东西因此直接协议期了.
function interceptors(cb, type) {
  if (type === 'remove') {
    delete interceptorsList[cb];
  } else if (typeof cb === 'function') {
    interceptorsList[cb] = cb;
  }
}

边边角角的小功能

  1. 设置超出时间与超出时的回调.
  2. 请求的取消
class C_http {
  constructor() {
    let request = new XMLHttpRequest();
    request.timeout = 5000;
    request.responseType = 'json';
    request.ontimeout = this.ontimeout;
    this.request = request;
  }
 ontimeout() {
    throw new Error('超时了,快检查一下');
  }
abort() {
    this.request.abort();
  }
}

简易的'axios'就作好, 普通的请求都没问题的

四. 服务器

请求作好了, 固然要启动服务了, 本次就不链接数据库了, 要否则就跑题了.

koa2
不了解koa的同窗跟着作也没问题

npm install koa-generator -g
Koa2 项目名

cc_vue/use/server 是本次工程的服务相关存放处.

cc_vue/use/server/bin/www
端口号能够随意更改, 当时9999被占了我就设了9998;

const pros = '9998';
var port = normalizePort(process.env.PORT || pros);

cc_vue/use/server/routes/index.js

这个页面就是专门处理路由相关, koa很贴心, router.get就是处理get请求.
每一个函数必须写async也是为了著名的'洋葱圈'.
想了解更多相关知识能够去看koa教程, 我也是用到的时候才会去看一眼.
写代码的时候遇到须要测试延迟相关的时候, 不要总用定时器, 要多本身启动服务.

const router = require('koa-router')();

router.get('/', async (ctx, next) => {
  ctx.body = {
    data: '我是数据'
  };
});

router.post('/', async (ctx, next) => {
  ctx.body = ctx.request.body;
});

module.exports = router;

写到如今能够开始跑起来试试了

五.跨域

😺一个很传统的问题出现了'跨域'.
这里咱们简单的选择插件来解决, 十分粗暴.
cc_vue/use/server/app.js

npm install --save koa2-cors
var cors = require('koa2-cors');
app.use(cors());

既然说到这里就, 那就总结一下吧
跨域的几种方式

  1. jsonp 这个太传统了, 制做一个script标签发送请求.
  2. cors 也就是服务端设置容许什么来源的请求, 什么方法的请求等等,才能够跨域.
  3. postMessage 两个页面之间传值, 常常出如今一个页面负责登陆, 另外一个页面获取用户的登陆token.
  4. document.domain 相同的domain能够互相拿数据.
  5. window.name 这个没人用, 可是挺好玩, 有三个页面 a,b,c, a与b 同源, c单独一个源, a用iframe打开c页面, c把要传的值放在 window.name上,监听加载成功事件, 瞬间改变 iframe 的地址, 为b, 此时 b 同源, window 会被继承过来, 偷梁换柱, 利用了换地址 window不变的特色;
  6. location.hash 这个也好玩, 是很聪明的人想出来的, 有三个页面 a,b,c, a与b 同源, c单独一个源,a给c传一个 hash 值(由于一个网址而已,不会跨域), c把 hash解析好, 把结果 用iframe 传递给 b,b 使用 window.parent.parent 找到父级的父级, window.parent.parent.location.hash = 'xxxxx', 操控父级;
  7. http-proxy 就好比说vue的代理请求, 毕竟服务器之间不存在跨域.
  8. nginx 配置一下就行了, 比前端作好多了
  9. websocket 人家天生就不跨域.

本次测试的dom结构

<div id="app">
      <p>n: {{ n.length }}</p>
      <p>m: {{ m }}</p>
      <p>n+m: {{ n.length + m }}</p>
      <p>{{ http }}</p>
    </div>
let vm = new C({
    el: '#app',
    data: {
      n: [1, 2, 3],
      m: 2,
      http: '等待中'
    }
  });
  http.get('http://localhost:9998/', { name: 'lulu', age: '23' }).then(data => {
    vm.http = data.data;
    vm.n.length = 1
    vm.n.push('22')
  });

具体效果请在工程里面查看

end

作这个工程能让本身对vue对框架以及数据的操做有更深的理解, 受益不浅.
下一集:

  1. 对指令的解析.
  2. 具体指令的处理方式.
  3. 篇幅够的话聊聊事件与生命周期

你们均可以一块儿交流, 共同窗习,共同进步, 早日实现自我价值!!

github:github
我的技术博客:连接描述
更多文章,ui库的编写文章列表 :连接描述

相关文章
相关标签/搜索