Vue相关内容

vue是一个渐进式框架,就是说vue有很多功能,你需要哪一部分,就可以使用哪一部分到你的项目中。vue的核心特性是数据双向绑定和组件化。除此之外,vue配套的工具库还有vuex和vue-router,前者做状态管理,后者做路由管理。

经典图片

vue 组织架构图手动添加 vue框架结构图_数据

  1. 渐进式框架:可以逐渐添加需要使用的功能,不需要使用的可以不添加。
  2. 声明式渲染:数据发生变化时,视图可以自动更新,不用关心DOM的操作,专心于数据处理。例如:使用v-for时,我们只告诉Vue需要重复渲染多少次,并没有告诉Vue每一步该如何操作。这个过程就是声明式渲染,声明新参数的值,如何修改交给Vue去操作。
    v-if v-for v-on / @ v-bind / : v-modelv-model只在表单元素中生效)
  3. Vue的双向绑定原理:
    先上图
    我们知道,vue 只支持 IE9 及以上浏览器(不包含动画),最主要的一个点是因为一个 ES5 对象的静态方法 Object.defineProperty。Object.defineProperty 可以定义对象属性的 getter/setter,vue 在这对属性里加入了观察者代码,当对象属性被修改时,vue 会追踪变化并告知依赖组件去更新实时的值。具体就是由下面的几个功能模块来实现的:
  • Observer数据监听器:能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者,内部采用 Object.definePropertygettersetter来实现。
  • Compile指令解析器:它的作用对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
  • Watcher订阅者, 作为连接 ObserverCompile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数。
  • Dep消息订阅器,内部维护了一个数组,用来收集订阅者Watcher,数据变动触发notify函数,再调用订阅者的 update 方法。
  1. Observer(监听器):作用是检测到数据是否被改变,如果改变就通知订阅者。通过Object.defineProperty()中的getset方法实现。当读取该对象属性的数据时触发get方法,修改对象属性值时触发set方法。使用Object.defineProperty()get获取输入值,set来修改渲染值。
<body>
  <div id="app"></div>
  姓名:<input id="name" type="text" ><br>
  年龄:<input id="age" type="number" >

  <script>
    let obj = {
      name : 'zzk',
      _name:'zzk',
      age : '23',
      _age:'23'
    }

    let app = document.getElementById('app')
    app.innerHTML = `我叫:${obj.name},今年:${obj.age}`
	//监听name的改变
    Object.defineProperty(obj,'name',{
      get:function(){
        console.log('读取姓名:',this._name)
        //返回_name是防止无限递归。返回name时需要去读里面的值,此时又会调用get,直到堆栈益处
        return this._name
      },
      set:function(val){
        this._name = val
        app.innerHTML = `我叫:${this.name},今年:${this.age}`
        console.log('修改姓名:',val);
      }
    })
    //监听age的改变
    Object.defineProperty(obj,'age',{
      get:function(){
        console.log('读取姓名:',this._age)
        return this._age
      },
      set:function(val){
        this._age = val
        app.innerHTML = `我叫:${this.name},今年:${this.age}`
        console.log('修改年龄:',val);
      }
    })
	//获取输入的name
    let name = document.getElementById('name')
    name.addEventListener('input',function(){
      obj.name = name.value
    })
	//获取输入的age
    let age = document.getElementById('age')
    age.addEventListener('input',function(){
      obj.age = age.value
    })

  </script>
</body>
  1. Watcher(订阅者):属性变化时,更新视图。每当数据更新时调用set方法,通知Dep(发布者),Dep调用对应参数的watcher的update对页面进行更新。
  2. Compile(模版编译器):解析dom节点,替换模版数据并初始化视图,将模版指令对应的节点绑定对应的更新函数,创建Watcher。
  1. Vue的生命周期
    Vue实例从创建到销毁的过程。具体过程为创建、初始化数据、编译模板、挂载DOM、渲染=>更新=>渲染、卸载、销毁一系列事件。

