Vue

163 阅读9分钟
React & Vue
    React的数据监听变化的原理是通过进行对虚拟Dom(对象)树和真实的Dom树来引入比较进行局部的渲染。(需要使用PureComponent/shouldComponent来优化。否则可能导致大量不必要的VDOM的重新渲染)。
    vue的数据监听变化的原理是通过getter/setter以及一些函数的劫持,能够精确的知道数据的变化,不需要特别的优化能到很好的性能

1.v-key & Diff

​ 所谓虚拟DOM,使我们可以不直接操作DOM元素,只操作数据便可以重新渲染页面。而隐藏在背后的原理便是其高效的Diff算法,它的核心是基于两个简单的假设:

  1. 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构

  2. 同一个层级的一组节点(兄弟节点),他们可以通过唯一的id进行区分

当页面的数据发生变化时,Diff算法只会比较同一层级的节点:

如果节点类型不同,直接干掉前面的节点,再创建并插入新的节点,不会再比较这个节点以后的子节点了

如果节点类型相同,则会重新设置该节点的属性,从而实现节点的更新

//keys的作用,vue与React都一样
<ul>
  <li>first</li>
  <li>second</li>
</ul>
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>
//比较上述两个列表,由于是直接在末尾新增一个元素,此时性能没有问题
//但若是下例,在列表头部新增一个元素,会使得所以的li元素被销毁与重建,即针对每个子元素产生mutate而不是移动重用,所以若简单实现性能开销就比较大了
<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>
<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>
//所以 keys 就诞生了,当子元素拥有 key 时,使用 key 来匹配原有树上的子元素以及最新树上的子元素,
//就知道只有带着 '2014' key 的元素是新元素,带着 '2015' 以及 '2016' key 的元素仅仅移动了
// keys 需要在同组兄弟节点中保持唯一
<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>
<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

需要使用key来给每个节点做一个唯一的标识,Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点

​ 所以一句话,key的作用主要是为了高效的更新虚拟DOM。另外vue的在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue可以区分他们,否则vue只会替换其内部属性而不会触发过渡效果。

2.父子组件通信

1.父向子

vue中数据流是从父到子的向下的数据流,当父组件的属性发生改变后,子组件使用了该属性会同步

2.子向父

/*	
	1.传递函数,将父组件的状态以及更新状态的函数作为子组件的props传入并按需调用即可
	2.$emit / $on
		- $emit和$on的事件必须在一个公共的实例上,才能够触发;
		- 该机制其实就是保存父组件的函数,需要改变父组件的状态时调用其中对应的事件函数即可
*/ 
//公共实例对象
import Vue from 'vue';
let Bus = new Vue();
export default Bus;
//App.vue
import Bus from './EventBus.js';
created() {	    // 父监听
    Bus.$on('changeStus',(data)=> {	// 这里的this是Bus,即公共实例
        console.log(this);
        this.stus = data;
    });
}
//Son.vue
import Bus from './EventBus.js';
methods:{
    change() {
        Bus.$emit('changeStus',[1,2,3]);// 触发 Bus 公共实例上的指定方法
    }
}

3.router-view简单实现

1.history

  • 只有调用go||forward函数,才会根据历史记录也换页面;切换的过程会触发popstate事件
  • 如果不希望看到丑陋的#可以使用history模式 , 其原理依赖于 history.pushState 函数
    • a标签点击以后,如果没有# 必然会页面跳转发起请求
    • 使用pushState函数可以改变 url,如/abc但却不会发起请求
    • 通过 location.pathname 获取该值 /abc 做页面局部的替换
<a class="router-link" href="/" >Home</a>
<a class="router-link" href="/about" >About</a>
<div id="router-view"></div>
const links = document.querySelectorAll(".router-link");
for (const link of links) {
    link.addEventListener("click",function(e){
        e.preventDefault();
        const href = e.target.getAttribute("href");
        history.pushState({},"title",href);//变更地址栏
        switchContent(href);//切换视图
    });
}
function switchContent(href){
    const view = document.querySelector("#router-view");
    switch(href){
        case "/":
            view.innerHTML = `Home`;
            break;
        case "/about":
            view.innerHTML = `About`;
            break;
        default:
            view.innerHTML = `unknown`;
    }
}

2.router-view实现原理

