Vue框架入门与实践之一

2023-12-14 15:25:40
1.Vue基础原理:
(1)vue.js中有两个核心功能:响应式数据绑定,组件系统
(2)MVC,MVP,MVVM之间的区别和理解;
*****MVC:
1)?视图(View):用户界面

2)?控制器(Controller):业务逻辑

3)?模型(Model):数据保存

MVC各个部分之间的通信方式如下:
1)视图传送指令到控制器

2)控制器完成业务逻辑后要求模型改变状态

3)模型将新的数据发送到视图,用户得到反馈

?? ?? ? ?以上的所有通信都是单向的;接受用户指令的时候,MVC有两种方式。一种是通过视图接受指令,然后传递给控制器;另一种是用户直接给控制器发送指令;
?? ?? ? ?实际使用中可能更加灵活,下面是Backbone.js为例说明:
1)?用户可以向视图(View)发送指令(DOM事件),再由View直接要求Model改变状态;

2)?用户也可以向Controller发送指令(改变URL触发hashChange事件,再由Controller发送给View

3) Controller很薄,只起到路由作用,而View非常厚,业务逻辑都放在View,所以Backbone索性取消了Controller,只保留了Router(路由器)

*****MVP:MVP适用于 事件驱动的应用架构中,如asp.net?web?form,window?forms应用
1)各部分之间的通信都是双向的;

2)视图(View)和模型(Model)不发生联系,都是通过表现(Presenter)传递的

3)View非常薄,不部署任何业务逻辑,称之为被动视图(Passive View)即没有任何主动性,而Presenter非常厚,所有逻辑都在这里

*****MVVM: MVVM模式将Presenter层替换为ViewModel,其他与MVP基本一致,示意图如下:
1)?它和MVP的区别是,采用双向绑定,视图层(View)的变动,自动反映在ViewModel,反之亦然,Angular和Vue,React采用这种方式

2) MVVM的提出源于WPF,主要用于分离应用界面层和业务逻辑层,WPF,Siverlight都基于数据驱动开发

3) MVVM模式中,一个ViewModel和一个View匹配,完全和View绑定,所有View中的修改变化,都会更新到ViewModel中,同时ViewModel的任何变化都会同步到View上显示;之所以自动同步是ViewModel的属性都实现了observable这样的接口,也就是说当使用属性的set方法,会同时出发属性修改的事件,使绑定的UI自动刷新;

(3)数据双向绑定的流程:
1)?建立虚拟DOM Tree,通过document.createDocumentFragment(),遍历指定根节点内部节点,根据{{prop}},v-model等规则进行compile(主要负责给node节点赋值);

2)?通过Object.defineProperty()进行数据变化拦截

3)?截取到的数据变化,通过发布者-订阅者模式,触发Watcher,从而改变虚拟DOM中的具体数据;
?? 订阅发布模式(又称为观察者模式)定义一种一对多的关系,让多个观察者同时监听一个主题对象,主题对象状态发生改变的时候通知所有的观察者。
? ?发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应的操作

4)?通过改变虚拟DOM元素值,从而改变最后渲染dom树的值,完成双向绑定
?完成数据双向绑定的关键在于:Object.defineProperty()
Vue的数据驱动主要实现建立在是三个对象上 Dep( 主题对象 ),Watcher,Compiler
Dep?主要负责依赖的收集

Watcher?主要负责Dep和Compiler之间的联系

Compiler?可以理解为virtual?dom(虚拟DOM)? + patch?也就是负责视图层的渲染

(4)简易双绑:首先,我们把注意力集中到这个属性上: Object.defineProperty;
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象

语法:Object.defineProperty(obj,prop,descriptor)

obj:要在其上定义属性的对象
prop:要定义或者修改的属性名字
descriptor:将定义或修改的属性描述符

举例如下:
var obj = {};
Object.defineProperty(obj,'hello',{? ?//这里整个都是属性描述符
??get:function(){
????//我们在这里拦截到了数据
????console.log("get方法被调用");
??},
??set:function(newValue){
????//改变数据的值,拦截下来额
????console.log("set方法被调用");
??}
});
obj.hello//输出为“get方法被调用”,输出了值。
obj.hello = 'new Hello';//输出为set方法被调用,修改了新值

