Vue的学习记录

235 阅读3分钟

1. vue和react的区别。

vue1.0是双向绑定的,vue2.0是单向绑定的,v-model是一个语法糖。 react是单向数据流。

2. vue有runtime版本和非runtime版本

node里面是没有DOM的 浏览器里面有DOM runtime版不能操作DOM,是给node准备的。

3. v-model实现的双向绑定

3.1 v-model

v-model是语法糖

<input type="text"  :value=user.name @input="user.name = $event.target.name">

等价于

<input type="text" v-model="user.name">

vue记不住光标的位置,每次输入都是由父元素将输入重新渲染。每次光标回到最前面。

FLUX兴起之后,vue的作者发现了双向绑定的一些问题之后,更倾向于单向绑定。 如果两个input都绑定了一个元素,两个人同时去操作input,就会出现问题。数据可能在任何一个时间任何一个地点被修改了。 所以一个数据只能一个人改,拥有这个数据的人才能改。 所以有了现在的v-model拆成了两部分实现。 优点: 1.数据拥有者清楚的知道数据变化的原因和时机,因为是他自己操作的; 2.数据拥有者可以阻止非法的数据变化。

vue通过两个单向绑定来模拟双向绑定所以通过修改@input的回调函数实现拦截。要改的话v-model是不行的,要把语法糖解开。
v-model 会忽略所有表单元素的 value、checked、selected 特性的初始值而总是将 Vue 实例的数据作为数据来源。应该通过 JavaScript 在组件的 data选项中声明初始值。

3.2 v-bind

它的用法是后面加冒号,跟上html元素的属性,例如:

<p v-bind:class="someclass"></p>

如果不加 v-bind 那么 someclass 就是个常量,没有任何动态数据参与。
当加上 v-bind 之后,它的值 someclass 不是字符串,而是vue实例对应的 data.someclass这个变量。
具体传入变量类型可参考Class与Style绑定。这非常适合用在通过css来实现动画效果的场合。除了class,其他大部分html原始的属性都可以通过这种方式来绑定,而且为了方便,它可以直接缩写成冒号形式。

var app = Vue({  
    el: '#app',  
    template: '<img :src="remoteimgurl">',  
    data: {    src: '',  },  
    beforeMount() {    fetch(...).then(...).then(res => this.src = res.remoteimgurl) },
})

上面这段代码中,默认情况下 data.src是空字符串,也就说不会有图片显示出来,但是当从远端获取到图片地址之后,更新了 data.src,图片就会显示出来了。

3.3 v-bind与v-model区别

有一些情况我们需要 v-bind 和 v-model 一起使用:

<input :value="name" v-model="body">

data.name 和 data.body,到底谁跟着谁变呢?甚至,它们会不会产生冲突呢?

实际上它们的关系和上面的阐述是一样的,v-bind 产生的效果不含有双向绑定,所以 :value 的效果就是让 input的value属性值等于 data.name 的值,而 v-model 的效果是使input和 data.body 建立双向绑定,因此首先 data.body 的值会给input的value属性,其次,当input中输入的值发生变化的时候,data.body 还会跟着改变。 上文提到过下面两句是等价的:

<input v-model="message">
<input v-bind:value="message" v-on:input="message = $event.target.value" />

那么 v-model 其实就是 v-bind 和 v-on 的语法糖。

4. vue-router

前端路由的优缺点

  • 优点: 用户体验很好,不需要每次都从服务器获取,快速展现给用户
  • 缺点: 1. 不利于seo,如果有十个页面,可能只有第一个页面会参与seo
    2. 使用浏览器前进后退,会重新发送请求,没有合理利用缓存 3. 单页面无法记住之前滚动的位置,无法在前进后退的时候记住滚动的位置。

区分组件还是页面

可以通过文件夹去区分这里是组件还是一个独立的页面,但是本质上是一样的。

动态路由

页面可以用$route.params.来获取参数

路由的hash

地址后面有#:路由的hash 默认是这种方式 mode:'hsah'
还有一种方式是在new Router时指定mode:'history',这样就访问不到hash了。 history也是目前主流的方式。history更加接近原始

嵌套路由

