( 第六篇 )仿写'Vue生态'系列___"模板loader与计算属性"

( 第六篇 )仿写'Vue生态'系列___"模板loader与计算属性"


本次任务css

  1. 编写'cc-loader', 使咱们可使用'xxx.cc'文件来运行个人框架代码.
  2. 为'cc_vue'添加生命周期函数.
  3. 新增'计算属性'.
  4. 新增'观察属性'.
一.'cc-loader'的定义与编写
  1. 本次只是编写一个最基础的版本, 后续完善组件化功能的时候, 会对它有所改动.
  2. 使'webpack'能够解析后缀为'cc'的文件.
  3. 必须作到很是的轻量.

让咱们一步一步来作出这样一个'loader', 首先我先介绍一下文件结构, 在'src'文件夹平级创建'loader'文件夹, 里面能够存放之后我作的全部'loader'.html

不可或缺的步骤就是定义'loader'的路径.
cc_vue/config/common.js
新增resolveLoader项vue

resolveLoader: {
   // 方式1: 
   // 若是我书写 require('ccloader');
   // 那么就会去 path.resolve(__dirname, '../loader')寻找这个引入.
    alias: {
      ccloader: path.resolve(__dirname, '../loader')
     },
    // 方式2: 当存在require('xxx');这种写法时, 先去'node_modules'找寻, 找不到再去path.resolve(__dirname,'../loader')找找看.
    modules:[
        'node_modules',
        path.resolve(__dirname,'../loader')
    ]
  },

'loader'文件的配置写完了, 那么能够开始正式写这个'loader'了.
cc_vue/loader/cc-loader.jsnode

// 1: 'source'就是你读取到的文件的代码, 他是'string'类型.
function loader(source) {
 // ..具体处理函数
 // 2: 处理完了必定要把它返回出去, 由于可能还有其余'loader'要处理它, 或者是直接执行处理好的代码.
  return source;
}

module.exports = loader;

模板的定义webpack

  1. 本次暂不处理title, 由于没啥技术含量暂时也不必.
  2. '#--style-->' 会被替换为css的内容.
  3. '#--template-->' 会被替换为模板的内容.
<!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>编译模板</title>
    #--style-->
  </head>
  <body>
      <div id="app">
          #--template-->
      </div>
  </body>
</html>

最终要达到的'.cc'文件的书写方式git

<template>
  <div class="box"
       v-on:click='add'>
    <span>{{n}}</span>
  </div>
</template>
<script>
    console.log('此处也可执行代码');
    export default {
      el: "#app",
      data: {
        n: 2
      },
      methods: {
        add() {
          this.n++;
        }
      }
    };
</script>
<style>
    .box {
      border: 1px solid;
      height: 600px;
    }
</style>

读取'.cc'文件
cc_vue/config/common.jsgithub

{
   test: /\.cc$/,
   use: ['cc-loader']
 },

loader正式走起
cc_vue/loader/cc-loader.jsweb

// 解析cc文件模板
// fs模块主要用来读写文件
let fs = require('fs');
let path = require('path');

