学习Vue源码(一) 仿照vue实现数据驱动功能



一、Vue的数据驱动

我们最一般的使用Vue的步骤可能如下:

(1)编写模板(可能有以下几种方式)

  1. 直接在HTML中写标签

  2. 通过使用template

  3. 通过使用.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);

然后应该就可以访问层级数据了。


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值