//Index.js
export default new Router({
  routes: {
      path: '/goods',
      name: 'GoodList',
      component: 'GoodList',
      children: [ //children注意都是数组
          path: 'title', //子路由的path不要加/,加了就是一级的路由了
          name: 'title',
          component: 'Title'
       ]
  }

})
//在子路由的router-link的to属性要几级写全to = '/goods/title'

编程式路由

  1. 通过js实现页面跳转
  2. $router.push("name")
  3. $router.push({})

5.v-if和v-show

v-if 是'真正的'条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建.
v-if 也是惰性的,如果在初始渲染时条件为假,那么什么都不做直到条件第一次为真的时候才会开始渲染条件块,相比之下,v-show就简单得多,不管初始条件是什么,元素总会被渲染,并且只是简单的基于css进行切换. 一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销.因此,如果需要非常频繁的切换,那么使用v-show好一点;如果在 运行时条件不太可能改变,则使用v-if 好点.

  • 类似display:none(v-show) ,visible:hidden(v-if)。

如果用v-if的话,整个dom结构压根就不会出现在页面上,如果是用v-show的话,要视后面的条件来定,如果是true,则显示,如果为false,则加上style=”display:none”。
所以呢,如果是组件之类的大块头,个人觉得用v-if更好一些,如果是一些暂时性隐藏,一会要显 示的,还是v-show更方便。对于v-style和v-show来比较,v-show相当于是v-style=”display:none”和v-style=”display:block”的快捷方式。

6. Vue中设置全局变量

Global.vue

const serverSrc='www.baidu.com';
const token='12345678';
const hasEnter=false;
const userSite="中国钓鱼岛";
  export default
  {
    userSite,//用户地址
    token,//用户token身份
    serverSrc,//服务器地址
    hasEnter,//用户登录状态
  }
  1. 使用方式1: 在需要的地方引用进全局变量模块文件,然后通过文件里面的变量名字获取全局变量参数值。
<template>
    <div>{{ token }}</div>
</template>

<script>
import global_ from '../../components/Global'//引用模块进来
export default {
    name: 'text',
    data () {
    return {
         token:global_.token,//将全局变量赋值到data里面,也可以直接使用global_.token
        }
    }
}
</script>

<style lang="scss" scoped>
</style>
  1. 使用方式2: 程序入口的main.js文件里面,将上面那个Global.vue文件挂载到Vue.prototype。
  import global_ from './components/Global'//引用文件
    Vue.prototype.GLOBAL = global_//挂载到Vue实例上面

接着在整个项目中不需要再通过引用Global.vue模块文件,直接通过this就可以直接访问Global文件里面定义的全局变量

<template>
    <div>{{ token }}</div>
</template>
<script>
export default {
 name: 'text',
data () {
    return {
         token:this.GLOBAL.token,//直接通过this访问全局变量。
        }
    }
}

</script>
<style lang="scss" scoped>
</style>

另外vuex也可以。

6.vue的双向绑定原理

6.1 访问器属性

对象的一种特殊属性,通过Object.defineProperty(obj,'变量名',{})方法单独定义。
get 和 set 方法内部的 this 都指向 obj,这意味着 get 和 set 函数可以操作对象内部的值。另外,访问器属性的会"覆盖"同名的普通属性,因为访问器属性会被优先访问,与其同名的普通属性则会被忽略。

<input type="text" id="a">
<span id="b"></span>
<script>
    let obj = {};
    Object.defineProperty(obj,"hello",{
        set:function(newVal){
            document.getElementById('a').value = newVal;
            document.getElementById('b').innerHTML = newVal;
        }
    })
    
    
    document.addEventListener('keyup', function(e){
        obj.hello = e.target.value;
    })
</script>

6.2 分解任务

<div id="app">
    <input type="text" v-model="text">
    {{text}}
</div>

var vm = new Vue({
    el:'#app',
    data:{
        text:'hello world'
    }
});

三个任务:

  1. 输入框以及文本节点与 data 中的数据绑定
  2. 输入框内容变化时,data 中的数据同步变化。即 view => model 的变化。
  3. data 中的数据变化时,文本节点的内容同步变化。即 model => view 的变化。

6.3 DocumentFragment

