vue博客网站源码分享 vue框架搭建个人博客网站模板

大家好,如果您还对vue博客网站源码分享不太了解,没有关系,今天就由本站为大家分享vue博客网站源码分享的知识,包括vue框架搭建个人博客网站模板的问题都会给大家分析到,还望可以解决大家的问题,下面我们就开始吧!

作者|爱编程的小和尚

责编|王晓曼

出品|CSDN博客

学过VUE如果不了解响应式的原理,怎么能说自己熟练使用VUE,要是没有写过一个简易版的VUE怎么能说自己精通VUE,这篇文章通过300多行代码,带你写一个简易版的VUE,主要实现VUE数据响应式(数据劫持结合发布者-订阅者)、数组的变异方法、编译指令,数据的双向绑定的功能。

本文需要有一定VUE基础,并不适合新手学习。

文章较长,且有些难度,建议大家,找一个安静的环境,并在看之前沐浴更衣,保持编程的神圣感。下面是实现的简易版VUE的源码地址,一定要先下载下来!因为文章中的并非全部的代码。

Github源码地址:https://github.com/young-monk/myVUE.git

前言

在开始学习之前,我们先来了解一下什么是MVVM,什么是数据响应式。

我们都知道VUE是一个典型的MVVM思想,由数据驱动视图。

那么什么是MVVM思想呢?

MVVM是Model-View-ViewModel,是把一个系统分为了模型(model)、视图(view)和view-model三个部分。

VUE在MVVM思想下,view和model之间没有直接的联系,但是view和view-model、model和view-model之间时交互的,当view视图进行dom操作等使数据发生变化时,可以通过view-model同步到model中,同样的model数据变化也会同步到view中。

那么实现数据响应式都有什么方法呢?1、发布者-订阅者模式:当一个对象(发布者)状态发生改变时,所有依赖它的对象(订阅者)都会得到通知。通俗点来讲,发布者就相当于报纸,而订阅者相当于读报纸的人。2、脏值检查:通过存储旧的数据,和当前新的数据进行对比,观察是否有变更,来决定是否更新视图。angular.js就是通过脏值检查的方式。最简单的实现方式就是通过setInterval定时轮询检测数据变动,但这样无疑会增加性能,所以,angular只有在指定的事件触发时进入脏值检测。3、数据劫持:通过Object.defineProperty来劫持各个属性的setter,getter,在数据变动时触发相应的方法。VUE是如何实现数据响应式的呢?

VUE.js则是通过数据劫持结合发布者-订阅者模式的方式。

当执行newVUE时,VUE就进入了初始化阶段,VUE会对指令进行解析(初始化视图,增加订阅者,绑定更新函数),同时通过Obserber会遍历数据并通过Object.defineProperty的getter和setter实现对的监听,当数据发生变化的时候,Observer中的setter方法被触发,setter会立即调用Dep.notify,Dep开始遍历所有的订阅者,并调用订阅者的update方法,订阅者收到通知后对视图进行相应的更新。

我来依次介绍一下图中的重要的名词:1、Observer:数据监听器,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者,内部采用Object.defineProperty的getter和setter来实现2、Compile:指令解析器,它的作用对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数3、Dep:订阅者收集器或者叫消息订阅器都可以,它在内部维护了一个数组,用来收集订阅者,当数据改变触发notify函数,再调用订阅者的update方法4、Watcher:订阅者,它是连接Observer和Compile的桥梁,收到消息订阅器的通知,更新视图5、Updater:视图更新所以我们想要实现一个VUE响应式,需要完成数据劫持、依赖收集、发布者订阅者模式。下面我来介绍我模仿源码实现的功能:

1、数据的响应式、双向绑定,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者

2、解析VUE常用的指令v-html,v-text,v-bind,v-on,v-model,包括(@和:)

3、数组变异方法的处理

4、在VUE中使用this访问或改变data中的数据

我们想要完成以上的功能,需要实现如下类和方法:

1、实现Observer类:对所有的数据进行监听

2、实现array工具方法:对变异方法的处理

3、实现Dep类:维护订阅者

4、实现Watcher类:接收Dep的更新通知,用于更新视图

5、实现Compile类:用于对指令进行解析

6、实现一个CompileUtils工具方法,实现通过指令更新视图、绑定更新函数Watcher

7、实现this.data代理:实现对this.data代理:实现对this.data代理:实现对this.data代理,可以直接在VUE中使用this获取当前数据

我是使用了webpack作为构建工具来协同开发的,所以在我实现的VUE响应式中会用到ES6模块化,webpack的相关知识。