1.非hash方式

  • Vue.use 初始化给 _route 添加监视,监听 popstate事件 ( 即 /xxx ) 或 hashchange( 即 #/xxx ) 事件,更改后更新视图
  • <router-link to="xxx">link</router-link>,即<a href=""#/index>index</a>,禁用组件其它事件,对click进行操作
  • /xxx 匹配所有路由规则 routers , 得到匹配结果 matched
    • new Router({path:'/index',name:'index',component:Index})
  • 将 matched 保存到 _route 供未来 router-view使用
  • history.pushState(/xxx)改变地址栏
  • 触发更新,调用组件 render 函数,获取 _route 并渲染 _route 保存的matched组件

2.hash凡是

  • 安装插件时监视 hashchange( 即 #/xxx ) 事件,监视 _router
  • 处理 routers 获取到 path 关联组件
  • 等待 hashchange 触发,匹配 routes 中的数据,最终得到 matched 赋值给 _router
  • 触发 _router 的监视行为,router-view 这个组件此时获取到 _router 来作为渲染的内容

4.Vue 原理

0.脏检查/observe/Proxy

//1.[Angular v1.x] : 使用脏检查,性能差,全浏览器支持;
let obj = {a:1,b:22}
let obj_copy = Object.assign({},obj)
json.a = 333;	//update
delete json.b;	//delete
json.c = 666;	//add
for(var name in obj_copy){
    if(!obj[name]){
        console.log(`delete ${name}`);
    }else if(obj[name] !== obj_copy[name]){
        console.log(`update ${name} from ${obj_copy[name]} to ${obj[name]}`);
    }
}
for(var name in obj){
    if(!obj_copy[name])	console.log(`add new attr ${name}`);
}
//[Vue] :Obejct.observe ---> ES6 Proxy,原生支持,性能最高,高级浏览器支持;给target对象套一层proxy壳,所有操作只能通过proxy对象或子类对象来完成,若直接操作target是无法被proxy监控到的。
//2.观察者 : Object.observe 已被启用
let json={};     
Object.observe(json, (changes)=>{ //被监控
    console.log('变了',changes);
});
json.a=12;

//3.proxy
let json={a: 12, b: 5};
let p=new Proxy(json, {
    get(target, key, proxy){ //target-->json
        if(key in target){
            return target[key];
        }else{
            throw new Error('no this key');
        }
    },
    set(target, key, val, proxy){
        //console.log(target, key, val, proxy);
        if(val>=0 && val<=100){
            target[key]=val;
        }else{
            throw new Error('invaild value, only accept 0-100');
        }
    },
    has(target, key){	//其实就是对代理对象p使用 in 操作时会调用has
        console.log(target, key);
    },
    deleteProperty(target, key){
        // throw new Error("read only");
        delete target[key];
    }
});
alert('c' in p);	//has()
console.log(p);
p.c = 99;	//right set
p.d = 1000;	//error set
delete p.a;	//delete
console.log(p);

1.my-vue - ES5

/**
	1.观察者,可观察者 (应该是模拟 Obejct.observe )
    2.Object.defineProperty的 setter/getter 实现数据的监控与响应
    3.编译 template 为DOM元素后,将DOM元素映射为可观察对象,再正则匹配打补丁;setter监控到数据更新后,直接遍历所有可观察对象,全部data即不管有没有发生更新都用以更新可观察对象中的DOM元素属性。
    即并没有模拟实现 虚拟DOM 与 真实DOM 的比对与按需局部更新DOM,只模拟实现了数据驱动视图,保持数据与视图的同步
注意:熟悉 setter/getter 的触发与运用
*/
//定义观察者
var tempObservable;
// 观察者
function Observer() {
    this.observables = [];
}
Observer.prototype.notify = function () {
    for (var i = this.observables.length - 1; i >= 0; i--) {
        this.observables[i].update();
    }
}
Observer.prototype.subscribe = function () {
    this.observables.push(tempObservable);
}
// 可观察对象 具备事件触发的能力
function Observable(node, propName, data) {
    this.$node = node;
    this.$propName = propName;
    this.$data = data;
}
Observable.prototype.update = function () {
    if (this.$node.nodeType === 1) { //element
        this.$node.value = this.$data[this.$propName];
    } else if (this.$node.nodeType === 3) { //text
        // 会触发get 在get内部判断
        this.$node.nodeValue = this.$data[this.$propName];
    }
}
//------------------------------------------------------------
//定义 Vue class
function Vue(options) {  //接收 options 转存为 Vue 实例属性
    this.$options = options;
    this.$el = options.el;
    this.$data = options.data;  //接收 data function
    this.?data = this.$data();
    this.$template = options.template;
    this.init();
}
//初始化 Vue 实例,监控 ?data 
Vue.prototype.init = function () {
    //1.遍历 ?data ,监视属性
    this.defineReactive(this.?data); //????
    //2.解析DOM
    this.compiler(this.$template, this.?data);
}
//编译 template ,正则查找并打补丁
Vue.prototype.compiler = function (tempstr, data) {
    // 将tempstr 插入到app中
    var box = document.querySelector(this.$el);
    box.innerHTML = tempstr;
    // 分析box 获取到其中的标记
    var nodes = box.children[0].childNodes;
    // 正则匹配
    var regexText = /.*\{\{(.*)\}\}.*/;
    var regexV = /^v-(.*)$/;
    for (var i = nodes.length - 1; i >= 0; i--) {
        var node = nodes[i];
        if (node.nodeType === 3) {
            // 分类判断 nodeType === 3 文本节点 nodeValue
            var result = regexText.exec(node.nodeValue);
            if (result) {
                // 获取结果与this.?data匹配 this.?data[xxx]
                // 触发获取
                this.textMatch(result[1].trim(), node);  // text
            }
        } else if (node.nodeType === 1) {
            // NodeTypes === 1 input标签 ele.value
            // 获取元素属性名称
            var nodeAttrs = node.attributes;
            console.log(nodeAttrs);
            for (var j = 0; j < nodeAttrs.length; j++) {
                var attr = nodeAttrs[j]; // name,value
                var result = regexV.exec(attr.name);
                if (result) {
                    console.log(attr.name, attr.value, '被匹配了', result);
                    // 要根据指令名称干活
                    this.directive[result[1]](attr.value, node, data);
                }
            }
        }
    }
}
//Vue 指令处理
Vue.prototype.directive = {
    model: function (propName, node, data) { // 参数是text
        var self = this;
        console.log(this)
        tempObservable = new Observable(node, propName, data);
        // 给一个初始值
        node.value = data[propName]; // 触发get
        // 给元素添加事件
        node.addEventListener('input', function (e) {
            // 触发set
            data[propName] = e.target.value;
        });
    }
}
//使用 setter/getter 来监控 ?date 的数据,实现响应
Vue.prototype.defineReactive = function (object) {
    var observer = new Observer();
    for (var key in object) {
        let tempValue = object[key];
        Object.defineProperty(object, key, {
            set: function (newValue) {
                console.log("setter");
                tempValue = newValue;
                observer.notify();
            },
            get: function () {
                console.log("getter");
                if (tempObservable) {
                    observer.subscribe();
                    tempObservable = null;
                }
                return tempValue;
            }
        });
    }
}
//专用于匹配处理文本补丁: {{attribute}}
Vue.prototype.textMatch = function (propName, node) {
    // 看这里: 1:创建存储信息的行为 可观察对象
    // 2:将其挂载全局
    // 3: 触发get函数,并从全局中取出1
    tempObservable = new Observable(node, propName, this.?data);
    // console.log('找到文本节点啦', propName, node);
    // 替换当前node的值,并赋值为data的数据 
    node.nodeValue = this.?data[propName]; // 触发get
}
//使用样例
let vm = new Vue({
    el: "#app",
    data() {
        return { text: 'abc' }
    },
    template: `<div>
                	<input type="text" v-model="text" />
                	{{ text }}
                </div>`
})

2.my-vue - ES6

/*
	1.定义一个 Vue Class 并继承 Proxy
	2.定义 Proxy 构造函数来监控 options.data 的数据变化,每监控到变化则触发 _render 重渲染
	3.每次渲染更新都是先使用带指令原始模板替换上一次的渲染结果,然后用最新的 data 数据编译模板,匹配Vue指令并打补丁,其中{{}}是替换, : 和 @ 指令是遍历元素直接修改元素属性
	即并没有模拟实现 虚拟DOM 与 真实DOM 的比对与按需局部更新DOM,只模拟实现了数据驱动视图,保持数据与视图的同步
*/
(function (global) {
    //原型链顶端不能为 undefined , 必须是一个有效值
    Proxy.prototype = Proxy.prototype || Object.prototype; 
    //使用 Proxy 来监控 data 变化,与其分别创建Vue实例和用于监控Vue实例中的data的Proxy两个对象
    //不如让 Vue 直接继承 Proxy,Vue 实例自身就可以直接使用 Proxy API 监控 data
    class MyVue extends Proxy {
        constructor(options) {
            let _this, _container = {};
            let data = options.data || {};
            //调用父类Proxy监控data
            //为什么不直接监控 this 呢? 1.super之后才能使用 this;2.this容易造成setter死递归
            super(data, {
                get(target, property, receiver) {
                    if (property in target) return target[property];
                    throw new Error(`[Vue warn]: Property or method "${property}" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.`);
                },
                set(target, property, newValue, receiver) {
                    target[property] = newValue;
                    //触发重新渲染
                    _container._render(_this);
                }
            });
            _this = this;
            //el / data / methods / render
            _container.$el = document.querySelector(options.el);
            //old_el保存的是含所有Vue指令和{{attribute}}的原始模板 template
            _container.$old_el = _container.$el.cloneNode(true);//true:递归复制子孙节点
            _container.$methods = options.methods || {};
            _container.$data = data;
            _container._render = render.bind(_container);
            //首次渲染
            _container._render(_this);
        }
    }

    function render(_this) {
        //this : _container ;  _this : vm instance
 //将上一次渲染的结果 替换回 原始template,因为原始template中才有 {{attribute}} 而不是 {{value}}
        //重新渲染模板需要知道原有的Vue指令和{{attribute}}
        this.$el.parentNode.replaceChild(this.$old_el, this.$el); // replaceChild(newnode,oldnode);
        this.$el = this.$old_el;
//需要重新深拷贝一份原始template,因为上句代码会导致 this.$el 与 this.$old_el 引用同一份原始template
        //而后续基于 this.$el 操作,从而导致 this.$old_el 中的原始template最终被破坏
        this.$old_el = this.$old_el.cloneNode(true);
        //处理{{}}
        this.$el.innerHTML = this.$el.innerHTML.replace(/\{\{[^\}]+\}\}/g, (str) => {
            let s = str.substring(2, str.length - 2);
            return _eval_exp(s, this.$data);
        });
        let aEle = Array.from(this.$el.getElementsByTagName('*'));
        aEle.push(this.$el);
        aEle.forEach(el => {
            Array.from(el.attributes).forEach(attr => {
                //处理:xxx="xxx"
                if (attr.name.startsWith(':')) {
                    let name = attr.name.substring(1);
                    let value = _eval_exp(attr.nodeValue, this.$data);
                    el.removeAttribute(attr.name);
                    el.setAttribute(name, value);
                    //处理@xxx="xxx"
                } else if (attr.name.startsWith('@')) {
                    let name = attr.name.substring(1);
                    let fn = this.$methods[attr.nodeValue];
                    if (!fn) {
                        throw new Error(`no '${attr.nodeValue}' method`)
                    } else {
                        el.addEventListener(name, fn.bind(_this), false);
                    }
                } else if (attr.name == 'v-for') {
                    //
                } else if (attr.name == 'v-html') {
                    //
                }
            });
        });

    }
    function _eval_exp(s, $data) {
        s = s.replace(/\w+/g, (s) => {
            return '$data.' + s;
        });
        return eval(s);
    }
    global.MyVue = MyVue;
})(window);
<div id="div">
  <div :title="name" class="">我叫{{name}},我是:aaa,我今年{{age}}岁</div>
  <div class="">{{a+b}}</div>
  <input type="button" value="按钮" @click="fn">
</div>
<script src="./my-vue.js"></script>
<script>
    let vm = new MyVue({
        el: '#div',
        data: {name: 'blue', age: 18, a: 12, b: 5},
        methods: {
          fn(){
            console.log("click event");
            this.a++; 	this.age++;		this.name+='-';
          }
        }
      });
</script>

5.practice : vue-ele

1.axios mount

Vue.prototype.axios = axios.create({
	 baseURL: 'http://localhost:8090/api/',
  	 timeout: 1000 
});

2.main entry & component

//main vue instance
new Vue({
  el: '#app',
  router,	//instance of vue-router
  store,	//vuex
  components: { App },	//root component
  template: '<App/>'	
})
//root component : 主要用于指定路由占位符 router-view 和 根节点 #app
<template>
  <div id="app">
    <router-view/>
  </div>
</template>
<script>
	export default {name: 'App'}
</script>

3.vue-router

Vue.use(Router);
export default new Router({
  routes: [
    {
      path: '/',
      name: 'index',
      component: Index
    },
    {
      path: '/detail/:id/',
      name: 'detail',
      component: Detail
    }
  ]
})

6.Vuex

new Store --- state --- mutation(commit) --- action(dispatch) --- getter(computed)
//user.store.js
export default new Vuex.Store({
  state: {  },
  mutations:{
     addUserCount(state, arg){
      state.user.count+=arg;
    }
  },
  actions:{
    addUser({commit, state}, arg){
      commit('addUserCount', arg);
    }
  }
})
Vue.use(Vuex);
const store=new Vuex.Store({
  strict: true,       //强制全用mutation完成修改——如果在mutation之外修改报错
  state: {
      count: 0,
      todos: [
          { id: 1, text: '...', done: true },
          { id: 2, text: '...', done: false }
        ]
  },
  modules: {
    user: require('./stores/user.store'),	//this.$store.state.user
    company: {count: 0}						//this.$store.state.company
  },
  mutations: {
    addCount(state, arg){
      arg=arg||1;
      state.count+=arg;
    },
    minusCount(state, arg){
      arg=arg||1;
      state.count-=arg;
    }
  },
  actions: {
    abc({commit, state}, arg){
      commit('addCount', arg);
    }
  },
  getters: {	//类似 Vue 中的 Computed 用于派生属性
    count(){
      return state.count;
    },
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
});
//组件内访问:index.vue
this.$store.state.count
this.$store.dispatch('abc', 3);	//dispatch actions
this.$store.dispatch('user/addUser')

6.practice : MintUI

1.自定义Vue插件

// vue插件必须具备Install函数
function Installer () {
    // 自身初始化行为
}
Installer.install = function (Vue) {
    // 接收Vue的构造函数,给原型挂载属性或注册全局组件或过滤器
    // console.log(Vue);
    // 1: 注册全局组件
    Vue.component('test',{
        template:`<h1>哈哈</h1>`
    });
    // 2: 挂载属性
    // Vue.prototype.$log = function() {
    //     console.log('hahaahhahaah')
    // }
    // this.$log = 'abxadksadas' 子类对象可以修改父类的属性
    let log = function () {
        console.log('我们自己插件的log函数')
    }
    // 给原型定义属性的获取和设置,设置:见鬼去吧,获取就给你
    Object.defineProperty(Vue.prototype,'$log',{
        // 设置 $log属性时的行为 || 不给,不能设置
        set:function (newV) {
            console.log('你做梦');
            // log = newV;
        },     
        get:function () {
            // 获取方式
            return log;
        }
    })
}
export default Installer;
//usage
import Installer from '@/plugins/installer';
Vue.use(Installer);

2.拦截器

// 定义拦截器
// 1: 请求发起前显示loading open();
Axios.interceptors.request.use(function(config) {
    // 不变配置:可变,可以设置公共的请求头操作
    MintUI.Indicator.open({
      text: '玩儿命加载中...',
      spinnerType: 'fading-circle'
    });
    // console.log(config);
    return config;  // config:{ headers}
})
// 2: 响应回来后关闭loading close()
Axios.interceptors.response.use(function(response) {
    //reponse: { config:{ },data:{} ,headers }
    // 接收响应头或者响应体中的数据,保存起来,供请求的拦截器中使用头信息操作
    MintUI.Indicator.close();
    // console.log(response);
    return response;
})

3.注册全局组件

import MyUl from './components/common/MyUl';
import MyLi from './components/common/MyLi';
Vue.component(MyUl.name,MyUl);
Vue.component(MyLi.name,MyLi);
import MySwipe from './components/common/Swipe';
Vue.component(MySwipe.name,MySwipe);

4.全局过滤器

Vue.filter('convertTime',function(data,formatStr){
    return Moment(data).format(formatStr);	// 2015-04-16T03:50:28.000Z
});
// 相对时间过滤器
Vue.filter('relTime',function(time){
    return Moment(time).fromNow();	// 2015-04-16T03:50:28.000Z
});