Vue更新节点

  1. Virtural DOM:将真实DOM生成一颗Virtural DOM,如果某个节点数据改变产生一个新的Vnode,之前的Virtural DOM则为oldVnode,然后对比VnodeoldVnode

    Virtural DOM 将真实的DOM数据抽取出来,以对象的形式模拟树结构
    真实的节点元素:

<div id="wrap">
    <p class="title">Hello world!</p>
</div>

Virtural DOM:

{
   tag:'div',
   attrs:{
       id:'wrap'
   },
   children:[
      {
        tag:'p',
         text:'Hello world!',
         attrs:{
            class:'title',
         }
      }
   ]
}

diff算法

1.patch方法:判断节点是继续比较还是直接进行替换。

function patch (oldVnode, vnode) {
    // some code
    if (sameVnode(oldVnode, vnode)) {
    	patchVnode(oldVnode, vnode)
    } else {
    	const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
    	let parentEle = api.parentNode(oEl)  // 父元素
    	createEle(vnode)  // 根据Vnode生成新元素
    	if (parentEle !== null) {
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
            api.removeChild(parentEle, oldVnode.el)  // 移除以前的旧元素节点
            oldVnode = null
    	}
    }
    // some code 
    return vnode
}

首先判断VnodeoldVnode是否一样,一样则继续比较子节点内容。如果不一样则Vnode直接替换oldVnode
diff算法是逐层进行比较,如果第一层不相同就不继续比较第二层,直接进行替换

  • sameVnode方法:判断节点是否相同。
function sameVnode (a, b) {
  return (
    a.key === b.key &&  // key值
    a.tag === b.tag &&  // 标签名
    a.isComment === b.isComment &&  // 是否为注释节点
    // 是否都定义了data,data包含一些具体信息,例如onclick , style
    isDef(a.data) === isDef(b.data) &&  
    sameInputType(a, b) // 当标签是<input>的时候,type必须相同
  )
}

3.patchVnode方法:对VnodeoldVnode做出更新操作。

patchVnode (oldVnode, vnode) {
    const el = vnode.el = oldVnode.el
    let i, oldCh = oldVnode.children, ch = vnode.children
    if (oldVnode === vnode) return
    if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
        api.setTextContent(el, vnode.text)
    }else {
        updateEle(el, vnode, oldVnode)
    	if (oldCh && ch && oldCh !== ch) {
            updateChildren(el, oldCh, ch)
    	}else if (ch){
            createEle(vnode) //create el's children dom
    	}else if (oldCh){
            api.removeChildren(el)
    	}
    }
}

patchVnode方法主要功能:

  • 找到对应的真实dom,设置为el
  • 判断VnodeoldVnode是否指向同一个对象,如果是,那么直接返回
  • 如果他们都有都有文字节点且不相等,将oldVnodeel的文本节点设置为Vnode的文本节点
    这也就解释了为什么<input></input>修改后内容不会改变,因为直接复用的父节点的<input></input>,如果想重新生成<input></input>的内容需要使用key,下文会讲到
  • 如果两者都有字节点,则执行updateChildren方法进行更新,下文会讲到
  • 如果Vnode有子节点,oldVnode没有子节点,则将Vnode的子节点真是化之后添加到el
  • 如果oldVnode有子节点,Vnode没有子节点,则删除el的字节点

