整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Vue入门教程(一)之基本使用

、MVVM简介

如果你是第一次学前端,那么本节知识一定要了解,什么是MVVM。

MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。MVVM的核心是ViewModel层,负责转换Model中的数据对象来让数据变得更容易管理和使用。是一种简化用户界面的事件驱动编程方式

下边我们来画张图来大体了解下MVVM的工作原理图:

该层向上与视图层进行双向数据绑定

向下与Model层通过接口请求进行数据交互

(1)View

View是视图层, 也就是用户界面。前端主要由HTH L和csS来构建, 为了更方便地展现vi eu to del或者Hodel层的数据, 已经产生了各种各样的前后端模板语言, 比如FreeMarker,Thyme leaf等等, 各大MV VM框架如Vue.js.Angular JS, EJS等也都有自己用来构建用户界面的内置模板语言。

(2)Model

Model是指数据模型, 泛指后端进行的各种业务逻辑处理和数据操控, 主要围绕数据库系统展开。这里的难点主要在于需要和前端约定统一的接口规则

(3)ViewModel

ViewModel是由前端开发人员组织生成和维护的视图数据层。在这一层, 前端开发者对从后端获取的Model数据进行转换处理, 做二次封装, 以生成符合View层使用预期的视图数据模型。

View Model所封装出来的数据模型包括视图的状态和行为两部分, 而Model层的数据模型是只包含状态的

视图状态和行为都封装在了View Model里。这样的封装使得View Model可以完整地去描述View层。由于实现了双向绑定, View Model的内容会实时展现在View层, 这是激动人心的, 因为前端开发者再也不必低效又麻烦地通过操纵DOM去更新视图。 MVVM框架已经把最脏最累的一块做好了, 我们开发者只需要处理和维护View Model, 更新数据视图就会自动得到相应更新,真正实现事件驱动编程。 View层展现的不是Model层的数据, 而是ViewModel的数据, 由ViewModel负责与Model层交互, 这就完全解耦了View层和Model层, 这个解耦是至关重要的, 它是前后端分离方案实施的重要一环。


2、为什么要使用MVVM

MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大优点

(1) 低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。

(2) 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。

(3)独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,使用Expression Blend可以很容易设计界面并生成xaml代码。

(4)可测试。界面素来是比较难于测试的,测试可以针对ViewModel来写


3、VUE概述

(1)什么是vue?

Vue是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合

这是官网给出的介绍,可能不是那么容易理解。简单来说,Vue是一个视图层框架,帮助我们更好的构建应用。

使用Vue和原生JS一个最显著的差别就是,Vue不再对DOM直接进行操作,而是通过对数据的操作,来改变页面。使用Vue构建的页面,是有一个个的组件组成的,当组件中定义的数据发生变化时,组件的显示也会跟着变化,且此过程无需刷新页面。

(2)MVVM模式的实现者

Model:模型层, 在这里表示JavaScript对象 View:视图层, 在这里表示DOM(HTML操作的元素) ViewModel:连接视图和数据的中间件, Vue.js就是MVVM中的View Model层的实现者 在MVVM架构中, 是不允许数据和视图直接通信的, 只能通过ViewModel来通信, 而View Model就是定义了一个Observer观察者

ViewModel能够观察到数据的变化, 并对视图对应的内容进行更新 ViewModel能够监听到视图的变化, 并能够通知数据发生改变 至此, 我们就明白了, Vue.js就是一个MV VM的实现者, 他的核心就是实现了DOM监听与数据绑定

(3)为什么要使用Vue

易用:熟悉HTML、CSS、JavaScript之后,可快速度上手vue。学习曲线平稳。

轻量级:Vue.js压缩后有只有20多kb,超快虚拟DOM

高效:吸取了Angular(模块化) 和React(虚拟DOM) 的优势, 并拥有自己独特的功能

开源:文档齐全,社区活跃度高


4、VUE之Hello World!

步骤一:创建空文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>

</body>
</html>