实现Observer类

我们都知道要用Obeject.defineProperty来监听属性的数据变化,我们需要对Observer的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter,这样的话,当给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。当然我们在新增加数据的时候,也要对新的数据对象进行递归遍历,加上setter和getter。

但我们要注意数组,在处理数组时并不是把数组中的每一个元素都加上setter和getter,我们试想一下,一个从后端返回的数组数据是非常庞大的,如果为每个属性都加上setter和getter,性能消耗是十分巨大的。我们想要得到的效果和所消耗的性能不成正比,所以在数组方面,我们通过对数组的7个变异方法来实现数据的响应式。只有通过数组变异方法来修改和删除数组时才会重新渲染页面。

那么监听到变化之后是如何通知订阅者来更新视图的呢?我们需要实现一个Dep(消息订阅器),其中有一个notify方法,是通知订阅者数据发生了变化,再让订阅者来更新视图。

我们怎么添加订阅者呢?我们可以通过newDep,通过Dep中的addSaubs方法来添加订阅者。我们来看一下具体代码。

我们首先需要声明一个Observer类,在创建类的时候,我们需要创建一个消息订阅器,判断一下是否是数组,如果是数组,我们便改造数组,如果是对象,我们便需要为对象的每一个属性都加入setter和getter。

import{arrayMethods}from’./array’//数组变异方法处理classObserver{constructor(data){//用于对数组进行处理,存放数组的观察者watcherthis.dep=newDepif(Array.isArray(data)){//如果是数组,使用数组的变异方法data.__proto__=arrayMethods//把数组数据添加__ob__一个Observer,当使用数组变异方法时,可以更新视图data.__ob__=this//给数组的每一项添加数据劫持(setter/getter处理)this.observerArray(data)}else{//非数组数据添加数据劫持(setter/getter处理)this.walk(data)}}}

在上面,我们给data的__proto__原型链重新赋值,我们来看一下arrayMethods是什么,arrayMethods是array.js文件中,抛出的一个新的Array原型:

//获取Array的原型链constarrayProto=Array.prototype;//重新创建一个含有对应原型的对象,在下面称为新ArrayconstarrayMethods=Object.create(arrayProto);//处理7个数组变异方法[‘push’,’pop’,’shift’,’unshift’,’reverse’,’sort’,’splice’].forEach(ele=>{//修改新Array的对应的方法arrayMethods[ele]=function{//执行数组的原生方法,完成其需要完成的内容arrayProto[ele].call(this,…arguments)//获取Observer对象constob=this.__ob__//更新视图ob.dep.notify}})export{arrayMethods}

此时呢,我们就拥有了数组的变异方法,我们还需要通过observerArray方法为数组的每一项添加getter和setter,注意,此时的每一项只是最外面的一层,并非递归遍历。

//循环遍历数组,为数组每一项设置setter/getterobserverArray(items){for(leti=0;i<items.length;i++){this.observer(items[i])}}

如果是一个对象的话,我们就要对对象的每一个属性递归遍历,通过walk方法:

