( 第六篇 )仿寫'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
本次書寫的功能都挺有意思的, 寫的時候也很開心, 畢竟代碼是讓人快樂的東西, 下一期我要往框架裏面加一些好玩的功能, 具體加什麼還沒確定, 但是我比較喜歡一些搞怪的, 反正大家一起玩耍唄.