知识点
- 如何获取所有文本节点
defineProperty
和defineProperties
- 将字符串替换为参数变量
- 如何标记依赖的元素
题目
做一个小型的MVVM
库,可以做到数据和视图之间的自动同步。
你需要做的就是完成一个函数bindViewToData
,它接受一个DOM节点和一个对象data
作为参数。bindViewToData
会分析这个DOM节点下的所有文本节点,并且分析其中被{{
和}}
包裹起来的表达式,然后把其中的内容替换成在data
上下文执行该表达式的结果。例如:
<div id='app'>
<p>
My name is {{firstName + ' ' + lastName}}, I am {{age}} years old.
</p>
<div>
const appData = {
firstName: 'Lucy',
lastName: 'Green',
age: 13
}
bindViewToData(document.getElementById('app'), appData)
// div 里面的 p 元素的内容为
// My name is Lucy Green, I am 13 years old.
appData.firstName = 'Jerry'
appData.age = 16
// div 里面的 p 元素的内容自动变为
// My name is Jerry Green, I am 16 years old.
当数据改变的时候,会自动地把相应的表达式的内容重新计算并且插入文本节点。
实现
获取文本节点
对DOM的操作和属性又有点忘记了,获取子节点原来都使用的节点的children
属性,但是children
里面并没有文本节点,要使用的是childNodes
属性。
二者的最主要的区别就是:childNodes
会返回文本节点和空节点,而children
属性不会返回文本节点和空节点
所以这里我们要使用childNodes
属性,并且通过nodeName === '#text'
筛选文本节点(也可以使用nodeType === 3
),然后通过node.data
获取文本
然后通过递归筛选出所有的文本节点,这之前练习过的获取元素标签《前端练习20 DOM标签统计》是类似的,有几个方式
复习一下,这里是将所有文本节点放到一个数组中:
可以在递归调用时多传递一个变量参数来存储结果:
const getAllTextNode = (node, result = []) => {
if (node.nodeName === '#text') {
result.push(node.data.trim());
}
if (node.childNodes.length > 0) {
result.concat([...node.childNodes].map(childNode => getAllTextNode(childNode, result)))
}
return result
};
也可以直接递归,通过将数组返回值展平来实现:
const getAllTextNode = (node) => {
if (node.nodeName === '#text') {
return node.data.trim();
}
if (node.childNodes.length > 0) {
return [].concat(...[...node.childNodes].map(childNode => getAllTextNode(childNode)))
}
};
当然也可以通过reduce
来实现:
const getAllTextNode = (node) => {
if (node.nodeName === '#text') {
return node.data.trim();
}
if (node.childNodes.length > 0) {
return [...node.childNodes].reduce((total, current) => {
return total.concat(getAllTextNode(current))
}, []).filter(v => v)
}
};
实际使用的时候,不能只返回一个成员是字符串或者是节点的数组,还需要将节点的初始值存起来,否则的话替换一次之后就没有办法响应式的变化了
// 收集依赖
const collectDep = node => {
if (node.nodeName === '#text' && node.data.trim()) {
const str = node.data.trim();
if (str.includes('{{') && str.includes('}}')) {
return {
node: node,
originText: node.data
}
}
}
if (node.childNodes.length > 0) {
return [].concat(...[...node.childNodes].map(childNode => collectDep(childNode)).filter(v => v))
}
};
上面的方法我们返回值是一个数组,数组成员是对象,里面的originText
就是我们存的表达式初始值
使用defineProperty
实现响应式,使用了defineProperty
将data
里面每个属性的setter
和getter
劫持
注意defineProperty
和``defineProperties的
setter`方法传入的参数是不同的,实现的时候又搞混了
// 劫持data的setter,实现响应式更新
const keys = Object.keys(data);
if (keys.length > 0) {
keys.forEach(v => {
let value = data[v];
Object.defineProperty(data, v, {
get() {
return value
},
set(newValue) {
value = newValue;
update(depends, data);
return value;
}
})
});
}
将字符串替换为参数变量
这个方法和之前练习过的《前端练习28 执行任意表达式》是一样的,避免使用eval
和with
,使用new Function
来实现
// 替换字符串为变量
const replaceStr = (str, data) => {
return str.replace(/\{\{(.*?)\}\}/g, (match, p1) => {
const keys = Object.keys(data);
const values = keys.map(v => data[v]);
return new Function(...keys, `return ${p1}`)(...values)
})
};
标记依赖的元素
在Vue中,每个组件都有一个watch
对象,它就是用来观察属性的变化,并将每个属性的依赖关系
我总感觉我写的东西总是没有很好的设计感,总是没有面向对象的感觉,功能能实现,未来扩展困难,不优雅
这就是我现在最大的问题,实际上,我也没有遇到过将来要扩展的情况,写的代码都只是完成功能而已,太缺乏实践。
结果
最终我的实现:
const appData = {
firstName: 'Lucy',
lastName: 'Green',
age: 13
};
// 替换字符串为变量
const replaceStr = (str, data) => {
return str.replace(/\{\{(.*?)\}\}/g, (match, p1) => {
const keys = Object.keys(data);
const values = keys.map(v => data[v]);
return new Function(...keys, `return ${p1}`)(...values)
})
};
// 收集依赖
const collectDep = node => {
if (node.nodeName === '#text' && node.data.trim()) {
const str = node.data.trim();
if (str.includes('{{') && str.includes('}}')) {
return {
node: node,
originText: node.data
}
}
}
if (node.childNodes.length > 0) {
return [].concat(...[...node.childNodes].map(childNode => collectDep(childNode)).filter(v => v))
}
};
// 更新节点列表
const update = (nodeList, data) => {
if (Array.isArray(nodeList) && nodeList.length > 0) {
nodeList.forEach(v => {
v.node.data = replaceStr(v.originText, data)
})
}
};
const bindViewToData = (el, data) => {
// 收集依赖
let depends = collectDep(el);
// 将依赖中的变量进行替换
update(depends, data);
// 劫持data的setter,实现响应式更新
const keys = Object.keys(data);
if (keys.length > 0) {
keys.forEach(v => {
let value = data[v];
Object.defineProperty(data, v, {
get() {
return value
},
set(newValue) {
value = newValue;
update(depends, data);
return value;
}
})
});
}
};
总结
- 好多细节不确定,DOM操作的某些属性和方法也记不清了,估计手写实现够呛,必须一遍调试一遍写
- 没有很好的面向对象编程的思维,写的代码不优雅
- 多练多写吧