?? ?? ? ?通过以上方法可以看出, 获取对象属性值触发get,设置对象属性值触发set,因此我们可以想象到数据模型对象的属性 设置和读取可以驱动view层的数据变化,view的数据变化传递给数据模型对象,在Set里面可以做很多事情。
?? ?? ? ?在这基础上,我们可以做到数据的双向绑定:
??
??let obj = {};
????Object.defineProperty(obj, 'name', {
????????set: function(newValue){
????????????console.log('触发setter');
????????????document.querySelector('.text-box').innerHTML = newValue;
????????????document.querySelector('.inp-text').value = newValue;
????????},
????????get: function(){
????????????console.log('触发getter');
????????}
????});


????document.querySelector('.inp-text').addEventListener('keyup', function(e){
????????obj.name = e.target.value;
????}, false);

html:
<input class="inp-text" type="text">
<div class="text-box"></div>

?? ?? ? ?以上只是 Vue的核心思想,通过对象底层属性的set和get进行数据拦截,vue的虚拟DOM又是怎么实现的呢?且看以下分解
(5)虚拟DOM树:
*****创建虚拟DOM的关键:var?frag =? document.createDocumentFragment()
DocumentFragment(文档片段) 可以看做是 节点容器 ,它可以包含多个子节点,可以把它插入到DOM中,只有它的子节点会插入目标节点,所以可以把它看做是一组节点容器。使用DocumentFragment处理节点 速度和性能优于直接操作DOM 。Vue进行编译的时候就是将 挂载目标的所有子节点劫持到DocumentFragment 中,进过处理后再将DocumentFragment 整体返回到挂载目标
*****view层的{{msg}}和?v-model的HTML如下:
<div id="container">
????{{ msg }}

????<input class="inp-text" type="text" v-model="inpText">
????<div class="text-box">
????????<p class="show-text">{{ msg }}</p>
????</div>
</div>

*****view层的{{msg}}和?v-model的编译规则如下:
??
??var container = document.getElementById('container');
????//这里我们把vue实例中的data提取出来,更加直观
????var data = {
????????msg: 'Hello world!',
????????inpText: 'Input text'
????};
????var fragment = virtualDom(container, data);
????container.appendChild(fragment);


????//虚拟dom创建方法,将目标盒子内所有子节点添加到其内部,注意这里只有子节点
????function virtualDom(node, data){
????????let frag = document.createDocumentFragment();
????????let child;
????????// 遍历dom节点
????????while(child = node.firstChild){
????????????compile(child, data);
????????????frag.appendChild(child);
????????}
????????return frag;
????}
?????
????//编译规则,子节点通过compile进行编译,a:如果节点为元素,其nodeType = 1;b:如果节点为文本,其nodeType = 3?
????function compile(node, data){
????????let reg = /\{\{(.*)\}\}/g;
????????if(node.nodeType === 1){ // 标签
????????????let attr = node.attributes;
????????????for(let i = 0, len = attr.length; i < len; i++){
????????????????// console.log(attr[i].nodeName, attr[i].nodeValue);
????????????????if(attr[i].nodeName === 'v-model'){
????????????????????let name = attr[i].nodeValue;
????????????????????node.value = data[name];? ?//给node节点赋值data
????????????????}
????????????}
????????????if(node.hasChildNodes()){
????????????????node.childNodes.forEach((item) => {
????????????????????compile(item, data); // 递归,如果第二步子节点仍有子节点,通过hasChildNodes()来确认,如果有递归调用Compile方法
????????????????});
????????????}
????????}
????????if(node.nodeType === 3){ // 文本节点
????????????if(reg.test(node.nodeValue)){
????????????????let name = RegExp.$1;
????????????????name = name.trim();
????????????????node.nodeValue = data[name];
????????????}
????????}
????}

