一、Vue的数据驱动
我们最一般的使用Vue的步骤可能如下:
(1)编写模板(可能有以下几种方式)
-
直接在HTML中写标签
-
通过使用template
-
通过使用.vue文件(中的template)
(2)创建Vue实例
(3)把Vue挂载到页面中(mount)
而Vue是如何实现数据驱动的呢?
Vue首先会先获取到模板,模板中有着一些“坑”,即类似于{{message}}的东西,这些东西HTML是不认识的,但是Vue会把想要的数据填充进去,最后呈现出一个我们想要的页面。同时也应该初步了解到,在vue中,模板经过了以下几步处理:template(字符串模板) >>> 抽象语法树ast >>> VNode >>> 真实Dom。
二、手写数据驱动
1.写一个compiler()函数
接下来尝试用纯js代码来实现数据驱动的功能,它实现:对于如下的代码,
<div id="root">
<div>
<div>
<p>{{name}},{{message}}</p>
</div>
</div>
</div>
给定name以及message的数据,能够准确的将数据填充到我们想要的位置。
//为了实现这个功能,我们编写一个compiler()函数,它接受两个参数,一个是template,表示模板
//一个是data,表示数据对象,因此我们先给出一份数据
let data = {
name:'小红帽',
message:'喜欢采蘑菇'
};
function compiler( template, data){
let childNodes = template.childNodes; //获取模板内的子结点
for (let i = 0; i < childNodes.length; i++){
let type = childNodes[i].nodeType; //nodeType表示结点类型;返回值为3,代表文本结点,返回值为1,代表元素结点
if(type === 3){
let txt = childNodes[i].nodeValue;
let regex_hks = /\{\{(.+?)\}\}/g; //g表示全局匹配
txt = txt.replace( regex_hks , function ( _ ,g) { // replace()方法接受两个参数,一个是正则表达式,一个是匹配到结果后的处理函数
let path = g.trim(); // 其中第0个参数表示匹配到的内容,第n个参数表示匹配到的第n组
let value = data[path]; //每匹配到一个结果,该函数就会被调用,并返回一个替换该结果的值
return value; //全部替换完成,返回一个新的字符串
});
childNodes[i].nodeValue = txt;
}
else if(type === 1){
compiler( childNodes[i], data); //如果不是文本结点,递归(使用递归保证了即便是层级的html结构依然可以找到文本结点)
}
}
}
//在执行compiler()函数之前与之后,我们分别打印以下模板
//看一看有什么不同
let template = document.querySelector('#root');
console.log(template);
compiler(template,data);
console.log(template);
结果如下所示:
可以看到,compiler()函数确实成功的将数据填充到了模板中,但是我们会发现,在执行compiler()之前打印出的模板依然是被填充了之后的,这是为什么呢?
其实这里是因为 console.log()函数的缘故,该函数不是立即打印当前状态的值,而是打印最终的值,即惰性求值,这一点需要注意。
具体了解的话可以参考这篇博客:console.log()的惰性求值问题
2.修改一个小问题
我们上面的代码还有一个小问题,上面的功能如果我们用Vue来实现的话:
<div id="root">
<div>
<div>
<p>{{name}},{{message}}</p>
</div>
</div>
</div>
<script>
let template = document.querySelector('#root');
console.log( template );
let vm = new Vue({
el:'#root',
data:{
name:'小红帽',
message:'喜欢采蘑菇'
}
})
console.log( template );
</script>
则结果如下:
可以看到,在vue中,不管是前面打印,还是在vue执行之后打印模板,模板中的“坑”都没有被替换掉,这可能就值得我们思考了。
为什么vue没有替换模板,这是很显而易见的事情,如果替换了模板,那么我们的模板就不存在了,那如果以后模板里面的插值更改了,vue还该怎么去监听这个变化呢?因此,正确的做法是,将模板保留,将渲染后的模板重新拷贝一份,而不是直接覆盖上去,当然,vue在这个过程中还有一个vnode的操作,即虚拟结点,但此时我们先不关注该问题。
所以,应该有如下改动:
结果如下:
3.仿照vue写一个简单的数据驱动demo
在上面的代码中,我们只是用函数实现了将数据插入到html中的这一操作,但我们知道,Vue通过传入多个属性来构建vue实例,只需要传入数据以及el属性即可轻松完成该功能,接下来就实现这样一个简单的封装操作:
在这之前我们在控制台简单打印一个Vue实例,可以看到:
在Vue中,所有只读属性都以 $ 开头,而所有可读可写的内部属性一般以下划线 _ 开头,我们也沿用这个规则。
代码如下:
//仿照Vue的LnVue,实现简单的数据驱动
function LnVue(options) { //LnVue的构造函数
this._el = options.el;
this._data = options.data;
this.$el = this._template = document.querySelector(this._el);
this.render();
}
LnVue.prototype.render = function (){
this.compiler( this._template );
}
LnVue.prototype.compiler = function( template ){
//生成template的copy
let generateTemplate = template.cloneNode( true );
//调用compiler()函数
compiler(generateTemplate, this._data);
//替换$el
this.$el = generateTemplate;
//更新页面Dom
this.update( generateTemplate );
}
LnVue.prototype.update = function ( GTemplate ){
this._template.parentNode.replaceChild(GTemplate , this._template);
}
let vm = new LnVue({
el:'#root',
data:{
name:'小红帽',
message:'喜欢采蘑菇'
}
})
4.考虑到插值时的层级关系
在上面的示例中,我们只是使用了最基本的数据替换,没有考虑到层级关系所带来的影响,接下来考虑这种数据结构:
data:{
name:'小红帽',
message:'喜欢采蘑菇',
profile:{
name:'大灰狼',
grade:'三年级',
score:[98,89,92],
friends:{
name:'小红帽',
grade:'二年级'
}
}
}
显然,这种结构是要复杂了一些的,因为它嵌套着层级关系,如果我们在模板中这样使用这些数据:
<div id="root">
<div>
<div>
<p>{{name}},{{message}}</p>
<p>{{profile.name}},{{profile.grade}}</p>
<p>{{profile.name}}的朋友:{{profile.friends.name}},{{profile.friends.grade}}</p>
<p>{{profile.name}}的成绩:{{profile.score[0]}},{{profile.score[1]}},{{profile.score[2]}}</p>
</div>
</div>
</div>
那么显然,我们之前写的代码就会出现问题,因为之前我们是通过正则匹配{{}}中的内容,但是现在它匹配到的就是 profile.friends.name 之类的字符串,这种字符串显然直接找是找不到的,因此我们需要通过这种层级路径找到它的真实值,我们把这个功能封装成一个函数:
function getDatabyPath(data, path ) {
let paths = path.trim().split(/\.|\[|\]/); //分割层级路径以及带有[]的路径
let res = data;
let prop;
while( prop = paths.shift() ){ //从队首依次弹出数据
if(prop === '') continue; //解决分割中出现 '' 的问题(分割[?]时会出现该问题)
res = res[prop];
}
return res;
}
之后在compiler()函数中修改一下:
let path = g.trim();
let value = getDatabyPath( data, path);
然后应该就可以访问层级数据了。