4.updateChildren方法:对VnodeoldVnode的子节点进行更新。
代码复杂,此处先讲清楚该方法的功能和逻辑

  • Vnode的子节点VcholdVnode的子节点oldCh单独提取出来
  • oldChVch各有两个头尾变量StartIdxEndIdx,他们的两个变量相互比较,一共有4种比较方式。(old-StartIdx : V-StartIdex / old-StartIdx :V-EndIdex / old-EndIdx:V-StartIdx / old-EndIdx:V-EndIdx
  • 如果4种比较都没有匹配,如果设置key,就会用key进行比较
  • 在遍历的过程中,变量会往中间移动,一旦old-StartIdx > old-EndIdxV-StartIdex > V-EndIdx表明 oldChVch中至少有一个已经完成了遍历,则结束比较

图解updateChildren

粉红色的部分为oldCh和vCh

vue 组织架构图手动添加 vue框架结构图_vue 组织架构图手动添加_02

  1. 我们将它们取出来并分别用s和e指针指向它们的头child和尾child
  2. vue 组织架构图手动添加 vue框架结构图_Vue_03

  3. 现在分别对oldSoldESE两两做sameVnode比较,有四种比较方式,当其中两个能匹配上那么真实dom中的相应节点会移到Vnode相应的位置,这句话有点绕,打个比方
  • 如果是oldSE匹配上了,那么真实dom中的第一个节点会移到最后

  • 如果是oldES匹配上了,那么真实dom中的最后一个节点会移到最前,匹配上的两个指针向中间移动

  • 如果四种匹配没有一对是成功的,分为两种情况

    • 如果新旧子节点都存在key,那么会根据oldChildkey生成一张hash表,用Skeyhash表做匹配,匹配成功就判断S和匹配节点是否为sameNode,如果是,就在真实dom中将成功的节点移到最前面,否则,将S生成对应的节点插入到dom中对应的oldS位置,S指针向中间移动,被匹配old中的节点置为null。
    • 如果没有key,则直接将S生成新的节点插入真实DOM(ps:这下可以解释为什么v-for的时候需要设置key了,如果没有key那么就只会做四种匹配,就算指针中间有可复用的节点都不能被复用了)
  • 再配个图(假设下图中的所有节点都是有key的,且key为自身的值)
  • vue 组织架构图手动添加 vue框架结构图_Vue_04

  • 第一步

oldS = a, oldE = d;
S = a, E = b;

oldSS匹配,则将dom中的a节点放到第一个,已经是第一个了就不管了,此时dom的位置为:a b d

  • 第二步
oldS = b, oldE = d;
S = c, E = b;

oldSE匹配,就将原本的b节点移动到最后,因为E是最后一个节点,他们位置要一致,这就是上面说的:当其中两个能匹配上那么真实dom中的相应节点会移到Vnode相应的位置,此时dom的位置为:a d b

  • 第三步
oldS = d, oldE = d;
S = c, E = d;

oldEE匹配,位置不变此时dom的位置为:a d b

  • 第四步
oldS++;
oldE--;
oldS > oldE;

遍历结束,说明oldCh先遍历完。就将剩余的vCh节点根据自己的的index插入到真实dom中去,此时dom位置为:a c d b

一次模拟完成。

这个匹配过程的结束有两个条件:

  • oldS > oldE表示oldCh先遍历完,那么就将多余的vCh根据index添加到dom中去(如上图)
  • S > E表示vCh先遍历完,那么就在真实dom中将区间为[oldS, oldE]的多余节点删掉
  • vue 组织架构图手动添加 vue框架结构图_数据_05

  • 下面再举一个例子,可以像上面那样自己试着模拟一下
  • vue 组织架构图手动添加 vue框架结构图_Vue_06

  • 当这些节点sameVnode成功后就会紧接着执行patchVnode了,可以看一下上面的代码
if (sameVnode(oldStartVnode, newStartVnode)) {
    patchVnode(oldStartVnode, newStartVnode)
}

就这样层层递归下去,直到将oldVnode和Vnode中的所有子节点比对完。也将dom的所有补丁都打好。

v-if中key的作用

举一个例子,在一个场景中,我们有一个切换按钮,可以切换输入框为邮箱或者姓名,如果在邮箱的输入框中输入结束后再点击切换按钮,会发现所有dom都重新构建但是输入框的内容并没有发生变化:

  1. 此时为输入邮箱
  2. vue 组织架构图手动添加 vue框架结构图_Vue_07

  3. 输入邮箱地址
  4. vue 组织架构图手动添加 vue框架结构图_vue 组织架构图手动添加_08

  5. 点击输入类型切换:
    此时已经重新切换了为用户名输入,但是输入框的内容还保留为之前的内容
  6. vue 组织架构图手动添加 vue框架结构图_Vue_09

  7. 删除输入框的内容可以看到提示语句也已经改变
  8. vue 组织架构图手动添加 vue框架结构图_js_10

  9. 产生的原因是因为上面讲的patchVnode方法中有一个判断:如果他们都有都有文字节点且不相等,将oldVnodeel的文本节点设置为Vnode的文本节点。

要避免这个现象只需要设置key,在Vue渲染的时候会认为此dom不相同会直接进行替换,代码如下:

<template>
  <div>
    <div v-if="type === 'name'">
      <label>用户名:</label>
      <input type="text" placeholder="请输入用户名..." key="name"/>
    </div>
    <div v-else>
      <label>邮箱:</label>
      <input type="text" placeholder="请输入邮箱..." key="email"/>
    </div>
    <div>
      <button @click="handleToggleClick">切换输入的类型</button>
    </div>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      type: "",
    };
  },
  methods: {
    handleToggleClick() {
      this.type = this.type === "name" ? "mail" : "name";
    },
  },
};
</script>
Vue子父组件通信