(6)响应式原理:
第一步:核心思想:Object.defineProperty(obj,key,{set,get})----定义访问器属性
????
function defineReact(obj, key, value){
????????Object.defineProperty(obj, key, {
????????????set: function(newValue){
????????????????console.log(`触发setter`);
????????????????value = newValue;
????????????????console.log(value);
????????????},
????????????get: function(){
????????????????console.log(`触发getter`);
????????????????return value;
????????????}
????????});
????}

第二步:这里只是针对data数据的属性的响应式定义(从数据出发去理解原理,数据驱动),但是如何去实现 vue实例vm绑定data每个属性,通过以下方法:
????function observe(obj, vm){
????????Object.keys(obj).forEach((key) => {
????????????defineReact(vm, key, obj[key]);? ?//定义访问器属性
????????})
????}

第三步:vue的构造函数:到这里就实现了Vue实例绑定data属性
????function Vue(options){
????????this.data = options.data;
????????let id = options.el;


????????observe(this.data, this); // 将每个data属相绑定到Vue的实例上this
????}

第四步:如何去实现Vue,实例化Vue:
????var vm = new Vue({
????????el: 'container',
????????data: {
????????????msg: 'Hello world!',
????????????inpText: 'Input text'
????????}
????});
????
????console.log(vm.msg); // Hello world!
????console.log(vm.inpText); // Input text

第五步:要实现第四步的效果,必要前提是在Vue内部初始化虚拟Dom:
?????function Vue(options){
????????this.data = options.data;
????????let id = options.el;


????????observe(this.data, this); // 将每个data属相绑定到Vue的实例上this
????????
????????//------------------------添加以下代码
????????let container = document.getElementById(id);
????????let fragment = virtualDom(container, this); // 这里通过vm对象初始化
????????container.appendChild(fragment);
????????
????}

第六步:至此我们已经实现了 dom的初始化下一步我们在v-model元素添加监听事件,这样就可以通过view层的操作来修改vm对应的属性值;在compile编译的时候,可以准确的找到v-model属性元素,因此我们把监听事件添加到compile内部
????function compile(node, data){
????????let reg = /\{\{(.*)\}\}/g;
????????if(node.nodeType === 1){ // 标签
????????????let attr = node.attributes;
????????????for(let i = 0, len = attr.length; i < len; i++){
????????????????// console.log(attr[i].nodeName, attr[i].nodeValue);
????????????????if(attr[i].nodeName === 'v-model'){
????????????????????let name = attr[i].nodeValue;
????????????????????node.value = data[name];


????????????????????// ------------------------添加监听事件
????????????????????node.addEventListener('keyup', function(e){
????????????????????????data[name] = e.target.value;
????????????????????}, false);
????????????????????// -----------------------------------
????????????????}
????????????}
????????????if(node.hasChildNodes()){
????????????????node.childNodes.forEach((item) => {
????????????????????compile(item, data);
????????????????});
????????????}
????????}
????????if(node.nodeType === 3){ // 文本节点
????????????if(reg.test(node.nodeValue)){
????????????????let name = RegExp.$1;
????????????????name = name.trim();
????????????????node.nodeValue = data[name];
????????????}
????????}
????}

第七步: 这一步我们操作页面输入框,可以看到以下效果,证明监听事件添加有效。
到这里我们已经实现了MVVM, 即 Model -> vm -> View || View -> vm -> Model? 中间桥梁就是vm实例对象;
(7)进一步完善响应式数据绑定,引入观察者模式原理:

*****订阅者:三个订阅者都有update方法


????var subscribe_1 = {
????????update: function(){
????????????console.log('This is subscribe_1');
????????}
????};
????var subscribe_2 = {
????????update: function(){
????????????console.log('This is subscribe_2');
????????}
????};
????var subscribe_3 = {
????????update: function(){
????????????console.log('This is subscribe_3');
????????}
????};

*****发布者:发布者通过notify方法对订阅者广播,订阅者通过update来接受信息


????function Publisher(){
????????this.subs = [subscribe_1, subscribe_2, subscribe_3]; // 添加订阅者
????}
????Publisher.prototype = {
????????constructor: Publisher,
????????notify: function(){? ? ??
????????????this.subs.forEach(function(sub){
????????????????sub.update();
????????????})
????????}
????};