步骤二:引入vue.js (本人下载的开发版的vue.js,跟本html文件放在了同一目录下,所以直接引用)

<script type="text/javascript" src="vue.js"></script>

步骤三:创建vue实例

<script type="text/javascript">
        var vm = new Vue({
            el:'#app',
            data:{
                msg:'Hello World'
            }
        });
</script>

步骤四:数据与页面元素绑定

<div id="app">
        {{msg}}
</div>

完整的html

<!DOCTYPE html>
<html lang="en">
<body>
    <div id="app">
        {{msg}}
    </div>
    <script type="text/javascript" src="vue.js"></script>
    <script type="text/javascript">
        var vm = new Vue({
            el:'#app',
            data:{
                msg:'Hello World'
            }
        });
    </script>
</body>
</html>

浏览器打开:

参数分析:

el : '#app' -- 绑定元素的ID(元素的挂载位置,值可以是CSS选择器或者是DOM元素)

data : { msg : 'Hello World' } -- 模型数据,属性名:msg 值:Hello World

{{msg}} : 在绑定的元素中使用{{ }}将Vue创建的名为msg的属性包起来, 即可实现数据绑定功能,我们在调试状态下手动修改下msg的值,在不刷新页面的情况下就会展示我们修改后的值,这就是借助了Vue的数据绑定功能实现的。 MV VM模式中要求View Model层就是使用观察者模式来实现数据的监听与绑定, 以做到数据与视图的快速响应

下一篇:VUE入门教程(二)之模板语法(指令)



于parseHTML函数代码实在过于庞大,我这里就不一次性贴出源代码了,大家可以前往(https://github.com/vuejs/vue/blob/dev/src/compiler/parser/html-parser.js)查看源代码。

我们来总结一下该函数的主要功能:

1、匹配标签的 "<" 字符

匹配的标签名称不能是:script、style、textarea

有如下情况:

1、注释标签 /^<!\--/

2、条件注释 /^<!\[/

3、html文档头部 /^<!DOCTYPE [^>]+>/i

4、标签结束 /^<\/ 开头

5、标签开始 /^</ 开头

然后开始匹配标签的属性包括w3的标准属性(id、class)或者自定义的任何属性,以及vue的指令(v-、:、@)等,直到匹配到 "/>" 标签的结尾。然后把已匹配的从字符串中删除,一直 while 循环匹配。

解析开始标签函数代码:

function parseStartTag () {
   // 标签的开始 如<div
    const start = html.match(startTagOpen)
    if (start) {
      const match = {
        tagName: start[1], // 标签名称
        attrs: [], // 标签属性
        start: index // 开始位置
      }
       // 减去已匹配的长度
      advance(start[0].length)
      let end, attr
      while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
        attr.start = index
        v
        advance(attr[0].length)  
        attr.end = index
        match.attrs.push(attr) // 把匹配到的属性添加到attrs数组
      }
      if (end) { // 标签的结束符 ">"
        match.unarySlash = end[1]
        advance(end[0].length)  // 减去已匹配的长度
        match.end = index  // 结束位置
        return match
      }
    }
  }

处理过后结构如下:

接下来就是处理组合属性,调用 “handleStartTag” 函数

 function handleStartTag (match) {
    const tagName = match.tagName // 标签名称
    const unarySlash = match.unarySlash // 一元标签
    if (expectHTML) {
      if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
        // 解析标签结束
        parseEndTag(lastTag)
      }
      if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
        parseEndTag(tagName)
      }
    }
   // 是否为一元标签
    const unary = isUnaryTag(tagName) || !!unarySlash
    const l = match.attrs.length
    // 标签属性集合
    const attrs = new Array(l)
    for (let i = 0; i < l; i++) {
      const args = match.attrs[i]
      const value = args[3] || args[4] || args[5] || ''
      const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href' ? options.shouldDecodeNewlinesForHref : options.shouldDecodeNewlines
      attrs[i] = {
        name: args[1], // 属性名称
        value: decodeAttr(value, shouldDecodeNewlines) // 属性值
      }
      if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
        // 开始位置
        attrs[i].start = args.start + args[0].match(/^\s*/).length
        // 结束位置
        attrs[i].end = args.end
      }
    }

    if (!unary) {
      stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
      lastTag = tagName
    }
		// 调用start函数
    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end)
    }
  }