父传子:props

  1. 基础使用方式:
// 父组件
<template>
<div>
    parent:下面是我的子组件
    <childSon :userName='name'></childSon>
</div>
</template>
<script>
import childSon from './Childs'
export default {
    name:'Parent',
    components:{
        childSon
    },
    data(){
        return{ 
            name:'啊哈'
        }
    }
}
</script>

//子组件
<template>
<div>
    child:这是父组件给我传的数据——{{userName}}
</div>
</template>
<script>
export default {
    name:'Childs',
    props:['userName'],
    data(){
        return{

        }
    }
}
</script>
  1. props验证:接受指定类型的数据
//父组件
<template>
<div>
    parent:下面是我的子组件
    <childSon :name='name' :firstName='firstName' :age='18' ></childSon>
</div>
</template>
<script>
import childSon from './Childs'
export default {
    name:'Parent',
    components:{
        childSon
    },
    data(){
        return{ 
            name:'大卫',
            firstName:'大华'
        }
    }
}
</script>

//子组件
<template>
<div>
     child:这是父组件给我传的数据——name:{{name}}——firstName:{{firstName}}——lastName:{{lastName}}——age:{{age}}
</div>
</template>
<script>
export default {
  name: "Child",
  props: {
    name: String,
    firstName: {
      type: String, //规定值的类型
      required: true, //必须传值,否则报错
    },
    lastName: {
      type: String,
      default: "lastNameDefault", //如果不传值,则为default的值
    },
    age: {
      type: [String, Number], //类型可以是多种
      validator: function (value) { //自定义验证,如果不满足条件Console会报错,但是不影响正常值得输出
        let num = parseInt(value);
        if (num > 0 && num < 10) {
          return true;
        } else {
          return false;
        }
      },
    },
  },
  data() {
    return {};
  },
};
</script>

运行结果如下:

如果validator不满足条件也可以正常输出,但是Console里会报错

vue 组织架构图手动添加 vue框架结构图_vue 组织架构图手动添加_11


vue 组织架构图手动添加 vue框架结构图_Vue_12

  1. props传入一个本地变量:父元素<input></input>里面内容改变,子组件的值也相应改变
//父组件
<template>
<div>
    parent:<input type='text' v-model="content">下面是我的子组件 
    <childSon :content='content' ></childSon>
</div>
</template>
<script>
import childSon from './Childs'
export default {
    name:'Parent',
    components:{
        childSon
    },
   data() {
    return {
      content:'er'
    };
  },
}
</script>

//子组件
<template>
<div>
    child:这是父组件给我传的数据——{{con}}
