最近在进行Vue的教学工作,为了能够更好的在小兄弟面前显摆显摆,我准备在教学中加入对Vue功能的原生实现环节,也将自己理解的过程写入博客,作为记录.
在Vue中,数据是核心,Vue让我们更加关注数据本身而不用理会它渲染和实现的过程.
Vue是一个典型的MV-VM框架,是MVC的进一步发展.在解释MVVM框架之前,我们首先要了解MVC是什么
MVC 模式代表 Model-View-Controller(模型-视图-控制器) 模式。这种模式用于应用程序的分层开发。
由于View层,也就是为我们的用户呈现的页面混合了HTML、CSS和JavaScript,而且页面众多,所以,代码的组织和维护难度其实很复杂.
最早的前端页面其实就是服务器上已经编写好的,内容完全静态的页面,用户在请求时,会直接返回.
但是随着业务的复杂和用户交互的需求,完全静态的页面不能够满足用户的需求.我们无法对成千上万的用户提供成千上万个不同的静态页面.
于是人们首先想到的是使用C/C++等语言,通过动态编译HTML的方式,为用户提供不同的编译完成的HTML页面.这种技术也叫做CGI(Common Gateway Interface)
CGI的效率比较低,因为我们需要在用户请求时,完整的编译一遍需要返回的页面,用户量多的时候也会不堪负重.
后来人们发现,对于用户返回的HTML,其实只有一部分数据是会改变的,而HTML的基本内容一般情况下不需要重新编译.
于是ASP,JSP,PHP应运而生,其中PHP以标记语言的特性,能够和HTML混写,服务器在接收请求之后,PHP解释器不会理会除了PHP标签内部以外的内容,而将各异性的内容编译输出即可
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
echo $name?>
</body>
</html>
请注意,这是一个php文件,混写了HTML
这就使得开发变得更为便利了起来.因此,直到现在,世界范围内排名前一千万的网站中,仍有近百分之八十的网站在使用PHP技术,这和PHP的灵活的特性是密不可分的.
关于ASP和JSP技术,笔者目前没有很深的了解,就不进一步介绍了.
随着时代的进一步发展,互联网应用的用户量越来越大,功能也越来越复杂,因而开发团队变得更为庞杂.这种PHP和HTML混写的方式不太利于团队分工协作,我到底是要写PHP呢?还是要写HTML?
首先出现的是"模板引擎"的概念,PHP程序员为了分离HTML结构,CSS样式,JS特效等与PHP业务逻辑,放弃了混写模式,采用了类似Smarty等模板进行开发,通过在HTML中预留{$name}
等数据的占位符,专注页面的开发,其实在这一时代,前端程序员才正式的被分离了出来.
而模板引擎会读取这些HTML,通过并结合后端程序员的数据处理和业务逻辑,将具体的$name
和原有的HTML一并的渲染出来,再返回给用户.
实际上 这和混写没有本质的区别,都是在后端进行页面的渲染
由此View(视图) 被分离了出来.
而后端程序员所做的操作一般的流程是
接受用户的请求-->数据库数据的CURD操作-->完成业务逻辑-->返回响应
在这个过程中,为了让程序员更加专注与业务逻辑的处理,而并不需要把精力放在具体的SQL语句的拼接,数据的处理上,Controller和Model也被分离了出来,
MVC其实不是一种设计模式,而是为了软件开发而诞生的一种开发模式,MVC允许在不改变视图的情况下改变视图对用户输入的响应方式,用户对View的操作交给了Controller处理,在Controller中响应View的事件调用Model的接口对数据进行操作,一旦Model发生变化便通知相关视图进行更新.这也实现了程序员的业务分工.
数据的通信是单向的
关于MVC,我计划书写使用PHP实现MVC框架的系列文章.
我们讲的是Vue,并不是后端MVC框架.那么,既然后端MVC已经将视图,模型和控制器进行了分层开发,我们为什么还要把传统的View层,也就是我们的前端实现再次分层呢?
这其实就涉及到了前端渲染和后端渲染的比较.
传统意义上的后端开发,我们上文中也提起过,是服务器在后端处理数据,拼接HTML,渲染模板,并将页面返回给客户端.
即便我们做好了任务分层,也仅仅是明确了开发职责,我们无法避免用户访问时,页面在后端服务器的渲染.虽然有一些缓冲技术或者页面静态化技术,但随着用户量的增加和请求的变化,服务器承受的压力会迅速增加.
为了减缓服务器的压力,人们开始使用前端技术,也就是使用javascript渲染HTML.
前端渲染,其实就是指浏览器会从后端得到一些信息,这些信息或许是Vue的模板/组件,亦或是JSON等各种数据交换格式所包装的数据,甚至是直接的合法的HTML字符串.这些形式都不重要,重要的是,将这些信息组织排列形成最终可读的HTML字符串是由浏览器来完成的,在形成了HTML字符串之后,再进行显示。
浏览器在完成第一次请求之后,将模板/应用程序输出,剩下的数据处理都交由前端来完成,我们的服务器只负责通过接口提供数据,而浏览器则会完成剩下的工作.Web不再是一张张页面,而是一个整体的应用,一个由路由系统、数据系统、页面(组件)系统…组成的应用程序,这样的组织形式会减轻服务器的压力,程序的开发也会变得更为敏捷,
前端渲染和后端渲染互有优劣
技术都是为人服务的.没有优劣之分.我们应该知道在哪些场景使用哪些技术.
有了前端渲染的相关知识,我们首先来接触一下前端的MVC概念.
如果前端没有框架,只使用原生的html+js,MVC模式可以这样理解:
将HTML看成View,负责页面的呈现,js看成Controller,负责处理用户与应用的交互,响应对View的操作(事件监听), 将js的Ajax当做Model,也就是数据层,通过Ajax从服务器获取数据。调用Model对数据进行操作,完成Model与View的同步(根据Model的改变,通过选择器对View进行操作)
在原有的"行为-样式-结构"分离的基础之上,我们通过原生js或者jQuery技术,能够很轻易的完成对页面的前端渲染.
<style>
#div{
width:400px;
height:100px;
background-color:lightblue
}
style>
<div id="oBox">
点击查看新闻
div>
<script>
//controller
var oBox = document.getElementById('oBox');
oBox.addEventListener('click',function(){
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if(this.readyState == 4 && this.status == 200){
//model
var data = this.responseText;
oBox.innerHTML == data;
}
}
xhr.open("someurl");
xhr.send(null);
});
script>
如果想要完成前端路由,我们可以使用hash或者history(pjax)轻易的实现.
虽然这样的前端开发,可以看做"MVC",但是对于职责的划分,并不是那么明确,并且看起来有些强行的MVC,Model层被弱化了很多.采用jQuery开发,代码书写和上述代码没有本质的区别,因为jQuery本身更加注重的是"Query".这样的划分,并不是真正的MVC.
实际开发过程中 MVC往往会更为灵活,我们来看一个MVC框架backbone的例子.
var M = Backbone.Model.extend({
defaults:{name:"lisi"} ,
initialize : function(){ //new M时,会执行这个初始化函数。
this.on("change",function(){ //监听change事件
alert(1);
})
}
})
var model = new M();
//这里只要改变模型,就会触发,从而改变视图。
model.set("name","wangwu");
这里Model的对象不只包含数据,也有对属性(name)的监听事件.所以backbone里的Model也不是纯Model,它有一部分Controller的功能。
而backbone里的View层也往往比较厚
var M = Backbone.Model.extend({
defaults:{name:"lisi"}
});
var V = Backbone.View.extend({
//new V时,触发初始化函数,进行对视图的监听
initialize:function(){
//视图监听模型变化,一旦改变,触发回调函数
this.listenTo(this.model, "change", this.modify);
},
//回调函数 修改视图
modify:funtion(model){
$("#oBox").append(this.model.name);
}
});
var m = new M();
// 视图中绑定模型
var v = new V({model:m});
//改变模型的name值时,就会触发change事件,执行回调函数modify,改变视图中的模型设置值。
m.set("name","wangwu");
这里view即包含了视图显示也包含了事件监听,注册了Model,属于View+Model,Controller被弱化了,更加注重了数据.
上述两个例子,我们可以看出,在backbone中,View和Model是可以互相直接影响的,而Controller的功能被分布在了View和Model中.
总结一下,backbone中的MVC有以下特性:
MVVM(Model-View-ViewModel)最早由微软提出.ViewModel指 “Model of View”——视图的模型.这个概念曾在一段时间内被前端圈热炒,以至于很多初学者拿jQuery和Vue做对比…
其实两者之间没有什么可比性,完全是两个世界的东西.
MVVM更加注重的是数据,并且弱化了View层,我们不需要也不应该在视图层上做更多的DOM操作.
而jQuery更加注重的是DOM的操作.
下面,我们以Vue为例,简单介绍一下MV-VM.
<div id="oBox">{{name}}div>
<script>
//viewmodel
let vm = new Vue({
el:"#oBox",
//model
data:{
name:"lisi"
}
});
script>
在上面的例子中 Vue 通过VM层实现了Model和View的数据绑定,当我从任何位置,哪怕是控制台更改name的值,View当中的name也会随之发生变化,我们无需关注DOM是如何操作的,只需要关注数据本身就可以了,VM会帮助我们自动完成数据的同步.
Model的变化会影响到View,同样的,View当中的数据变化也能够影响Model.
<div id="oBox">
输入购买个数 : <input name="nums" v-model="num"> <br>
总价: {{price()}} 元
div>
<script>
//viewmodel
let vm = new Vue({
el:"#oBox",
//model
data:{
num:0
},
methods:{
price(){
return parseInt(this.num) * 10;
}
}
});
script>
上例中的体现即为双向数据绑定,
下面 我们步入正题,用原生JavaScript实现数据的绑定/双向数据绑定.
为了完成Vue中的数据绑定功能,我们使用了ES6中的Proxy.
Proxy可以理解为,在目标对象之前假设一层"拦截",外界对该对象的访问,都必须通过这层拦截.它可以对外界的访问进行过滤和改写.Proxy的意思是代理,我们可以把它称作"代理器";
ES6原生提供Proxy构造函数,用来生成Proxy对象
var proxy = new Proxy(target,handler);
target
是用于代理的对象,而handler
参数也是一个对象,用于定制拦截行为;
具体代理的行为可以分为
当然,除了Proxy以外,我们也可以使用Object.defineProperty()
定义属性权限
书归正传,我们简单实现一下Vue的数据绑定/同步功能;
我们期待
首先,我们用Proxy实现数据代理,其实也就是我们的VM层,直接跟View/Model进行通信;
<div id="oBox">
姓名: {{name}}<br>
年龄: {{age}}
div>
<script type="text/javascript">
let el = document.getElementById('oBox');
// 保留原始状态 以便多次编译
let template = el.innerHTML;
// Vue 使用 Proxy 完成数据代理 在数据变的时候更新html
// 真正的数据
let _data = {
name:"whitsats",
age:18
}
let data = new Proxy(_data,{
// 监控数据修改
set(obj,name,value){
alert(`有人试图设置 ${name}=>${$value}`)
}
});
script>
在命令行修改data.name
,会弹出相应的窗体;
那么剩下的工作其实就是在数据发生改变时,重新渲染模板了;
我们定义一个render函数,用于渲染html,它会在初始化和数据变化时对页面进行重新的渲染
<div id="oBox">
姓名: {{name}}<br>
年龄: {{age}}
div>
<script type="text/javascript">
let el = document.getElementById('oBox');
// 保留原始状态 以便多次编译
let template = el.innerHTML;
// Vue 使用 Proxy 完成数据代理 在数据变的时候更新html
// 真正的数据
//Model
let _data = {
name:"whitsats",
age:18
}
// ViewModel
let data = new Proxy(_data,{
// 监控数据修改 obj即为_data
// 代理数据data修改,会影响到原始数据_data
set(obj,name,value){
obj[name] = value;
render();
}
});
render();
function render(){
// replace 正则替换的第一个参数为匹配到的字符串
// 初次渲染 会将原始数据_data渲染到模板上
// 代理数据data修改,会影响_data的值,我们仍然返回_data中的值
el.innerHTML = template.replace(/\{\{\w+\}\}/g,str=>{
str = str.substring(2,str.length-2);
return _data[str];
});
}
script>
我们上一步中的数据绑定是 Model发生变化 影响View变化,
我们期待的是通过View可以直接映射到Model变化,而Model变化同样可以导致View的更改.
首先做好模板
<div id="oBox">
<input type="text" v-model="name" />
姓名: {{name}}<br>
年龄: {{age}}
div>
我们期望
input
框中的value
在页面初始化时是_data
原始数据中的name
值_data
原始数据中的name
发生变化,input
的value
值也同步了变化input
中输入内容,同样也能影响到_data
看似是View和Model的互相影响,其实通过前面关于MVVM的描述中,VM才是实现双向数据绑定的关键,View和Model不应该有直接的关联.
为了实现上述功能,我们需要将VM功能完善一下
首先实现input
的初始化工作
我们寻找带有v-model
属性的input
标签,并根据绑定的属性名称进行赋值
<body>
<div id="oBox">
<input type="text" v-model="name" />
姓名: {{name}}<br>
年龄: {{age}}
div>
<script type="text/javascript">
let el = document.getElementById('oBox');
let template = el.innerHTML;
// Model
let _data = {
name:"whitsats",
age:18
}
// ViewModel
let data = new Proxy(_data,{
set(obj,name,value){
obj[name] = value;
render();
}
});
render();
function render(){
el.innerHTML = template.replace(/\{\{\w+\}\}/g,str=>{
str = str.substring(2,str.length-2);
return _data[str];
});
// 寻找v-model
// 1. 格式化元素为数组
// 2. 筛选数组中含有v-model属性的input标签
// 3. 循环v-model属性标签,并赋予相应的值
Array.from(el.getElementsByTagName('input'))
.filter(ele=>ele.getAttribute('v-model'))
.forEach(input=>{
let name = input.getAttribute('v-model');
input.value = _data[name];
});
}
script>
成功渲染之后,我们需要完成View到Model的绑定,同样需要VM来完成,这里肯定会使用到事件监听;
<div id="oBox">
<input type="text" v-model="name" />
姓名: {{name}}<br>
年龄: {{age}}
div>
<script type="text/javascript">
let el = document.getElementById('oBox');
let template = el.innerHTML;
//Model
let _data = {
name:"whitsats",
age:18
}
//ViewModel
let data = new Proxy(_data,{
set(obj,name,value){
obj[name] = value;
render();
}
});
render();
function render(){
el.innerHTML = template.replace(/\{\{\w+\}\}/g,str=>{
str = str.substring(2,str.length-2);
return _data[str];
});
// 寻找v-model
Array.from(el.getElementsByTagName('input'))
.filter(ele=>ele.getAttribute('v-model'))
.forEach(input=>{
let name = input.getAttribute('v-model');
input.value = _data[name];
input.oninput = function(){
//此处只是通过事件通知代理数据进行变化
//代理数据一旦变化, 触发set,重新渲染
data[name] = this.value;
}
});
}
script>
坑算是开了XD!
我比较懒,系列文章可能更新的慢一些
在不同平台上开的博客也都长了草了
emmm…尽快更新吧