( 第六篇 )仿写'Vue生态'系列___"模板loader与计算属性"
本次任务
- 编写'cc-loader', 使我们可以使用'xxx.cc'文件来运行我的框架代码.
- 为'cc_vue'添加生命周期函数.
- 新增'计算属性'.
- 新增'观察属性'.
一.'cc-loader'的定义与编写
- 本次只是编写一个最基础的版本, 后续完善组件化功能的时候, 会对它有所改动.
- 使'webpack'可以解析后缀为'cc'的文件.
- 必须做到非常的轻量.
让我们一步一步来做出这样一个'loader', 首先我先介绍一下文件结构, 在'src'文件夹平级建立'loader'文件夹, 里面可以存放以后我做的所有'loader'.
不可或缺的步骤就是定义'loader'的路径.
cc_vue/config/common.js
新增resolveLoader项
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.js
// 1: 'source'就是你读取到的文件的代码, 他是'string'类型.function loader(source) { // ..具体处理函数 // 2: 处理完了一定要把它返回出去, 因为可能还有其他'loader'要处理它, 或者是直接执行处理好的代码. return source;}module.exports = loader;
模板的定义
- 本次暂不处理title, 因为没啥技术含量暂时也没必要.
- '#--style-->' 会被替换为css的内容.
- '#--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'文件的书写方式
<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.js
{ test: /\.cc$/, use: ['cc-loader'] },
loader正式走起
cc_vue/loader/cc-loader.js
// 解析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);
二.生命周期函数的注入
生命周期这种东西面试总考, 但是说实话没有多神秘, 核心就是个回调函数而已.
本次我就做两个生命周期, 定义如下.
- created: this实例上的数据处理完毕, 但此时获取不到dom.
- mounted: dom渲染到页面上, 整体流程结束时触发.
- 对于用户没传坐下兼容.
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); } } }); } }
我说下原理
- 若computed存在, 则他的取值方式变为执行'call'.
- 比如说我用到了'v'这个计算属性, 他的值是'return n+m',在行间调用它的时候{{v}}, 会走到CompileUtil.text这个函数, 这里有一步'new Watcher...'操作.
- 'new Watcher...'里面会去调用'getVal'函数, 拿到最新的变量来更新dom.
- 这个'new Watcher...'会被记录到对应的Dep里面, 'new'的过程中'Dep.target' 会被赋值上这个'Watcher',也就是说以后当这个'v'有变化的时候, 会触发这个'new Watcher...'里面的更新操作.
- 'Watcher'被'new'的时候, 会传入'vm'与'expr表达式', 这个表达式执行的时候里面的所有'this.变量'会被加上标记, 所以才导致里面的任何变量的变化都会引起计算属性的变化.
- 比如出现{{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>
四.观察者的编写
既然写了'计算属性'那就顺手把观察属性一并完成把, 这个功能也挺有意思的, 我们可以使用这个属性对一个量进行'观察', 当这个量变化的时候触发我们的函数, 同时传入两个参数新值与老值.
我说下思路:
- 既然是监控一个值, 那大几率应该是在双向绑定的时候进行监控.
- 这次先做基本功能, 像是'x.y.u.z'这种观察模式暂时不做.
- 指定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库的编写文章列表