</div>
</template>
<script>
export default {    
    name:'Childs',
    props:['content'],
    data(){
        return{
           con:this.content
        }
    }
}
</script>
  1. props传入对象:v-bind会自动将对象属性给子组件赋值
//父组件
template>
<div>
    parent:下面是我的子组件
    <childSon v-bind='obj' ></childSon>
</div>
</template>
<script>
import childSon from './Childs'
export default {
    name:'Parent',
    components:{
        childSon
    },
    data(){
        return{ 
            obj: {
                name: 'lily',
                age: '16'
            }
        }
    }
}

//子组件
<template>
<div>
    child:这是父组件给我传的数据——name:{{name}}——age:{{age}}
</div>
</template>
<script>
export default {    
    name:'Childs',
    props:{
        name: String,
        age: {
            type: [String,Number], //类型可以是多种
            validator: function(value) { //自定义验证

                let num = parseInt(value)
                if (num > 0 && num <100) {
                    return true;
                } else {
                    return false;
                }
            }
        }
    }
}
</script>
</script>

子传父:$emit

子组件的值不能传递给父组件,只能通过$emit将事件发送出去。$emit方法的第一个参数是事件名称,第二个参数是要传递的值。父组件必须要监听$emit抛出的事件才能获取到传递的值。

//父组件
<template>
<div>
    parent:这是我的子组件传给我的值:{{num}}
    <childSon :content='content' @getNum='getMsg'></childSon> //监听子组件抛出的方法,通过函数来获取抛出的值
</div>
</template>
<script>
import childSon from './Childs'
export default {
    name:'Parent',
    components:{
        childSon
    },
   data() {
    return {
      content:'er',
      num:''
    }
  },
  methods:  {
      getMsg(num){
          this.num = num;
      }
  }
}
</script>

//子组件
<template>
  <div>
    child:这是父组件给我传的数据——{{ content }} <br />
    <button @click="sendMsgtoParent">点击我可以向父子间传递参数哦</button>
  </div>
</template>
<script>
export default {
  name: "Child",
  props: ["content"],
  data() {
    return {
      num: 0,
    };
  },
  methods: {
    sendMsgtoParent() {
      this.$emit("getNum", this.num++); //第一个是抛出的事件名称,第二个是参数
    },
  },
};
</script>
CLASS(ES6中引入)
  1. js中的继承都是通过原型链来实现
  2. js中只有对象没有类的概念
  3. class是一种语法糖
  4. class中的所有方法都是定义在prototype
  5. class也是一种函数
  6. class中extends做了两件事:class B extends A
    • 将类B对象中的__proto__指向Ab由类A构建)
    • vue 组织架构图手动添加 vue框架结构图_子节点_13


    • 将类B的原型对象(prototype)的__proto__指向A
    • vue 组织架构图手动添加 vue框架结构图_子节点_14


如果没有继承关系时如下所示:

  1. 将类B对象中的__proto__指向Object
  2. vue 组织架构图手动添加 vue框架结构图_子节点_15


  3. 将类B的原型对象(prototype)的__proto__指向function
  4. vue 组织架构图手动添加 vue框架结构图_Vue_16


funcitonclass的不同写法

//函数名和实例化构造名相同且大写(非强制,但这么写有助于区分构造函数和普通函数)
function Person(name,age) {
    this.name = name;
    this.age=age;
}
Person.prototype.say = function(){
    return "我的名字叫" + this.name+"今年"+this.age+"岁了";
}
var obj=new Person("zzk",18);//通过构造函数创建对象,必须使用new 运算符
console.log(obj.say());//我的名字叫zzk今年18岁了
class Person{//定义了一个名字为Person的类
    constructor(name,age){//constructor是一个构造方法,用来接收参数
        this.name = name;//this代表的是实例对象
        this.age=age;
    }
    say(){//这是一个类的方法,注意千万不要加上function
        return "我的名字叫" + this.name+"今年"+this.age+"岁了";
    }
}
var obj=new Person("zzk",18);
console.log(obj.say());//我的名字叫zzk今年18岁了