我们简单说一下最后调用的start函数的作用:

1、判断是否为svg标签,并处理svg在ie下的兼容性问题

2、遍历标签属性,验证其名称是否有效

3、标签名是否为 style 或者 script ,如果在服务端会提示warn警告

4、检查属性是否存在 v-for、v-if、v-once指令

5、如果是更元素就验证其合法性,不能是 slot 和 template 标签,不能存在 v-for指令

以上就是界面html模板的开始标签的分析,接下来我们来分析如何匹配结束标签。

请看:Vue源码全面解析三十 parseHTML函数(解析html(二)结束标签)

如有错误,欢迎指正,谢谢。

ue的使用相信大家都很熟练了,使用起来简单。但是大部分人不知道其内部的原理是怎么样的,今天我们就来一起实现一个简单的vue。

Object.defineProperty()

实现之前我们得先看一下Object.defineProperty的实现,因为vue主要是通过数据劫持来实现的,通过get、set来完成数据的读取和更新。


var obj = {name:'wclimb'}

var age = 24

Object.defineProperty(obj,'age',{

enumerable: true, // 可枚举

configurable: false, // 不能再define

get () {

return age

},

set (newVal) {

console.log('我改变了',age +' -> '+newVal);

age = newVal

}

})

> obj.age

> 24

> obj.age = 25;

> 我改变了 24 -> 25

> 25

从上面可以看到通过get获取数据,通过set监听到数据变化执行相应操作,还是不明白的话可以去看看Object.defineProperty文档。

流程图

html代码结构

<div id="wrap">

<p v-html="test"></p>

<input type="text" v-model="form">

<input type="text" v-model="form">

<button @click="changeValue">改变值</button>

{{form}}

</div>

js调用

JavaScript

new Vue({

el: '#wrap',

data:{

form: '这是form的值',

test: '<strong>我是粗体</strong>',

},

methods:{

changeValue(){

console.log(this.form)

this.form = '值被我改变了,气不气?'

}

}

})

Vue结构

class Vue{

constructor(){}

proxyData(){}

observer(){}

compile(){}

compileText(){}

}

class Watcher{

constructor(){}

update(){}

}

  • Vue constructor 构造函数主要是数据的初始化
  • proxyData 数据代理
  • observer 劫持监听所有数据
  • compile 解析dom
  • compileText 解析dom里处理纯双花括号的操作
  • Watcher 更新视图操作

Vue constructor 初始化

class Vue{

constructor(options = {}){

this.$el = document.querySelector(options.el);

let data = this.data = options.data;

// 代理data,使其能直接this.xxx的方式访问data,正常的话需要this.data.xxx

Object.keys(data).forEach((key)=> {

this.proxyData(key);

});

this.methods = obj.methods // 事件方法

this.watcherTask = {}; // 需要监听的任务列表

this.observer(data); // 初始化劫持监听所有数据

this.compile(this.$el); // 解析dom

}

}

上面主要是初始化操作,针对传过来的数据进行处理

proxyData 代理data

class Vue{

constructor(options = {}){

......

}

proxyData(key){

let that = this;

Object.defineProperty(that, key, {

configurable: false,

enumerable: true,

get () {

return that.data[key];

},

set (newVal) {

that.data[key] = newVal;

}

});

}

}

上面主要是代理data到最上层,this.xxx的方式直接访问data

observer 劫持监听

class Vue{

constructor(options = {}){

......

}

proxyData(key){

......

}

observer(data){

let that = this

Object.keys(data).forEach(key=>{

let value = data[key]

this.watcherTask[key] = []

Object.defineProperty(data,key,{

configurable: false,

enumerable: true,

get(){

return value

},

set(newValue){

if(newValue !== value){

value = newValue

that.watcherTask[key].forEach(task => {

task.update()

})

}

}

})

})

}

}