*****实例化publisher:


????var publisher = new Publisher();
????publisher.notify();

*****创建中间件来处理发布者-订阅者模式:

????var publisher = new Publisher();
????var middleware = {
????????publish: function(){
????????????publisher.notify();
????????}
????};
????middleware.publish();
(8)观察者模式嵌入:
我们已经实现了,接下来要实现:更新视图,同事把订阅-发布者模式嵌入
1) 修改?v-model?属性元素? ->?触发修改vm的属性值? ->?触发set
2)?发布者添加订阅? ->? notify分发订阅? ->?订阅者update数据

*****发布者:


????function Publisher(){
????????this.subs = []; // 订阅者容器
????}
????Publisher.prototype = {
????????constructor: Publisher,
????????add: function(sub){
????????????this.subs.push(sub); // 添加订阅者
????????},
????????notify: function(){
????????????this.subs.forEach(function(sub){
????????????????sub.update(); // 发布订阅
????????????});
????????}
????};
*****订阅者: 考虑到要把订阅者绑定data的每个属性,来观察属性的变化,参数:name参数可以有compile中获取的name传参;
?? ??? ??? ??? ?? ? ?由于传入的node节点类型分为两种,可以分为两个订阅者来处理,同时可以对node节点类型进行判断,通过switch分别处理:
?
???function Subscriber(node, vm, name){
????????this.node = node;
????????this.vm = vm;
????????this.name = name;
????}
????Subscriber.prototype = {
????????constructor: Subscriber,
????????update: function(){
????????????let vm = this.vm;
????????????let node = this.node;
????????????let name = this.name;
????????????switch(this.node.nodeType){
????????????????case 1:
????????????????????node.value = vm[name];? //赋值功能移到了订阅者这里
????????????????????break;
????????????????case 3:
????????????????????node.nodeValue = vm[name];??//赋值功能移到了订阅者这里
????????????????????break;
????????????????default:
????????????????????break;
????????????}
????????}
????};

*****我们要把订阅者添加到compile进行虚拟dom的初始化,替换掉原来的赋值:


????function compile(node, data){
????????let reg = /\{\{(.*)\}\}/g;
????????if(node.nodeType === 1){ // 标签
????????????let attr = node.attributes;
????????????for(let i = 0, len = attr.length; i < len; i++){
????????????????// console.log(attr[i].nodeName, attr[i].nodeValue);
????????????????if(attr[i].nodeName === 'v-model'){
????????????????????let name = attr[i].nodeValue;
????????????????????// --------------------这里被替换掉
????????????????????// node.value = data[name];
????????????????????new Subscriber(node, data, name);


????????????????????// ------------------------添加监听事件
????????????????????node.addEventListener('keyup', function(e){
????????????????????????data[name] = e.target.value;
????????????????????}, false);
????????????????}
????????????}
????????????if(node.hasChildNodes()){
????????????????node.childNodes.forEach((item) => {
????????????????????compile(item, data);
????????????????});
????????????}
????????}
????????if(node.nodeType === 3){ // 文本节点
????????????if(reg.test(node.nodeValue)){
????????????????let name = RegExp.$1;
????????????????name = name.trim();
????????????????// ---------------------这里被替换掉
????????????????// node.nodeValue = data[name];
????????????????new Subscriber(node, data, name);
????????????}
????????}
????}
*****既然是对虚拟dom编译的初始化, Subscriber也要初始化,即Subscriber.update,因此要对Subscriber作进一步的处理:
???function Subscriber(node, vm, name){
????????this.node = node;
????????this.vm = vm;
????????this.name = name;
????????
????????this.update();
????}
????Subscriber.prototype = {
????????constructor: Subscriber,
????????update: function(){
????????????let vm = this.vm;
????????????let node = this.node;
????????????let name = this.name;
????????????switch(this.node.nodeType){
????????????????case 1:
????????????????????node.value = vm[name];
????????????????????break;
????????????????case 3:
????????????????????node.nodeValue = vm[name];
????????????????????break;
????????????????default:
????????????????????break;
????????????}
????????}
????};

