前言
在上篇 拆解Vue2核心模块实现-简易模板解析中实现了对简易<template>模板支持,不过遗留一个组件节点还未实现,这篇就对组件来一个简易的实现。
需求拆解
- 全局组件,通过
Vue.use方式注册 - 局部组件,通过
components属性注册
局部组件
Vue 基类首先增加 props 和 components 初始化属性处理
class Vue {
constructor(options) {
const { data, el, props, template, components } = options || {};
this.$options = options || {};
this.$props = props;
this.$data = data;
this.$template = template;
this.$components = components;
this.$el = typeof el === "string" ? document.querySelector(el) : el;
// 增加props处理
this._proxyProps(this.$props);
this._proxyData(this.$data);
new Observer(this.$data);
new Compiler(this);
}
/**
* 将props属性代理到this上,data、 computed、methods的实现类似
* @param props
*/
_proxyProps(props) {
if (!isPlainObject(props)) {
return;
}
Object.keys(props).forEach((key) => {
Object.defineProperty(this, key, {
enumerableL: true, // 是否可枚举
configurable: true, // 是否可遍历
get() {
return props[key];
},
set(val) {
wranLog("禁止给props属性赋值");
},
});
});
}
/**
* 将data属性代理到this上
* @param data
*/
_proxyData(data) {
if (!isPlainObject(data)) {
return;
}
Object.keys(data).forEach((key) => {
if (this[key]) {
wranLog("data属性和props冲突");
return;
}
Object.defineProperty(this, key, {
enumerableL: true,
configurable: true,
get() {
return data[key];
},
set(val) {
if (val === data[key]) {
return;
}
data[key] = val;
},
});
});
}
}
Compiler 改造
增加 compileComponent 处理组件节点
class Compiler {
constructor(vm) {
this.el = vm.$el;
this.vm = vm;
this.ast = parser(vm.$template);
this.compile(this.el, this.ast);
}
/**
* 编译模板,处理节点
* @param el
*/
compile(parent, ast) {
const { children } = ast;
if (children) {
children.forEach((nodeData) => {
const { tagName } = nodeData;
let el;
if (!tagName) {
el = this.compileText(nodeData);
} else if (tagName != "fragment") {
const c = tagName.charAt(0);
if (/[A-Z]/.test(c)) {
el = this.compileComponent(nodeData);
} else {
el = this.compileElement(nodeData);
}
}
if (nodeData.children && nodeData.children.length) {
this.compile(el, nodeData);
}
if (el) {
requestAnimationFrame(() => {
parent.appendChild(el);
});
}
});
}
}
/**
* 编译元素节点
*/
compileElement(nodeData) {
const { tagName, attributes } = nodeData;
const node = document.createElement(tagName);
Array.from(attributes).forEach((attr) => {
let attrName = attr.name;
if (isDirective(attrName)) {
attrName = attrName.substr(2);
const key = attr.value;
this.update(node, key, attrName);
}
});
return node;
}
/**
* 编译组件
*/
compileComponent(nodeData) {
const { tagName, attributes } = nodeData;
let props = {}
const Com = this.vm.$components[tagName]
if(!Com) {
return
}
Array.from(attributes).forEach((attr) => {
let attrName = attr.name;
if (isDirective(attrName)) {
// attrName = attrName.substr(2);
// const key = attr.value;
// this.update(C, key, attrName);
} else {
props[attrName] = attr.value;
}
});
new Com({
el: this.el,
props
})
return
}
/**
* 更新操作
*/
update(node, key, attrName) {
const updateFn = this[`${attrName}Updater`];
updateFn && updateFn.call(this, node, this.vm[key], key);
}
/**
* v-text 指令
*/
textUpdater(node, value, key) {
if(node.nodeType) {
node.textContent = value;
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
} else {
node.replaceProps(Object.assign(node.$props,))
}
}
/**
* v-model 指令
*/
modelUpdater(node, value, key) {
if(node.nodeType) {
node.value = value;
new Watcher(this.vm, key, (newValue) => {
node.value = newValue;
});
node.addEventListener("input", (a) => {
this.vm[key] = node.value;
});
} else {
node
}
}
/**
* 文本节点,处理差值表达式
*/
compileText(nodeData) {
const { content } = nodeData;
const node = document.createTextNode(content);
// .:匹配任意字符,():提取内容
const reg = /\{\{(.+?)\}\}/g;
const value = content;
if (reg.test(value)) {
const key = RegExp.$1.trim();
node.textContent = value.replace(reg, this.vm[key]);
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
return node;
}
}
测试
index.js 改动如下
class Button extends Vue {
constructor(options) {
options.template = `<button>{{text}}</button>`
super(options)
}
}
new Vue({
el: "#app",
components: {Button},
template: `
<h1>template解析</h1>
静态文本节点
<div>{{msg}}</div>
<input v-model="msg" type="text" />
<Button text="按钮" />`,
data: {
msg: "msg",
},
});
运行结果,Button 组件正确渲染出来了:
全局组件
- Vue 基类增加以下方法
static use(component) {
if(!Vue.components) {
Vue.components = {}
}
Vue.components[component.name] = component
}
getComponents(name) {
return this.$components[name] || Vue.components[name]
}
- Compiler的compileComponent改造如下
compileComponent(nodeData) {
const { tagName, attributes } = nodeData;
let props = {}
const Com = this.vm.getComponents(tagName)
if(!Com) {
return
}
Array.from(attributes).forEach((attr) => {
let attrName = attr.name;
if (isDirective(attrName)) {
} else {
props[attrName] = attr.value;
}
});
new Com({
el: this.el,
props
})
return
}
测试
index.js 改动如下
class Empty extends Vue {
name = 'Empty'
constructor(options) {
options.template = `<div>Empty</div>`
super(options)
}
}
class Button extends Vue {
constructor(options) {
options.template = `<button>{{text}}</button>`
super(options)
}
}
Vue.use(Empty)
new Vue({
el: "#app",
components: {Button},
template: `
<h1>template解析</h1>
静态文本节点
<div>{{msg}}</div>
<input v-model="msg" type="text" />
<Button text="按钮" />
<Empty />`,
data: {
msg: "msg",
},
});
运行结果,全局组件 Empty 也正确渲染出来了:
props 优化
当前的 props 是静态的,需要支持 v-bind
增加 props 的处理
- 增加
props的Observer - 增加
replaceProp函数操作props改变
class Vue {
constructor(options) {
const { data, el, props, template, components } = options || {};
this.$options = options || {};
this.$props = props || {};
this.$data = data;
this.$template = template;
this.$components = components;
this.$el = typeof el === "string" ? document.querySelector(el) : el;
this._proxyProps(this.$props);
this._proxyData(this.$data);
new Observer(this.$props);
new Observer(this.$data);
new Compiler(this);
}
static use(component) {
if(!Vue.components) {
Vue.components = {}
}
Vue.components[component.name] = component
}
getComponents(name) {
return this.$components[name] || Vue.components[name]
}
// props 改变
replaceProp(key,value) {
this.$props[key] = value
}
/**
* 将props属性代理到this上,data、 computed、methods的实现类似
* @param props
*/
_proxyProps(props) {
if (!isPlainObject(props)) {
return;
}
Object.keys(props).forEach((key) => {
Object.defineProperty(this, key, {
enumerableL: true, // 是否可枚举
configurable: true, // 是否可遍历
get() {
return props[key];
},
set(val) {
wranLog("禁止给props属性赋值");
},
});
});
}
/**
* 将data属性代理到this上
* @param data
*/
_proxyData(data) {
if (!isPlainObject(data)) {
return;
}
Object.keys(data).forEach((key) => {
if (this[key]) {
wranLog("data属性和props冲突");
return;
}
Object.defineProperty(this, key, {
enumerableL: true,
configurable: true,
get() {
return data[key];
},
set(val) {
if (val === data[key]) {
return;
}
data[key] = val;
},
});
});
}
}
Compiler 增加 v-bind 和 props属性支持
主要是 compileComponent 和 compileText 的修改,增加 bindUpdater 处理 v-bind 指令
class Compiler {
constructor(vm) {
this.el = vm.$el;
this.vm = vm;
this.ast = parser(vm.$template);
this.compile(this.el, this.ast);
}
/**
* 编译模板,处理节点
* @param el
*/
compile(parent, ast) {
const { children } = ast;
if (children) {
children.forEach((nodeData) => {
const { tagName } = nodeData;
let el;
if (!tagName) {
el = this.compileText(nodeData);
} else if (tagName != "fragment") {
const c = tagName.charAt(0);
if (/^[A-Z]/.test(c)) {
el = this.compileComponent(parent,nodeData);
} else {
el = this.compileElement(nodeData);
}
}
if (nodeData.children && nodeData.children.length) {
this.compile(el, nodeData);
}
if (el) {
requestAnimationFrame(() => {
parent.appendChild(el);
});
}
});
}
}
/**
* 编译元素节点
*/
compileElement(nodeData) {
const { tagName, attributes } = nodeData;
const node = document.createElement(tagName);
Array.from(attributes).forEach((attr) => {
let attrName = attr.name;
if (isDirective(attrName)) {
attrName = attrName.substr(2);
const key = attr.value;
this.update(node, key, attrName);
}
});
return node;
}
/**
* 编译组件
*/
compileComponent(parent,nodeData) {
const { tagName, attributes } = nodeData;
const Com = this.vm.getComponents(tagName)
if(!Com) {
return
}
const C = new Com({
el:parent
})
Array.from(attributes).forEach((attr) => {
let attrName = attr.name;
if (isBinding(attrName)) {
if (isDirective(attrName)) {
attrName = attrName.substr(2);
} else {
attrName = attrName.substr(1);
}
const key = attr.value;
this.bindUpdater(C, key,attrName);
} else {
C.replaceProp(attrName, attr.value)
}
});
return
}
/**
* 更新操作
*/
update(node, key, attrName) {
const updateFn = this[`${attrName}Updater`];
updateFn && updateFn.call(this, node, this.vm[key], key);
}
/**
* 组件动态props
*/
bindUpdater(Com,key,propsKey) {
let that = this.vm
if(inObject(this.vm.$props,key)) {
that = this.vm.$props
}
new Watcher(that, key, (newValue) => {
Com.replaceProp(propsKey,newValue)
});
Com.replaceProp(propsKey,that[key])
}
/**
* v-text 指令
*/
textUpdater(node, value, key) {
node.textContent = value;
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
/**
* v-model 指令
*/
modelUpdater(node, value, key) {
node.value = value;
new Watcher(this.vm, key, (newValue) => {
node.value = newValue;
});
node.addEventListener("input", (a) => {
this.vm[key] = node.value;
});
}
/**
* 文本节点,处理差值表达式
*/
compileText(nodeData) {
const { content } = nodeData;
const node = document.createTextNode(content);
// .:匹配任意字符,():提取内容
const reg = /\{\{(.+?)\}\}/g;
const value = content;
if (reg.test(value)) {
const key = RegExp.$1.trim();
node.textContent = value.replace(reg, this.vm[key]);
let that = this.vm
if(inObject(this.vm.$props,key)) {
that = this.vm.$props
}
new Watcher(that, key, (newValue) => {
node.textContent = newValue;
});
}
return node;
}
}
最终效果
index.js 改为:
class Empty extends Vue {
name = 'Empty'
constructor(options) {
options.props = {text: '空'}
options.template = `<div>{{text}}</div>`
super(options)
}
}
class Button extends Vue {
constructor(options) {
options.props = {text: ''}
options.template = `<button>{{text}}</button>`
super(options)
}
}
Vue.use(Empty)
new Vue({
el: "#app",
components: {Button},
template: `
<h1>template解析</h1>
静态文本节点
动态节点<div>{{msg}}</div>
v-model: <input v-model="msg" type="text" />
<div>
v-bind:<Button :text="msg" />
</div>
组件静态props:<Empty text="暂无数据" />`,
data: {
msg: "",
},
});
运行测试如图:
本篇相关改动代码
- Vue基类代码地址:toy-vue(3):vue-base
- 在线预览地址:toy-vue(3):demo
总结
本篇简单实现了组件支持,包括全局和局部注册,为了支持父传子,增加了 props 对应处理