同样是使用Object.defineProperty来监听数据,初始化需要订阅的数据。

把需要订阅的数据到push到watcherTask里,等到时候需要更新的时候就可以批量更新数据了。下面就是;

遍历订阅池,批量更新视图。

set(newValue){

if(newValue !== value){

value = newValue

// 批量更新视图

that.watcherTask[key].forEach(task => {

task.update()

})

}

}

compile 解析dom

class Vue{

constructor(options = {}){

......

}

proxyData(key){

......

}

observer(data){

......

}

compile(el){

var nodes = el.childNodes;

for (let i = 0; i 0){

this.compile(node)

}

if(node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')){

node.addEventListener('input',(()=>{

let attrVal = node.getAttribute('v-model')

this.watcherTask[attrVal].push(new Watcher(node,this,attrVal,'value'))

node.removeAttribute('v-model')

return () => {

this.data[attrVal] = node.value

}

})())

}

if(node.hasAttribute('v-html')){

let attrVal = node.getAttribute('v-html');

this.watcherTask[attrVal].push(new Watcher(node,this,attrVal,'innerHTML'))

node.removeAttribute('v-html')

}

this.compileText(node,'innerHTML')

if(node.hasAttribute('@click')){

let attrVal = node.getAttribute('@click')

node.removeAttribute('@click')

node.addEventListener('click',e => {

this.methods[attrVal] && this.methods[attrVal].bind(this)()

})

}

}

}

},

compileText(node,type){

let reg = /{{(.*)}}/g, txt = node.textContent;

if(reg.test(txt)){

node.textContent = txt.replace(reg,(matched,value)=>{

let tpl = this.watcherTask[value] || []

tpl.push(new Watcher(node,this,value,type))

return value.split('.').reduce((val, key) => {

return this.data[key];

}, this.$el);

})

}

}

}

这里代码比较多,我们拆分看你就会觉得很简单了

  1. 首先我们先遍历el元素下面的所有子节点,node.nodeType === 3 的意思是当前元素是文本节点,node.nodeType === 1 的意思是当前元素是元素节点。因为可能有的是纯文本的形式,如纯双花括号就是纯文本的文本节点,然后通过判断元素节点是否还存在子节点,如果有的话就递归调用compile方法。下面重头戏来了,我们拆开看:


if(node.hasAttribute('v-html')){

let attrVal = node.getAttribute('v-html');

this.watcherTask[attrVal].push(new Watcher(node,this,attrVal,'innerHTML'))

node.removeAttribute('v-html')

}

上面这个首先判断node节点上是否有v-html这种指令,如果存在的话,我们就发布订阅,怎么发布订阅呢?只需要把当前需要订阅的数据push到watcherTask里面,然后到时候在设置值的时候就可以批量更新了,实现双向数据绑定,也就是下面的操作

that.watcherTask[key].forEach(task => {

task.update()

})

然后push的值是一个Watcher的实例,首先他new的时候会先执行一次,执行的操作就是去把纯双花括号 -> 1,也就是说把我们写好的模板数据更新到模板视图上。

最后把当前元素属性剔除出去,我们用Vue的时候也是看不到这种指令的,不剔除也不影响

至于Watcher是什么,看下面就知道了

Watcher

class Watcher{

constructor(el,vm,value,type){

this.el = el;

this.vm = vm;

this.value = value;

this.type = type;

this.update()

}

update(){

this.el[this.type] = this.vm.data[this.value]

}

}

之前发布订阅之后走了这里面的操作,意思就是把当前元素如:node.innerHTML = ‘这是data里面的值’、node.value = ‘这个是表单的数据’

那么我们为什么不直接去更新呢,还需要update做什么,不是多此一举吗?

其实update记得吗?我们在订阅池里面需要批量更新,就是通过调用Watcher原型上的update方法。

效果

在线效果地址,大家可以浏览器看一下效果,由于本人太懒了,gif效果图就先不放了,哈哈