walk(data){//数据劫持if(data&&typeofdata===”object”){for(constkeyindata){//绑定setter和getterthis.defineReactive(data,key,data[key])}}}

在上面的调用了defineReactive,我们来看看这个方法是干什么的?这个方法就是设置数据劫持的,每一行都有注释。

//数据劫持,设置setter/getteerdefineReactive(data,key,value){//如果是数组的话,需要接受返回的Observer对象letarrayOb=this.observer(value)//创建订阅者/收集依赖constdep=newDep//setter和getter处理Object.defineProperty(data,key,{//可枚举的enumerable:true,//可修改的configurable:false,get{//当Dep有watcher时,添加watcherDep.target&&dep.addSubs(Dep.target)//如果是数组,则添加上数组的观察者Dep.target&&arrayOb&&arrayOb.dep.addSubs(Dep.target)returnvalue},set:(newVal)=>{//新旧数据不相等时更改if(value!==newVal){//为新设置的数据添加setter/getterarrayOb=this.observer(newVal);value=newVal//通知dep数据发送了变化dep.notify}}})}}

我们需要注意的是,在上面的图解中,在Observer中,如果数据发生变化,会通知消息订阅器,那么在何时绑定消息订阅器呢?就是在设置setter和getter的时候,创建一个Dep,并为Dep添加订阅者,Dep.target&&dep.addSubs(Dep.target),通过调用dep的addSubs方法添加订阅者。

实现Dep

Dep是消息订阅器,它的作用就是维护一个订阅者数组,当数据发送变化是,通知对应的订阅者,Dep中有一个notify方法,作用就是通知订阅者,数据发送了变化:

//订阅者收集器exportdefaultclassDep{constructor{//管理的watcher的数组this.subs=}addSubs(watcher){//添加watcherthis.subs.push(watcher)}notify{//通知watcher更新domthis.subs.forEach(w=>w.update)}}

实现watcher

Watcher就是订阅者,watcher是Observer和Compile之间通信的桥梁,当数据改变时,接收到Dep的通知(Dep的notify()方法),来调用自己的update方法,触发Compile中绑定的回调,达到更新视图的目的。

importDepfrom’./dep’import{complieUtils}from’./utils’exportdefaultclassWatcher{constructor(vm,expr,cb){//当前的vue实例this.vm=vm;//表达式this.expr=expr;//回调函数,更新domthis.cb=cb//获取旧的数据,此时获取旧值的时候,Dep.target会绑定上当前的thisthis.oldVal=this.getOldVal}getOldVal{//将当前的watcher绑定起来Dep.target=this//获取旧数据constoldVal=complieUtils.getValue(this.expr,this.vm)//绑定完成后,将绑定的置空,防止多次绑定Dep.target=returnoldVal}update{//更新函数constnewVal=complieUtils.getValue(this.expr,this.vm)if(newVal!==this.oldVal||Array.isArray(newVal)){//条用更新在compile中创建watcher时传入的回调函数this.cb(newVal)}}}

上面中用到了ComplieUtils中的getValue方法,会在下面讲,主要作用是获取到指定表达式的值。

我们把整个流程分成两条路线的话:

newVUE==>Observer数据劫持==>绑定Dep==>通知watcher==>更新视图newVUE==>Compile解析模板指令==>初始化视图和绑定watcher

此时,我们第一条线的内容已经实现了,我们再来实现一下第二条线。

实现Compile

Compile主要做的事情是解析模板指令,将模板中的变量替换成数据,初始化渲染页面视图。同时也要绑定更新函数,添加订阅者。

因为在解析的过程中,会多次的操作dom,为提高性能和效率,会先将VUE实例根节点的el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中。

classComplie{constructor(el,vm){this.el=this.isNodeElement(el)?el:document.querySelector(el);this.vm=vm;//1、将所有的dom对象放到fragement文档碎片中,防止重复操作dom,消耗性能constfragments=this.nodeTofragments(this.el)//2、编译模板this.complie(fragments)//3、追加子元素到根元素this.el.appendChild(fragments)}}

我们可以看到,Complie中主要进行了三步,第一步nodeTofragments是讲所有的dom节点放到文档碎片中操作,最后一步,是把解析好的dom元素,从文档碎片重新加入到页面中,这两步的具体方法,大家去下载我的源码,看一下就明白了,有注释。我就不再解释了。

我们来看一下第二步,编译模板:

complie(fragments){//获取所有节点constnodes=fragments.childNodes;[…nodes].forEach(ele=>{if(this.isNodeElement(ele)){//1.编译元素节点this.complieElement(ele)}else{//编译文本节点this.complieText(ele)}//如果有子节点,循环遍历,编译指令if(ele.childNodes&&ele.childNodes.length){this.complie(ele)}})}

我们要知道,模板可能有两种情况,一种是文本节点(含有双大括号的插值表达式)和元素节点(含有指令)。我们获取所有节点后对每个节点进行判断,如果是元素节点,则用解析元素节点的方法,如果是文本节点,则调用解析文本的方法。

complieElement(node){//1.获取所有的属性constattrs=node.attributes;//2.筛选出是属性的[…attrs].forEach(attr=>{//attr是一个对象,name是属性名,value是属性值const{name,value}=attr//判断是否含有v-开头如:v-htmlif(name.startsWith(“v-“)){//将指令分离text,html,on:clickconst[,directive]=name.split(“-“)//处理on:click或bind:name的情况on,clickconst[dirName,paramName]=directive.split(“:”)//编译模板complieUtils[dirName](node,value,this.vm,paramName)//删除属性,在页面中的dom中不会再显示v-html这种指令的属性node.removeAttribute(name)}elseif(name.startsWith(“@”)){//如果是事件处理@click=’handleClick’let[,paramName]=name.split(‘@’);complieUtils[‘on’](node,value,this.vm,paramName);node.removeAttribute(name);}elseif(name.startsWith(“:”)){//如果是事件处理:href=’…’let[,paramName]=name.split(‘:’);complieUtils[‘bind’](node,value,this.vm,paramName);node.removeAttribute(name);}})}

我们在编译模板中调用了complieUtils[dirName](node,value,this.vm,paramName)方法,这是工具类中的一个方法,用于处理指令。

我们再来看看文本节点,文本节点就相对比较简单,只需要匹配{{}}形式的插值表达式就可以了,同样的调用工具方法,来解析。

complieText(node){//1.获取所有的文本内容consttext=node.textContent//匹配{{}}if(/\\{\\{(.+?)\\}\\}/.test(text)){//编译模板complieUtils[‘text’](node,text,this.vm)}}

上面用来这么多工具方法,我们来看看到底是什么。

实现ComplieUtils工具方法

这个方法主要是对指令进行处理,获取指令中的值,并在页面中更新相应的值,同时我们在这里要绑定watcher的回调函数。

我来以v-text指令来解释,其他指令都有注释,大家自己看。

importWatcherfrom’./watcher’exportconstcomplieUtils={//处理text指令text(node,expr,vm){letvalue;if(/\\{\\{.+?\\}\\}/.test(expr)){//处理{{}}value=expr.replace(/\\{\\{(.+?)\\}\\}/g,(…args)=>{//绑定观察者/更新函数newWatcher(vm,args[1],=>{//第二个参数,传入回调函数this.updater.updaterText(node,this.getContentVal(expr,vm))})returnthis.getValue(args[1],vm)})}else{//v-textnewWatcher(vm,expr,(newVal)=>{this.updater.updaterText(node,newVal)})//获取到value值value=this.getValue(expr,vm)}//调用更新函数this.updater.updaterText(node,value)},}

Text处理函数是对dom元素的TextContent进行操作的,所以有两种情况,一种是使用v-text指令,会更新元素的textContent,另一种情况是{{}}的插值表达式,也是更新元素的textContent。

在此方法中我们先判断是哪一种情况,如果是v-text指令,那么就绑定一个watcher的回调,获取到textContent的值,调用updater.updaterText在下面讲,是更新元素的方法。如果是双大括号的话,我们就要对其进行特殊处理,首先是将双大括号替换成指定的变量的值,同时为其绑定watcher的回调。

//通过表达式,vm获取data中的值,person.namegetValue(expr,vm){returnexpr.split(“.”).reduce((data,currentVal)=>{returndata[currentVal]},vm.$data)},

获取textContent的值是用一个reduce函数,用法在最后面的链接中,因为数据可能是person.name我们需要获取到最深的对象的值。

//更新dom元素的方法updater:{//更新文本updaterText(node,value){node.textContent=value}}

updater.updaterText更新dom的方法,其实就是对textContent重新赋值。

我们再来将一下v-model指令,实现双向的数据绑定,我们都知道,v-model其实实现的是input事件和value之间的语法糖。所以我们这里同样的监听一下当前dom元素的input事件,当数据改变时,调用设置新值的方法:

//处理model指令model(node,expr,vm){constvalue=this.getValue(expr,vm)//绑定watchernewWatcher(vm,expr,(newVal)=>{this.updater.updaterModel(node,newVal)})//双向数据绑定node.addEventListener(“input”,(e)=>{//设值方法this.setVal(expr,vm,e.target.value)})this.updater.updaterModel(node,value)},

这个方法同样是通过reduce方法,为对应的变量设置成新的值,此时数据改变了,会自动调用更新视图的方法,我们在之前已经实现了。

//通过表达式,vm,输入框的值,实现设置值,input中v-model双向数据绑定setVal(expr,vm,inputVal){expr.split(“.”).reduce((data,currentVal)=>{data[currentVal]=inputVal},vm.$data)},

实现VUE

最后呢,我们就要来整合这些类和工具方法,在创建一个VUE实例的时候,我们先获取options中的参数,然后对起进行数据劫持和编译模板:

classVue{constructor(options){//获取模板this.$el=options.el;//获取data中的数据this.$data=options.data;//将对象中的属性存起来,以便后续使用this.$options=options//1.数据劫持,设置setter/getternewObserver(this.$data)//2.编译模板,解析指令newComplie(this.$el,this)}}

此时我们想要使用VUE中的数据,比如我们想要在vm对象中使用person.name,必须用this.$data.person.name才能获取到,如果我们想在vm对象中使用this.person.name直接修改数据,就需要代理一下this.$data。其实就是将当前的this.$data中的数据放到全局中进行监听。

exportdefaultclassVue{constructor(options){//…//1.数据劫持,设置setter/getter//2.编译模板,解析指令if(this.$el){//如果有模板//代理thisthis.proxyData(this.$data)}}proxyData(data){for(constkeyindata){//将当前的数据放到全局指向中Object.defineProperty(this,key,{get{returndata[key];},set(newVal){data[key]=newVal}})}}}

文章到了这里,就实现了一个简易版的VUE,建议大家反复学习,仔细体验,细细品味。

在文章的最后,我通过问、答的形式,来解答一些常见的面试题:

问:什么时候页面会重新渲染?

答:数据发生改变,页面就会重新渲染,但数据驱动视图,数据必须先存在,然后才能实现数据绑定,改变数据,页面才会重新渲染。

问:什么时候页面不会重新渲染?

答:有3种情况不会重新渲染:

1、未经声明和未使用的变量,修改他们,都不会重新渲染页面

2、通过索引的方式和更改长度的方式更改数组,都不会重新渲染页面

3、增加和删除对象的属性,不会重新渲染页面

问:如何使未声明/未使用的变量、增加/删除对象属性可以使页面重新渲染?

答:添加利用vm.$set/VUE.set,删除利用vm.$delete/VUE.delete方法

问:如何更改数组可以使页面重新渲染?

答:可以使用数组的变异方法(共7个):push、pop、unshift、shift、splice、sort、reverse

问:数据更新后,页面会立刻重新渲染么?

答:更改数据后,页面不会立刻重新渲染,页面渲染的操作是异步执行的,执行完同步任务后,才会执行异步的

同步队列,异步队列(宏任务、微任务)

问:如果更改了数据,想要在页面重新渲染后再做操作,怎么办?

答:可以使用vm.$nextTick或VUE.nextTick

问:来介绍一下vm.$nextTick和VUE.nextTick吧。

答:我们来看个小例子就明白啦:

<divid=”app”>{{name}}</div><script>constvm=newVue({el:’#app’,data:{name:’monk’}})vm.name=’theyoungmonk’;console.log(vm.name);//theyoungmonk此时数据已更改console.log(vm.$el.innerHTML);//monk此时页面还未重新渲染//1.使用vm.$nextTickvm.$nextTick(=>{console.log(vm.$el.innerHTML);//theyoungmonk此时数据已更改})//2.使用Vue.nextTickVue.nextTick(=>{console.log(vm.$el.innerHTML);//theyoungmonk此时数据已更改})</script>

问:vm.$nextTick和VUE.nextTick有什么区别呢?

答:VUE.nextTick内部函数的this指向Window,vm.$nextTick内部函数的this指向VUE实例对象。

Vue.nextTick(function{console.log(this);//window})vm.$nextTick(function{console.log(this);//vm实例})

问:vm.$nextTick和VUE.nextTick是通过什么实现的呢?

答:二者都是等页面渲染后执行的任务,都是使用微任务。

if(typeofPromise!==’undefined’){//微任务//首先看一下浏览器中有没有promise//因为IE浏览器中不能执行Promiseconstp=Promise.resolve;}elseif(typeofMutationObserver!==’undefined’){//微任务//突变观察//监听文档中文字的变化,如果文字有变化,就会执行回调//vue的具体做法是:创建一个假节点,然后让这个假节点稍微改动一下,就会执行对应的函数}elseif(typeofsetImmediate!==’undefined’){//宏任务//只在IE下有}else{//宏任务//如果上面都不能执行,那么则会调用setTimeout}

同样的这也是VUE的一个小缺点:VUE一直是等主线程执行完以后再执行渲染任务,如果主线程卡死,则永远渲染不出来。

问:利用Object.defineProperty实现响应式有什么缺点?

答:

1、天生就需要进行递归

2、监听不到数组不存在的索引的改变

3、监听不到数组长度的改变

4、监听不到对象的增删

版权声明:本文为CSDN博主「爱编程的小和尚」的原创文章,遵循CC4.0BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/Newbie___/article/details/105973085

?雷军:4G手机已清仓,全力转5G;QQ音乐播放中途插语音广告引热议;Wine5.9发布|极客头条

?中国AI应用元年来了!

?新基建东风下,开发者这样抓住工业互联网风口!

?15岁黑进系统,发挑衅邮件意外获Offer,不惑之年捐出全部财产,TwitterCEO太牛了!

?避坑!使用Kubernetes最易犯的10个错误

?必读!53个Python经典面试题详解

?赠书|1月以来Tether增发47亿USDT,美元都去哪儿了?

关于vue博客网站源码分享的内容到此结束,希望对大家有所帮助。

Published by

风君子

独自遨游何稽首 揭天掀地慰生平