function loader(source) {
  // 1: 咱们先把定义好的'模板文件'读取出来.
  let template = fs.readFileSync(
    path.resolve(__dirname, './index.html'),
    'utf8' // 可能存在中文的
  );
  // 2: 去除空格, 这样能更好的匹配...
  source = source.replace(/\s+/g, ' ');
  // 3: 匹配出'css'样式
  let s = (/<style>(.*)<\/style>/gm.exec(source)||[])[1];
  // 4: 匹配出js代码
  let j = /<script>(.*)<\/script>/gm.exec(source)[1];
  // 5: 匹配出模板元素
  let t = /<template>(.*)<\/template>/gm.exec(source)[1];
  // 6: 注入模板元素
  template = template.replace(/(#--template-->)/, t);
  // 7: 注入样式, 防止出现undefined啥的...
  template = template.replace(/(#--style-->)/, `<style> ${s||''}</style>`);
  // 8: 把这个处理好的模板结构放入最后要执行的'html'文件中
  fs.writeFileSync(
    path.resolve(__dirname, '../public/index.html'),
    `${template}`,
    err => console.log(err)
  );
  // 9: 这里咱们把'js'代码继续导出, 这样其余文件引入咱们'.cc'文件其实就是引入了'.cc'文件的'js'脚本.
  return j;
}

module.exports = loader;

总体来讲仍是挺简易的, 刚开始作的时候想复杂了, 接下来咱们就来引用它.
cc_vue/src/index.js面试

import component from '../use/5:loader相助/index.cc';
//...
// 由于最后我导出的只有'js'脚本, 也就是'new'的时候的配置项, 因此直接进行下面的操做就能够.
new C(component);
二.生命周期函数的注入

生命周期这种东西面试总考, 可是说实话没有多神秘, 核心就是个回调函数而已.
本次我就作两个生命周期, 定义以下.segmentfault

  1. created: this实例上的数据处理完毕, 但此时获取不到dom.
  2. mounted: dom渲染到页面上, 总体流程结束时触发.
  3. 对于用户没传坐下兼容.

cc_vue/src/Compiler.js

class Compiler {
  constructor(el = '#app', vm) {
    this.vm = vm;
    // 1: 拿到真正的dom
    this.el = this.isElementNode(el) ? el : document.querySelector(el);
    // 2: 制做文档碎片
    let fragment = this.node2fragment(this.el);
    // 3: 解析元素, 文档流也是对象
    this.compile(fragment);
    // 4: 进行生命周期函数, 他真的一点都不高大上
    vm.$created && vm.$created.call(vm);

    // 最后一步: 处理完再放回去
    this.el.appendChild(fragment);
    // 调用声明周期钩子
    vm.$mounted && vm.$mounted.call(vm);
  }
 //...

cc_vue/use/5:loader相助/index.cc
完整的测试一下

<template>
  <div class="box"
       v-on:click='add'>
    <span>{{n}}</span>
  </div>
</template>
<script>
console.log('此处也可执行代码');
export default {
  el: "#app",
  data: {
    n: 2
  },
  methods: {
    add() {
      this.n++;
    }
  },
  created() {
    let d = document.getElementsByClassName("box");
    console.log("created", this.n, d);
  },
  mounted() {
    let d = document.getElementsByClassName("box");
    console.log("mounted", this.n, d);
  }
};
</script>
<style>
.box {
  border: 1px solid;
  height: 600px;
}
</style>

有兴趣的朋友能够测试一下, 本身作这些东西真的通有趣的.

三.计算属性的编写

计算属性属于很经常使用的功能了, 他的神奇之处在于其中任何一个值的变化都会引发结果的同步更新,下面我就来实现这种看起来很棒的效果.
cc_vue/src/index.js

class C {
  constructor(options) {
  //...
  // 把$computed代理到vm身上
    this.proxyVm(this.$computed, this, true);

具体的代理过程须要有所调整

proxyVm(data = {}, target = this, noRepeat = false) {
    for (let key in data) {
      if (noRepeat && target[key]) {
        // 防止data里面的变量名与其余属性重复
        throw Error(`变量名${key}重复`);
      }
      Reflect.defineProperty(target, key, {
        enumerable: true, // 描述属性是否会出如今for in 或者 Object.keys()的遍历中
        configurable: true, // 描述属性是否配置,以及能否删除
        get() {
          // 初版
          // 计算属性上的值确定是函数啊, 因此这里要进行一下判断
          // 由于这个for只走一层, 因此不会出现与内部值'重叠'的现象
          // 每次把this指向纠正
          if (this.$computed && this.$computed.hasOwnProperty(key)) {
            return data[key].call(target);
          } else {
            return Reflect.get(data, key);
          }
          // 第二版, f是新传进来的变量, 表明是否是函数类型
           return f ? data[key].call(target) : Reflect.get(data, key);
        },
        set(newVal) {
          if (newVal !== data[key]) {
            Reflect.set(data, key, newVal);
          }
        }
      });
    }
  }

我说下原理

  1. 若computed存在, 则他的取值方式变为执行'call'.
  2. 好比说我用到了'v'这个计算属性, 他的值是'return n+m',在行间调用它的时候{{v}}, 会走到CompileUtil.text这个函数, 这里有一步'new Watcher...'操做.
  3. 'new Watcher...'里面会去调用'getVal'函数, 拿到最新的变量来更新dom.
  4. 这个'new Watcher...'会被记录到对应的Dep里面, 'new'的过程当中'Dep.target' 会被赋值上这个'Watcher',也就是说之后当这个'v'有变化的时候, 会触发这个'new Watcher...'里面的更新操做.
  5. 'Watcher'被'new'的时候, 会传入'vm'与'expr表达式', 这个表达式执行的时候里面的全部'this.变量'会被加上标记, 因此才致使里面的任何变量的变化都会引发计算属性的变化.
  6. 好比出现{{n+m+v}}的状况, 其实我是把他们当作总体进行解析的, 因此这种状况下计算属性依然没问题.

下面是个人测试代码

<template>
  <div class="box">
    <button v-on:click='addn'> n++</button>
    <button v-on:click='addm'> m++</button>
    <p>n: {{n}}</p>
    <p>m: {{m}}</p>
    <p>x: {{x}}</p>
    <p>n+m+x: {{v}}</p>
    <p>v+v: {{v+v}}</p>
  </div>
</template>
<script>
export default {
  data: {
    n: 1,
    m: 1,
    x: 1
  },
  methods: {
    addn() {
      this.n++;
      console.log(this.v)
    },
    addm() {
      this.m++;
    }
  },
  computed: {
    v() {
      return this.n + this.m + this.x;
    }
  }
};
</script>
四.观察者的编写

既然写了'计算属性'那就顺手把观察属性一并完成把, 这个功能也挺有意思的, 咱们可使用这个属性对一个量进行'观察', 当这个量变化的时候触发咱们的函数, 同时传入两个参数新值与老值.

我说下思路:

  1. 既然是监控一个值, 那大概率应该是在双向绑定的时候进行监控.
  2. 此次先作基本功能, 像是'x.y.u.z'这种观察模式暂时不作.
  3. 指定this为vm的同时传入新值与旧值.

cc_vue/src/Observer.js

//...
// 新增init变量, 用来区别是否是第一层数据
// data = {name:'cc',type:['金毛','胖子']};
// name属于第一层数据, '金毛'属于第二层数据
  observer(data, init = false) {
    let type = toString.call(data),
      $data = this.defineReactive(data, init);
//...
defineReactive(data, init) {
  //...
  set(target, key, value) {
        if (target[key] !== value) {
          if (init) { // 对data的数据进行watch处理
            (_this.vm.$watch||{})[key] && // 先肯定有watch
              _this.vm.$watch[key].call(_this, value, target[key]);
          }
          target[key] = _this.observer(value);
          dep.notify();
        }
        return value;
      }
end

本次书写的功能都挺有意思的, 写的时候也很开心, 毕竟代码是让人快乐的东西, 下一期我要往框架里面加一些好玩的功能, 具体加什么还没肯定, 可是我比较喜欢一些搞怪的, 反正你们一块儿玩耍呗.

框架github
ui组件库
我的技术博客
更多文章,ui库的编写文章列表

相关文章
相关标签/搜索