实现任务一,要对DOM编译。 DocumentFragment可看作节点容器,可以包含多个子节点,将它插入DOM节点中时,只有它的子节点会插入目标节点。
使用 DocumentFragment 处理节点,速度和性能远远优于直接操作 DOM。
Vue 进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持,通过 append 方法,DOM 中的节点会被自动删除)到 DocumentFragment 中,经过一番处理后,再将 DocumentFragment 整体返回插入挂载目标。

<div id="app">
    <input type="text" id="a">
    <span id="b"></span>
</div>
<script>
 let dom = nodeToFragment(document.getElementById('app'));
 console.log(dom);
 //DocumentFragments 是DOM节点。它们不是主DOM树的一部分。
 //通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。
 //在DOM树中,文档片段被其所有的子元素所代替。
 function nodeToFragment(node){
     let flag = document.createDocumentFragment();
     let child;
     while(child = node.firstChild){
         flag.appendChild(child);//劫持所有子节点
     }
     return flag;
 }
 document.getElementById('app').appendChild(dom);//返回到app中
</script>

6.4 数据初始化绑定

实现任务一的三段代码:

function compile(node,vm) {
    let reg = /\{\{(.*)\}\}/;
    //节点类型为元素
    if(node.nodeType === 1) {
        let attr = node.attributes;
        //解析属性
        for (let i = 0; i < attr.length; i++){
            if(attr[i].nodeName == 'v-model'){
                let name = attr[i].nodeValue; //获取v-model绑定的属性名
                node.value = vm.data[name];//将data的值赋给该node
                node.removeAttribute('v-model');
            }
        }
    }
    //节点类型为text
    if(node.nodeType === 3){
        if(reg.test(node.nodeValue)){
            let name = RegExp.$1;//获取匹配到的字符串
            name = name.trim();
            node.nodeValue = vm.data[name]; //将data的值赋给该node
        }
    }
}
function nodeToFragment(node,vm){
     let flag = document.createDocumentFragment();
     let child;
     while(child = node.firstChild){
         compile(child, vm);
         flag.appendChild(child);//劫持所有子节点
     }
     return flag;
 }
 
function Vue(options){
    this.data = options.data;
    let id = options.el;
    let dom = nodeToFragment(documentById(id),this);
    //编译完成后,将dom返回到app中
    document.getElementById(id).appendChile(dom);
}

let vm = new Vue({
    el: 'app',
    data: {
        text: 'hello world'
    }
});

6.5 响应式的数据绑定

当我们在输入框输入数据时,首先出发input事件(或者kyeup、change事件)。
在相应事件处理程序中,我们获取输入框的vlaue并赋值给vm实例的text属性。我们会利用defineProperty将data中的text设置为vm的访问器属性,因此vm.text赋值就会触发set方法。
在set中主要做两件事情,一个是更新属性的值,第二在任务三。

function defineReactive(obj, key, val){
    Object.defineProperty(obj, key, {
        get: function(){
            return val
        },
        set: function(newVal){
            if(newVal === val) return 
            val = newVal;
            console.log(val); //方便看效果
        }
    });
}

function observe(obj, vm){
    Object.keys(obj).forEach(key => {
        defineReactive(vm,key, obj[key]);
    });
}

function Vue(options){
    this.data = options.data;
    let data = this.data;
    
    observe(data, this);
    let id = options.el;
    let dom = nodeToFragment(document.getElementById(id), this);
    //编译完成后,将dom返回到pp中
    document.getElementById(id).appendChild(dom);
}
function compile(node, vm){
    let reg = /\{\{(.*)\}\}/;
    //节点类型为元素
    if(node.nodeType === 1){
        let attr = node.attributes;
        //解析属性
        for(let i = 0; i<attr.length; i++){
            if(attr[i].nodeName === 'v-model'){
                let name = attr[i].nodeValue'//获取v-model绑定的属性名
                node.addEventListener('input', function(e){
                   //给相应的data属性赋值,进而触发该属性的set方法
                   vm[name] = e.target.value;
                });
                node.value = vm[name];//将data的值赋给该node
                node.removeAttribute('v-model');
            }
        }
    }
    //节点类型为text
    if(node.nodeType === 3){
        if(reg.test(node.nodeValue)){
            let name = RegExp.$1;//获取匹配到的字符串
            name = name.trim();
            node.nodeValue= vm[name];//将data的值赋给该node
        }
    }
}