*****发布者添加到?defineRect函数,来观察数据的变化:


????function defineReact(data, key, value){
????????let publisher = new Publisher();
????????Object.defineProperty(data, key, {
????????????set: function(newValue){
????????????????console.log(`触发setter`);
????????????????value = newValue;
????????????????console.log(value);
????????????????publisher.notify(); // 发布订阅
????????????},
????????????get: function(){
????????????????console.log(`触发getter`);
????????????????if(Publisher.global){ //这里为什么来添加判断条件,主要是让publisher.add只执行一次,初始化虚拟dom编译的时候来执行
????????????????????publisher.add(Publisher.global); // 添加订阅者
????????????????}
????????????????return value;
????????????}
????????});
????}
*****这一步将订阅者添加到发布者容器内, 对订阅者改造:
??function Subscriber(node, vm, name){
????????Publisher.global = this;
????????this.node = node;
????????this.vm = vm;
????????this.name = name;
????????
????????this.update();
????????Publisher.global = null;
????}
????Subscriber.prototype = {
????????constructor: Subscriber,
????????update: function(){
????????????let vm = this.vm;
????????????let node = this.node;
????????????let name = this.name;
????????????switch(this.node.nodeType){
????????????????case 1:
????????????????????node.value = vm[name];
????????????????????break;
????????????????case 3:
????????????????????node.nodeValue = vm[name];
????????????????????break;
????????????????default:
????????????????????break;
????????????}
????????}
????};

2.Vue的状态管理Vuex:
(1)vuex是一个专门为vue.js设计的状态管理模式,并且也可以使用devtools进行调试,可以多个组件共享状态;
?? ?? ? ?简单来说,就是 共享的状态用state存放,用mutations来操作state,但是需要用store.commit来主动式的操作mutations;
(2)举例说明:
*****?在使用vues之前要先安装依赖(前提是已经 用Vue脚手架工具构建好项目
cnpm install vuex –save

*****在入口文件main.js里需要 引入?vuex,注册vuex,实例化store,把store放在全局的实例化对象
import Vue from 'vue'
import App from './App'
//1.引入vuex
import Vuex from 'vuex'
import Apple from './components/Apple'
import Banana from './components/Banana'
Vue.config.productionTip = false
//2.注册
Vue.use(Vuex);
//3.实例化store
let store=new Vuex.Store({
????state:{
????????totalPrice:0
????},
????mutations:{
????????increment(state,price){
????????????state.totalPrice+=price
????????},
????????decrement(state,price){
????????????state.totalPrice-=price
????????}
????},
????actions:{
????????increase (context,price){
????????????context.commit('increment',price)
????????},
????????decrease (context,price){
????????????context.commit('decrement',price)
????????}
????},
????getters:{
????????discount(state){
????????????return state.totalPrice *0.8;
????????}
????}
})
new Vue({
??el: '#app',
??//4.把store放在全局的实例化对象里,可以在全局的所有地方用
??store,
??components: { App},
??template: '<App/>'
})

*****参数介绍:


1)?state
? ?vuex使用单一状态树,那么就可以用一个对象包含全部的应用层级状态,所以state就作为数据源

2)?mutations
? ?更改Vuex的store中的状态的唯一方法就是提交mutations,Vuex中的mutations非常类似于事件:每个mutation都有一个字符串的?事件类型(type)和一个回调函数(handler),这个回调函数就是我们实际进状态更改的地方,并且它会接受state作为第一个参数。不能直接调用一个mutation?handler,这个选项更像是事件注册:当触发一个type为?increment的mutation时,就调用handler。要唤醒一个mutation?handler,需要调用store.commit方法触发相应的type,可以向store.commit传入额外的参数,这个参数就叫做mutation的载荷。在更多的情况下,载荷应该是一个对象,这样可以包含更多的字段;

? ?mutations必须是同步函数,那么我们如何来异步的更新state呢?答案就是actions

