更新记录
4.27 新增 computed 属性嵌套调用
4.26 更新了界面布局 新增利用 promise 实现 $nextTick
4.25 新增了v-if 的简单实现
总体架构
完整代码
先 copy 测试 欢迎纠错 吐槽~🐰
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<style></style>
<div
id="app"
style="width:100vw;border:1px solid red;position:relative"
>
<h1>{{msg}}</h1>
<h2>
<h3>我是普通模板编译:</h3>
<h4>我的最大年龄是<span style="color:red">{{age.max}}</span></h4>
<h4>我的最小年龄是<span style="color:red">{{age.min}}</span></h4>
</h2>
<h2>
<span>我是计算属性:</span>
<span style="color: aqua;">{{doubleAge}}</span>
</h2>
<h2>
<span>我是v-if的实现:</span>
<span v-if="alive" style="color:red">我还活着!!!....</span>
<hr />
<span v-if="dead" style="color:red">我挂了!!!....</span>
</h2>
<h2>
<span>我是click事件的实现,点击可以修改最大年龄为 200 岁:</span>
<button @click="beBiggerAge">修改最大年龄</button>
</h2>
<h2>
<span>我是click事件的实现,点击可以修改最小年龄为 0 岁 就挂了....</span>
<button @click="beMinnerAge">修改最小年龄</button>
</h2>
<h2>
<span
>我是v-model 的实现,可以修改最小年龄,如果输入到 0 岁
我就挂了...</span
>
<input type="text" v-model="age.min" />
</h2>
</div>
</body>
</html>
<script>
const PENDGING = 'PENDGING';
const FULFILLED = 'FULFILLED';
class Vue {
constructor(options) {
this.$options = options;
this.callbacks = [];
this.status = PENDGING;
this._init();
}
_init() {
this.$data = this.initData();
this.$methods = this.$options.methods;
this.$computed = this.$options.computed;
this.$watch = this.$options.watch;
new Observer(this.$data);
this.proxyData(this.$data);
this.proxyData(this.$methods);
this.proxyComputed(this.$computed);
this._watch();
this.$options.created.apply(this);
if (this.el) this.$options.$mount(this.el);
this.$options.mounted.apply(this);
}
//借鉴 promise 实现 nextTick 暂时没有做降级处理
$nextTick(cb) {
this.callbacks.push(cb);
//保证只能执行一次回调数组
if (this.status === PENDGING) {
executor.call(this);
this.status = FULFILLED;
}
const executor = () => {
Promise.resolve().then(() => {
this.status = PENDGING;
this.callbacks.forEach((cb) => cb());
});
};
}
_watch() {
Object.keys(this.$watch).forEach((key) => {
new Watcher(key, this, (newValue, oldValue) => {
this.$watch[key].call(this, newValue, oldValue);
});
});
}
//代理 computed 属性
proxyComputed(proxy) {
Object.keys(proxy).forEach((key) => {
Object.defineProperty(this, key, {
get() {
return proxy[key].call(this);
},
set(newValue) {
throw new Error('computed 属性不允许改变');
},
});
});
}
//代理this 使得可以直接访问 this.data this.method
proxyData(proxy) {
Object.keys(proxy).forEach((key) => {
Object.defineProperty(this, key, {
get() {
return proxy[key];
},
set(newValue) {
if (newValue !== proxy[key]) proxy[key] = newValue;
},
});
});
}
//初始化 data
initData() {
const type = typeof this.$options.data;
return type === 'function' ? this.$options.data() : this.$options.data;
}
//挂载 element
$mount(el) {
if (typeof el === 'string') this.$el = document.querySelector(el);
else if (el.nodeType === 1) this.$el = el;
else throw new Error('节点错误');
new Compiler(this.$el, this);
}
}
class Compiler {
constructor(el, vm) {
this.$vm = vm;
this.$el = el;
let fragment = this.vNodeFragment(el);
this.compilerFragment(fragment);
this.$el.appendChild(fragment);
}
vNodeFragment(el) {
let fragment = document.createDocumentFragment(); //创建文档碎片
while (el.firstChild) fragment.appendChild(el.firstChild);
return fragment;
}
compilerFragment(fragment) {
const childNodes = fragment.childNodes;
childNodes.forEach((node) => {
if (node.nodeType === 1) {
this.compileElement(node);
this.compilerFragment(node);
} else {
this.compileText(node);
}
});
}
compileText(node) {
let reg = /\{\{(.+?)\}\}/g;
let text = node.textContent;
if (reg.test(text)) {
let variable = CompilerUtils.getTextVariable(text);
const isComputed = this.$vm.$computed[variable];
//编辑 computed 属性
if (isComputed) {
CompilerUtils.computed(variable, node, this.$vm);
// console.log(isComputed, variable)
} else {
CompilerUtils.text(variable, node, this.$vm);
}
}
}
compileElement(node) {
const attrs = [...node.attributes];
attrs.forEach((attr) => {
const name = attr.name;
if (name.includes('v-')) {
//处理指令
const value = attr.value;
const [, type] = name.split('v-');
CompilerUtils[type](value, node, this.$vm);
} else if (name.includes('@')) {
//处理事件
const [, event] = name.split('@');
const method = attr.value;
// console.log(method, event, node)
CompilerUtils['addEvent'](method, event, node, this.$vm);
}
});
}
}
//计算属性
class ComputedWathcer {
constructor(variable, vm, cb) {
this.variable = variable;
this.vm = vm;
this.cb = cb;
this.value = this.getValue();
}
getValue() {
Dep.target = this;
let value = this.vm.$computed[this.variable].call(this.vm);
Dep.target = null;
return value;
}
update() {
let newValue = this.vm.$computed[this.variable].call(this.vm);
if (newValue !== this.value) {
this.value = newValue;
this.cb(newValue, this.value);
}
}
}
//模板编译工具
const CompilerUtils = {
domUpdater(node, { nextNode, newNode, parentNode }, exist, fromWatch) {
if (exist) {
fromWatch && parentNode.insertBefore(newNode, nextNode);
} else {
node.remove();
}
},
//暂时只实现了计算属性
if(variable, node, vm) {
//此时还没有模板编译完 依赖于 parentNode 所以需要执行一个异步
setTimeout(() => {
const fn = this.domUpdater;
const nextNode = node.nextElementSibling;
const newNode = node.cloneNode(true);
const parentNode = node.parentElement;
let otherNode = { nextNode, newNode, parentNode };
let computedIns = new ComputedWathcer(variable, vm, (nv, ov) => {
fn && fn(node, otherNode, nv, true);
if (nv) node = otherNode.newNode; //需要手动更新 node
});
fn && fn(node, otherNode, computedIns.value);
});
},
addEvent(method, event, node, vm) {
node.addEventListener(event, (...args) => {
vm[method].apply(vm, args);
});
},
textUpdater(node, value) {
node.textContent = value;
},
computed(variable, node, vm) {
let fn = this.textUpdater;
let computedIns = new ComputedWathcer(variable, vm, (nv, ov) => {
fn && fn(node, nv);
});
fn && fn(node, computedIns.value);
},
text(variable, node, vm) {
let fn = this.textUpdater;
let value = this.getValue(variable, vm);
new Watcher(variable, vm, (newValue) => {
fn && fn(node, newValue);
});
fn && fn(node, value);
},
getTextVariable(variable) {
let reg = /\{\{(.+?)\}\}/g;
let res = variable.replace(reg, ($0, $1) => $1);
return res;
},
getValue(variable, vm) {
return variable.split('.').reduce((prev, next) => {
return prev[next];
}, vm.$data);
},
setValue(variable, vm, newValue) {
const keys = variable.split('.');
const len = keys.length;
keys.reduce((prev, next, index) => {
if (index === len - 1) prev[next] = newValue;
return prev[next];
}, vm.$data);
},
inputUpdater(node, value) {
node.value = value;
},
model(variable, node, vm) {
const value = this.getValue(variable, vm);
const fn = this.inputUpdater;
fn && fn(node, value);
node.addEventListener('input', (event) => {
const newValue = event.target.value;
if (newValue !== value) this.setValue(variable, vm, newValue);
});
new Watcher(variable, vm, (newValue) => {
fn && fn(node, newValue);
});
},
};
//劫持数据 双向绑定
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
if (!data || typeof data !== 'object') return;
Object.keys(data).forEach((key) => {
this.defineReactive(key, data[key], data);
if (typeof data[key] === 'object') this.observe(data[key]);
});
}
defineReactive(key, value, data) {
let dep = new Dep();
let _this = this;
Object.defineProperty(data, key, {
get() {
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue) {
if (newValue !== value) {
value = newValue;
_this.observe(newValue);
dep.notify();
}
},
});
}
}
class Dep {
constructor() {
this.subs = [];
}
addSub(wathcer) {
this.subs.push(wathcer);
}
notify() {
this.subs.forEach((w) => w.update());
}
}
class Watcher {
constructor(variable, vm, cb) {
this.variable = variable;
this.vm = vm;
this.cb = cb;
this.value = this.get();
}
get() {
Dep.target = this;
const value = CompilerUtils.getValue(this.variable, this.vm);
Dep.target = null;
return value;
}
update() {
let newValue = CompilerUtils.getValue(this.variable, this.vm);
let oldValue = this.value;
if (newValue !== oldValue) {
this.value = newValue;
this.cb(newValue, oldValue);
}
}
}
new Vue({
data() {
return {
msg: '第一次测试',
name: 'mike',
age: {
max: 100,
min: 10,
},
};
},
watch: {
msg(newV, oldV) {
console.log(newV, oldV);
},
},
computed: {
dead() {
return !this.alive;
},
alive() {
return +this.age.min > 0;
},
doubleAge() {
return `你好我是mike,具备双倍年龄, 今年${this.age.max * 2}岁。`;
},
},
methods: {
beMinnerAge() {
this.age.min = 0;
},
beBiggerAge() {
this.age.max = 200;
},
},
created() {
this.msg = 'created 已创建';
console.log('created');
},
mounted() {
console.log('mounted');
},
}).$mount('#app');
</script>