这样任务二完成了,text的属性值会跟随输入框的内容同步变化。

6.6 订阅/发布模式

text属性变化了,set方法触发了,但是文本节点还是没变化。如何让同样绑定到 text 的文本节点也同步变化呢?这里又有一个知识点:订阅发布模式。
订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。
发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应操作。

//一个发布者publisher
let pub = {
    publish: function(){
        dep.notify();
    }
}
//三个订阅者subscribers
let sub1 = { update: function(){console.log(1)}};
let sub2 = { update: function(){console.log(2)}};
let sub3 = { update: function(){console.log(3)}};
//一个主题对象
function Dep(){
    this.subs = [sub1, sub2, sub3];
}
Dep.prototype.notify = function(){
    this.subs.forEach(function(sub){
        sub.update();
    })
}
//发布者发布消息,主题对象执行notify方法,进而触发订阅者执行update方法
let dep = new Dep();
pub.publish(); //1,2,3

当 set 方法触发后做的第二件事就是作为发布者发出通知:“我是属性 text,我变了”。文本节点则是作为订阅者,在收到消息后执行相应的更新操作。

6.7 双向绑定的实现

每当new一个Vue,主要做了两件事:

  1. 第一个是监听数据:observe(data)
  2. 编译HTML:nodeToFragment(id)

监听的过程中,为data中的每一个属性生成一个主题对象dep。
编译html过程中,会为每个与数据绑定相关的节点生成一个订阅者watcher,watcher会将自己添加到相应属性的dep中。
已经实现的是:修改输入框内容=>在事件回调函数中修改属性值=>触发属性的set方法。
接下来要做的是: 发出通知dep.notify()=>触发订阅者的update方法=>更新试图。
关键逻辑是:如何把watcher添加到关联属性的dep中。

function compile(node,vm){
    let reg = /\{\{(.*)\}\}/;
    //节点类型为元素
    if(node.nodeType === 1){ }
    //节点类型为text
    if(node.nodeType === 3){
        if(reg.test(node.nodeValue)){
            let name = RegExp.$1;//获取匹配到的字符串
            name = name.trim();
            //node.nodeValue = vm[name];//将data赋给该node
            
            new Watcher(vm,node,name);
        
        }
    }
}

在编译过程中,为每个与data关联的节点生成一个watcher。那么watcher函数中发生了什么呢

function Watcher(vm, node, name){
    Dep.target = this;
    this.name = name;
    this.node = node;
    this.vm = vm;
    this.update();
    Dep.target = null;
}

Watcher.prototype = {
    update: function(){
        this.get();
        this.node.nodeValue = this.value;
    },
    //获取data中的属性值
    get: function(){
        this.value = this.vm[this.name];//触发相应属性的get
    }
}
   首先,将自己赋给了一个全局变量 Dep.target;

   其次,执行了 update 方法,进而执行了 get 方法,get 的方法读取了 vm 的访问器属性,从而触发了访问器属性的 get 方法,get 方法中将该 watcher 添加到了对应访问器属性的 dep 中;

   再次,获取属性的值,然后更新视图。

   最后,将 Dep.target 设为空。因为它是全局变量,也是 watcher 与 dep 关联的唯一桥梁,任何时刻都必须保证 Dep.target 只有一个值。
function defineReactive(obj, key, val){
    let dep = new Dep();
    Object.defineProperty(obj,key,{
        get:function(){
            //添加订阅者watcher到主题对象Dep
            if(Dep.target)dep.addSub(Dep.target);
            return val
        },
        set: function(newVal){
            if(newVal === val)return
            val = newVal;
            //作为发布者发出通知
            dep.notify();
        }
    });
}
function Dep(){
    this.subs = []
}
Dep.prototype = {
    addSub: function(sub){
        this.subs.push(sub);
    },
    notify: function(){
        this.subs.forEach(function(sub){
            sub.update();
        });
    }
};

至此,就全实现了。