3)?actions
? ?actions类似于mutations,不同的是:
? ?actions提交的是mutations,而不是直接变更状态,这也就形成了actions--mutations--state的过程;
? ?actions可以包含任意异步操作;
? ?action?函数接受一个与store实例具有相同方法和属性的context对象,因此你可以调用context.commit提交一个mutation,或者通过context.state和context.getter来获取state和getter,但是如何触发呢?答案是:store.dispatch

4)?getters
? ?有时候我们需要从store中的state中派生出一些状态,getter会暴露为store.getter对象在组件中使用。

5)?modules
? ?除了上边用到的4个参数,store还有另一个参数:modules;
? ?vuex允许把store进行一个功能拆分,分割成不同的模块(module),每个模块都拥有自己的store,mutation,action,getters
*****App.vue:
<template>
??<div id="app">
????<Apple></Apple>
????<Banana></Banana>
????<p> 总价{{totalPrice}}</p>
????<p> 折后价:{{discountPrice}}</p>
??</div>
</template>
<script>
import HelloWorld from './components/HelloWorld'
import Apple from './components/Apple'
import Banana from './components/Banana'
export default {
??name: 'App',
??components: {
????HelloWorld,
????Apple,
????Banana
??},
??computed:{
????totalPrice(){
????//由于vuex的状态存储是响应式的,所以从store实例中读取状态的最简单方法就是使用计算属性来返回某个状态:
??????return this.$store.state.totalPrice
????},
????discountPrice(){
????//getter 会暴露为 store.getters 对象
??????return this.$store.getters.discount????
??????}
??}
}
</script>

*****当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余,为了解决这个问题,我们可以使用mapState辅助函数帮助我们生成计算属性;
?
import { mapState } from 'vuex'


computed: {
????...mapState(['totalPrice'])
????????
????...
??}

*****Banana.vue:

<template>
<div>
????????<p>{{msg}} 单价{{price}}</p>????
????????<button @click="addOne">add one</button>
????????<button @click="minusOne">minus one</button>
</div>
</template>
<script>
export default{
????data(){
????????return{
????????????msg:'banana',
????????????price:15
????????}
????},
????methods:{
????????addOne(){? ? ? ? ? ? ? ? ? ? ? ?//addOne()函数调用store.commit方法触发type为"increment"的mutation
????????????//直接commit一个mutation
????????????this.$store.commit('increment',this.price)
????????},
????????minusOne(){? ? ? ? ? ? ? ? ? ? //minusOne()函数调用store.commit方法触发type为"decrement"的mutation
????????????this.$store.commit('decrement',this.price)
????????}
????}
}
</script>

*****可以在组件中使用this.$store.commit('xxxx')提交mutation,或者使用 mapMutations辅助函数将组件中的methods映射为 store.commit调用;
methods:{
??????addOne(){
??????????this.increment(this.price)
??????},
??????minusOne(){
??????????this.decrement(this.price)
??????},
??????...mapMutations(['increment', 'decrement'])
}

*****Apple.vue:? action相当于中介
<template>
<div>
????<p> {{msg}}单价:{{price}} </p>????
????<button @click="addOne">add one</button>
????<button @click="minusOne">minus one</button>
</div>
</template>
<script>
export default{
????data(){
????????return{
????????????msg:'apple',
????????????price:5
????????}
????},
????methods:{
????????addOne(){? ? ? ? ? ? ? ? ?//addOne()函数里调用store.dispatch方法触发名为"increase"的action,对应的,在increase这个action里再去调用context.commit方法触发type为"increment"的mutation
????????????//dispatch一个action,以action作为一个中介再去commit一个mutation
????????????this.$store.dispatch('increase',this.price)
????????},
????????minusOne(){
????????????this.$store.dispatch('decrease',this.price)
????????}
????}
}
</script>

*****mutation和actions的区别与联系:
1)?action只能调用mutation不能直接更改state,执行action来分发(dispatch)事件通知store去改变
2)?action里可以进行一些异步的操作,再去触发mutation
3)?mutation里必须是同步的触发操作state

文章来源:https://blog.csdn.net/woshiyuyanjia/article/details/134871894
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。