最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • vue杂篇

    正文概述 掘金(追旅)   2021-01-15   310

    更多文章

    前言

    有段时间没用vue了,但这并不能减少我对vue的热爱,整理一下vue的知识,查漏补缺,内附原理、代码,篇幅可能过长

    想换工作了,杭州有木有小伙伴推荐的

    Vue、React异同

    相同:

    1. 使用了虚拟DOM
    2. 提供响应式和组件化的视图组件
    3. 将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库
    4. 提供生命周期钩子函数,开发者按需定制需求

    差异:

    1. Vue本质是MVVM框架,React是前端组件化框架
    2. Vue推崇模板,但也支持JSX语法,React支持JSX语法
    3. Vue提供了指令、过滤器等,方便操作,React没有
    4. Vue支持数据双向绑定
    5. Vue会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。React在应用的状态被改变时,全部子组件都会重新渲染。通过shouldComponentUpdate这个生命周期方法可以进行控制,但Vue将此视为默认的优化
    6. CSS作用域在React中是通过CSS-in-JS的方案实现的,Vue通过scoped
    7. Vue中子组件向父组件传递消息有两种方式:事件和回调函数,Vue更倾向于使用事件。在React中我们都是使用回调函数的

    MVVM是什么?和MVC区别?

    • MVC

    MVCModel-View-Controller缩写,Model即数据模型,负责数据库中数据的存取,View即视图,也就是页面上看到的,MVC的思想就是既不能在Model里面写View相关代码,也不能在View里面写Model相关代码,所以将同步ModelView的工作就交给了Controller,在数据简单的年代Controller负担起数据解析的工作毫无压力,但随着数据结构变得越来越复杂,Controller就变得非常臃肿,接下来引出了MVVM,将数据解析的工作交给了VM

    • MVVM

    MVVMModel-View-ViewModle缩写,最早由微软提出

    Model代表数据层,View代表视图层,负责展示数据,ViewModle则负责同步ModelView之间的关联,其实现同步关联的核心是DOMListenersDataBindings两个工具,DOMListeners 工具用于监听 ViewDOM 的变化,并会选择性的传给 ModelDataBindings 工具用于监听 Model 数据变化,并将其更新给 View

    MVVM``设计思想就是将复杂的``DOM中解脱出来,开发者从如何操作DOM变成了如何更新JS对象状态

    想要更详细的了解请看mvc和mvvm的区别

    生命周期

    1. 创建阶段
    • BeforeCreate(组件创建前)

    实例初始化后,数据监测(data observer)、watch/event事件配置调用之前,此阶段$el和data为undefined

    • Created(组件创建后)

    数据观测、事件配置已完成。data对象可访问,但$el还无法访问,ref为undefined,常用来做数据初始化、ajax等操作

    1. 挂载阶段
    • BeforeMount(组件挂载前)

    该函数在组件挂载前调用,此时 HTML 模板编译已完成,虚拟 DOM 已存在,$el 为可用状态,但 ref 仍不可用

    • Mounted(组件挂载后)

    组件挂载完成,数据渲染完成。DOM可以访问,此阶段只执行一次

    1. 更新阶段
    • BeforeUpdate(更新前)

    数据更新、虚拟 DOM 打补丁前调用,此阶段可以访问到当前现有DOM

    • Updated(更新后)

    数据更新、虚拟 DOM 打补丁后调用

    1. 卸载阶段
    • BeforeDestory(卸载前)

    在实例销毁前调用,可通过this获取实例,ref 仍然存在,此阶段通常用来清除定时器和绑定的监听事件

    • Destroyed(卸载后)

    实例被销毁,所有指令均被解绑,所有事件监听器移除

    1. keep-alive钩子函数
    • activated

    在被keep-alive缓存的组件激活时调用

    • deactivate

    在被keep-alive缓存的组件停用时调用。

    子父组件通信

    1. 父组件->子组件
    • props
    • provide/inject
    // provide向所有后代组件注入依赖
    // provide 和 inject 绑定并不是可响应的
    // 如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的
    // parent
    export default {
        data() {
            return {
                obj: {
                    a: 1
                },
                sex: "女"
            }
        },
        provide(){
            return {
                obj: this.obj, // obj响应
                sex: this.sex // sex非响应
            }
        }
    } 
    // child
    export default {
        inject: ['obj', 'sex']
    }
    
    • $attrs
    // 接收所有父组件除class和style且不作为prop识别的attribute绑定
    // parent
    <template>
        <Child :sex="'女'" :name="'老王'" :style="{}" />
    </template>
    // Child
    export default {
        created() {
            console.log(this.$attrs) // { sex: "女" }
        }
    }
    
    • $listeners
    // 包含了父作用域中的 (不含 .native 修饰器的)v-on事件监听器
    // parent
    <template>
        <Child1 @click1="click1" @click2="click2" @click3="click3" />
    </template>
    // child1
    <template>
        <Child2 v-on="$listeners" /> // 向下传递Child1绑定的事件
    </template>
    export default {
        created() {
            console.log(this.$listeners) // { click1: f, click2: f, click3: f }
        },
        methods: {
            click1() {
                this.$emit('click1)
            }
        }
    }
    // child2
    export default {
        created() {
            console.log(this.$listeners) // { click1: f, click2: f, click3: f }
        },
        methods: {
            click2() {
                this.$emit("click2")
            },
            click3() {
                this.$emit("click3")
            }
        }
    }
    
    1. 子组件->父组件
    • 回调函数
    • $emit
    1. 兄弟组件
    • 状态提升到父组件
    1. 通用
    • vuex
    • bus事件总线
    // main.js
    Vue.prototype.$bus = new Vue()
    // 发出
    this.$bus.$emit('test', 666)
    // 接收
    this.$bus.$on('test', (e)=> console.log(e)) // 666
    

    双向绑定原理

    Vue采用的是数据劫持 + 订阅者发布者模式,数据劫持是通过Object.defineProperty为组件data每个属性添加getset方法,在数据发生变化时触发set中的监听回调,通知给订阅者

    第一步:递归遍历数据(data),通过Object.defineProperty为属性添加getter和setter

    Observer.prototype = {
        walk: function(data) {
            var self = this;
            Object.keys(data).forEach(function(key) {
                self.defineReactive(data, key, data[key]);
            });
        },
        defineReactive: function(data, key, val) {
            var dep = new Dep();
            var childObj = observe(val); // 对象递归
            Object.defineProperty(data, key, {
                enumerable: true,
                configurable: true,
                get: function() { },
                set: function(newVal) { }
            });
        }
    }
    function observe(value, vm) {
        if (!value || typeof value !== 'object') return
        return new Observer(value);
    };
    

    第二步:dep作为观察者(收集依赖,发布消息)会在调用getter时注册函数,在调用setter时通知执行注册的函数

    Object.defineProperty(data, key, {
        get: function() {
            if (Dep.target) {
                dep.addSub(Dep.target); // 注册函数,收集依赖
            }
            return val;
        },
        set: function(newVal) {
            if (newVal === val) return;
            val = newVal;
            dep.notify(); // 触发update更新,发布消息
        }
    });
    Dep.prototype = {
        addSub: function(sub) {
            this.subs.push(sub);
        },
        notify: function() {
            this.subs.forEach(function(sub) {
                sub.update(); // watcher提供update方法
            });
        }
    };
    

    第三步:Compile解析模板指令,将其中的变量替换成数据。然后初始化渲染页面视图,并将每个指令对应的节点绑定上更新函数、监听函数。后续一旦数据发生变化,便会更新页面。页面发生变化时也会相应发布变动信息

    function Compile(el, vm) {
        this.vm = vm;
        this.el = document.querySelector(el);
        this.fragment = null;
        this.init();
    }
    
    Compile.prototype = {
        init: function () {
            if (this.el) {
                this.fragment = this.nodeToFragment(this.el);
                this.compileElement(this.fragment);
                this.el.appendChild(this.fragment);
            } else {
                console.log('Dom元素不存在');
            }
        },
        nodeToFragment: function (el) {
            var fragment = document.createDocumentFragment();
            var child = el.firstChild;
            while (child) {
                // 将Dom元素移入fragment中
                fragment.appendChild(child);
                child = el.firstChild
            }
            return fragment;
        },
        compileElement: function (el) {
            var childNodes = el.childNodes;
            var self = this;
            [].slice.call(childNodes).forEach(function(node) {
                var reg = /\{\{(.*)\}\}/;
                var text = node.textContent;
    
                if (self.isElementNode(node)) {  
                    // 节点
                    self.compile(node);
                } else if (self.isTextNode(node) && reg.test(text)) {
                    // 是否是符合这种形式{{}}的指令,解析模板
                    self.compileText(node, reg.exec(text)[1]);
                }
                if (node.childNodes && node.childNodes.length) {
                    // 递归子节点
                    self.compileElement(node);
                }
            });
        },
        compile: function(node) {
            var nodeAttrs = node.attributes;
            var self = this;
            Array.prototype.forEach.call(nodeAttrs, function(attr) {
                var attrName = attr.name;
                if (self.isDirective(attrName)) {
                    var exp = attr.value;
                    var dir = attrName.substring(2);
                    if (self.isEventDirective(dir)) {  
                        // 事件指令
                        self.compileEvent(node, self.vm, exp, dir);
                    } else {  
                        // v-model 指令
                        self.compileModel(node, self.vm, exp, dir);
                    }
                    node.removeAttribute(attrName);
                }
            });
        },
        compileText: function(node, exp) {
            var self = this;
            var initText = this.vm[exp];
            // 将初始化的数据初始化到视图中
            this.updateText(node, initText); 
            // 生成订阅器并绑定更新函数,Watcher中updaue会触发该事件
            new Watcher(this.vm, exp, function (value) { 
                self.updateText(node, value);
            });
        },
        compileEvent: function (node, vm, exp, dir) {
            var eventType = dir.split(':')[1];
            var cb = vm.methods && vm.methods[exp];
            if (eventType && cb) {
                node.addEventListener(eventType, cb.bind(vm), false);
            }
        },
        compileModel: function (node, vm, exp, dir) {
            var self = this;
            var val = this.vm[exp];
            this.modelUpdater(node, val);
            new Watcher(this.vm, exp, function (value) {
                self.modelUpdater(node, value);
            });
            // 监听input事件,视图变化更新data数据
            node.addEventListener('input', function(e) {
                var newValue = e.target.value;
                if (val === newValue) {
                    return;
                }
                self.vm[exp] = newValue;
                val = newValue;
            });
        },
        updateText: function (node, value) {
            node.textContent = typeof value == 'undefined' ? '' : value;
        },
        modelUpdater: function(node, value, oldValue) {
            node.value = typeof value == 'undefined' ? '' : value;
        },
        isDirective: function(attr) {
            return attr.indexOf('v-') == 0;
        },
        isEventDirective: function(dir) {
            return dir.indexOf('on:') === 0;
        },
        isElementNode: function (node) {
            return node.nodeType == 1;
        },
        isTextNode: function(node) {
            return node.nodeType == 3;
        }
    }
    

    第四步:Watcher订阅者作为Observer和Compile之间的桥梁,实例化时将自己添加到dep中,提供一个update方法待数据变化时触发Compile中的相应函数完成更新

    function Watcher(vm, exp, cb) {
        this.cb = cb;
        this.vm = vm;
        this.exp = exp;
        this.value = this.get();  // 将自己添加到订阅器dep
    }
    
    Watcher.prototype = {
        update: function() {
            this.run();
        },
        run: function() {
            var value = this.vm.data[this.exp];
            var oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal); // 触发Compile中的相应更新视图函数
            }
        },
        get: function() {
            Dep.target = this;  // 缓存自己
            var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
            Dep.target = null;  // 释放自己
            return value;
        }
    };
    
    

    computed、watch差异

    • Computed:
    1. 计算属性
    2. 支持缓存,只有依赖的数据发生变化时才会重新计算
    3. 不支持异步操作
    4. 自动监听依赖值的变化,从而动态返回内容

    通常一个值依赖于其他多个值计算而来的话我们会使用Computed,Computed默认具有getter,setter则需要手动写,如下:

    computed: {
        num: {  
            get() {
                return this.num1 + this.num2
            },
            set(e) {
                console.log('do somethings')
            }
        }
    }
    
    • Watch
    1. 监听
    2. 不支持缓存
    3. 可进行异步操作
    4. 监听是一个过程,在监听的值变化时,可以触发一个回调

    Watch如果需要在组件加载立即出发需要将immediate设置为ture,如果需要监听深层次的属性需要将deep设置为true

    需要注意的是一旦添加deep则所有属性将被监听,非常的耗性能,所以通常会优化一下只监听需要被监听的属性,如:obj.a,例外一个问题:不是添加了deep所有的操作都会被监听到,如对象新增属性obj.newAttr = 1、数组arr[0] = 1这样的操作,只有那些响应式的方式才能被监听到,如push、pop等

    watch: {
        obj: {
            handler: function(newVal, oldVal) {
                console.log(newVal, oldVal)
            },
            immediate: true,
            deep: true
        }
    }
    

    异步队列、$nextTick

    Vue中数据发生变化时,并不会立即更新DOM,通常会使用一个for循环去测试,这样做是为了避免不必要的计算和DOM操作。

    每个Watcher都有一个唯一id,当同一个Watcher被多次被触发,会根据id判断是否存在,避免重复推入队列,这是Vue做的优化,去重之后的队列中的Watcher则是需要被执行的,但这个队列是不会被立即执行的,会在下一个事件循环tick执行也就是我们说的nextTick,所以我们先看一下Vue中nextTick的实现,了解了nextTick,其实异步队列更新也就差不多了

    // flushCallbacks,执行所有的任务
    const callbacks = []
    let pending = false
    function flushCallbacks () {
      pending = false
      const copies = callbacks.slice(0)
      callbacks.length = 0
      for (let i = 0; i < copies.length; i++) {
        copies[i]()
      }
    }
    // 为了兼容,使用顺序是Promise->MutationObserver->setImmediate->setTimeout
    // 将任务放到微任务或者宏任务中执行,所有flushCallbacks中同步任务执行完毕执行异步任务
    // timerFunc
    let timerFunc;
    if (typeof Promise !== "undefined" && isNative(Promise)) {
        const p = Promise.resolve();
        timerFunc = () => {
            p.then(flushCallbacks);
            if (isIOS) setTimeout(noop);
        };
        isUsingMicroTask = true;
    } else if (
    !isIE &&
    typeof MutationObserver !== "undefined" &&
    (isNative(MutationObserver) ||
        // PhantomJS and iOS 7.x
        MutationObserver.toString() ===
        "[object MutationObserverConstructor]")
    ) {
        let counter = 1;
        const observer = new MutationObserver(flushCallbacks);
        const textNode = document.createTextNode(String(counter));
        observer.observe(textNode, {
            characterData: true
        });
        timerFunc = () => {
            counter = (counter + 1) % 2;
            textNode.data = String(counter);
        };
        isUsingMicroTask = true;
    } else if (
    typeof setImmediate !== "undefined" &&
    isNative(setImmediate)
    ) {
        timerFunc = () => {
            setImmediate(flushCallbacks);
        };
    } else {
        timerFunc = () => {
            setTimeout(flushCallbacks, 0);
        };
    }
    // nextTick
    export function nextTick (cb?: Function, ctx?: Object) {
        let _resolve
        // 收集任务,等待执行
        callbacks.push(() => {
            if (cb) {
                try {
                    cb.call(ctx)
                } catch (e) {
                    handleError(e, ctx, 'nextTick')
                }
            } else if (_resolve) {
                _resolve(ctx)
            }
        })
        if (!pending) { // 防止后续的 nextTick 重复执行 timerFunc
            pending = true
            // 执行任务
            timerFunc()
        }
    }
    

    ok,接下来是异步任务队里

    // update更新
    update () {
        // 各种情况的判断 ...  
        else {
            queueWatcher(this) // this 为当前的实例 watcher
        }
    }
    // queueWatcher
    const queue = []
    let has = {}
    let waiting = false
    let flushing = false
    let index = 0
    export function queueWatcher (watcher: Watcher) {
        const id = watcher.id
        // 判断id是否存在,去重操作  
        if (has[id] == null) {
            has[id] = true
            if (!flushing) {
                queue.push(watcher)
            } else {
                let i = queue.length - 1
                while (i > index && queue[i].id > watcher.id) {
                    i--
                }
                queue.splice(i + 1, 0, watcher)
            }
            if (!waiting) {
                waiting = true
                // 下一个tick执行任务,flushSchedulerQueue在nextTick中会添加到callbacks中
                nextTick(flushSchedulerQueue)
            }
        }
    }
    // flushSchedulerQueue
    function flushSchedulerQueue () {
        currentFlushTimestamp = getNow()
        flushing = true
        let watcher, id
        queue.sort((a, b) => a.id - b.id)
        for (index = 0; index < queue.length; index++) {
            watcher = queue[index]
            if (watcher.before) {
                watcher.before()
            }
            id = watcher.id
            has[id] = null
            watcher.run() // 执行更新任务
        }
    }
    

    可以看到不仅是我们日常开发会使用nextTick,对于Vue来说也是它的Api,要理解这些你要对js执行机制有一些了解

    v-for中key的作用

    为什么使用key呢?很简单,就是因为Vue追踪DOM的变化依赖一个标识(针对的是动态DOM),对于一个DOM的是新生的还是变换了位置,没有一个唯一标识去追踪,神仙也不可能知道这些变化,说白了就是给DOM一个身份,因为DOM中一样的标签如:div、p等他们看起来就像是克隆人,你无法识别,为啥要给个身份?其实是还是为了优化,优化的是diff算法,添加key和不添加key计算出来的补丁是完全不一样的,我把之前一篇文章的例子拿过来看一下(了解更多请看Virtual Dom 及 Diff 算法初探):

    const oldData = createElement('ul', { key: 'ul' }, [ // 标识  ul: 0
        createElement('li', {}, ['aaa']), //  li: 1    aaa: 2
        createElement('li', { }, ['bbb']), // li: 3    bbb: 4
        createElement('li', { }, ['ccc']) //  li: 5    ccc: 6
    ])
    const newData = createElement('ul', { key: 'ul' }, [
        createElement('li', {}, ['aaa']),
        createElement('li', { }, ['aaa']),
        createElement('li', { }, ['bbb']),
        createElement('li', { }, ['ccc'])
    ])
    const patches = diff(oldData, newData)
    console.log('patches----------', patches)
    

    vue杂篇

    根据patches结果,我们可以看出相应标识节点发生的变化,再来看一组数据

    const oldData = createElement('ul', { key: 'ul' }, [
        createElement('li', { key: 1 }, ['aaa']),
        createElement('li', { key: 2 }, ['bbb']),
        createElement('li', { key: 3 }, ['ccc'])
    ])
    const newData = createElement('ul', { key: 'ul' }, [
        createElement('li', { key: 1 }, ['aaa']),
        createElement('li', { key: 4 }, ['aaa']),
        createElement('li', { key: 2 }, ['bbb']),
        createElement('li', { key: 3 }, ['ccc'])
    ])
    const patches = diff(oldData, newData)
    console.log('patches----------', patches)
    

    vue杂篇

    可以看出不加key的补丁确实复杂了很多,日常复杂操作中又不知道要复杂多少倍

    另外一个问题是for循环中尽量避免自带的index索引,因为一个元素变化会导致后续所有DOM表示发生了变化,导致补丁变复杂,更新效率也变差

    v-if、v-show差异

    优化是无处不在的,分清v-ifv-show也是为了优化

    • v-if

    真正的条件渲染,对DOM销毁或重建,耗性能,切换过程中条件块内的事件监听器和子组件适当地被销毁和重建

    另外v-for具有比v-if 更高的优先级,同时使用也会造成性能问题,通常用computedv-show解决

    • v-show

    简单的css切换(display),开销小,频繁切换常用

    Vue组件中data必须是函数

    其实这也是个js的基础问题,因为同一对象被复用地址同一个会互相影响,也即是一个组件被多次引用这个数据就会乱掉,所以用函数返回一个新对象

    Vue初始化页面闪动问题

    <!-- css -->
    [v-cloak] { display: none; }
    <!-- html -->
    <div v-cloak>
      {{ message }}
    </div>
    

    Vue、React路由模式

    前端路由主要就是hash路由和history路由,vue-routerreact-router亦是如此

    • hash路由

    hash路由的url是带#号的,通过监听hashchange事件改变页面内容

    <a href="#/page1">page1</a>
    <a href="#/page2">page2</a>
    <div id="app"></div>
    
    const app = document.getElementById('app');
    window.addEventListener('onload',hashChange() )
    window.addEventListener('hashchange', () => hashChange())
    
    function hashChange() {
        switch (window.location.hash) {
            case '#/page1':
                app.innerHTML = 'page1'
                return
            case '#/page2':
                app.innerHTML = 'page2'
                return
            default:
                app.innerHTML = 'page1'
                return
        }
    }
    
    • history

    history新增了pushStatereplaceState(pushState是将传入url压入历史记录栈,replaceState将传入url替换当前历史记录栈),在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求

    <a href="/page1">page1</a>
    <a href="/page2">page2</a>
    <div id="app"></div>
    
    window.addEventListener('DOMContentLoaded', Load)
    window.addEventListener('popstate', PopChange)
    var routeView = null
    function Load() {
        routeView = document.getElementById('app')
        // 默认执行一次 popstate 的回调函数
        PopChange()
        var aList = document.querySelectorAll('a[href]')
        aList.forEach(aNode => aNode.addEventListener('click', function (e) {
            e.preventDefault() 
            var href = aNode.getAttribute('href')
            history.pushState(null, '', href)
            // popstate 是监听不到地址栏的变化,所以此处需要手动执行回调函数 PopChange
            PopChange()
        }))
    }
    function PopChange() {
        switch (window.location.pathname) {
            case '/page1':
                routeView.innerHTML = 'page1'
                return
            case '/page2':
                routeView.innerHTML = 'page2'
                return
            default:
                routeView.innerHTML = 'page1'
                return
        }
    }
    

    history模式需要后台进行配置,当url无法匹配到静态资源时,返回同一个index.html,以nginx为例:

    location / {
      try_files $uri $uri/ /index.html;
    }
    

    后台配置

    vue-router路由钩子函数

    通常会将路由钩子函数称之为路由守卫,守卫就是"看大门的",谁要进来就要看这个"看大门的"让不让你进来了,如果你身份符合条件就会让你进来,也就是我们经常会做的一件事:身份验证,"大门"也要分很多种的,主大门,屋内小门等等,所以路由钩子自然也会做区分:

    • 全局钩子

    这是主大门的"看门的",这里如果过不去,你怕是哪都去不了

    // 常用钩子:跳转前校验
    router.beforeEach((to, from, next) => {
      // ...
    })
    
    • 独享钩子

    深宫大院总有一些特殊身份的人,他特殊啊,所以他自然也独享一些"看门的"

    const router = new VueRouter({
      routes: [
        {
          path: '/foo',
          component: Foo,
          beforeEnter: (to, from, next) => { }
        }
      ]
    })
    
    • 各组件钩子

    各屋没那么特殊,但也会有个门,想进来也要看有没有"看门的"、"看门的"心情如何

    export default {
        data() {},
        beforeRouteEnter(to, from, next) {}, // 进门前你是不知道主人是谁的,所以此阶段this为undefined
        beforeRouteLeave (to, from, next) {}
    }
    

    Vue如何动态路由

    • router提供的动态路由

    无论Vue还是React都会提供/:id方式动态匹配,通常这种方式会用来实现匹配某一类相同结构的页面

    • 动态权限路由

    通常需要做权限控制就需要动态路由,有两种做法:

    1. 前端有一份全量的路由数据,然后根据请求后台的权限数据处理这份全量的路由的数据,将不符合条件的过滤掉不显示在菜单栏或页面其它地方,页面跳转的时候也通过路由守卫判断是否有权限,无权限则提示无权限

    2. 将前端路由分为静态和动态两份,静态的如登录页、404等可以直接写到路由中,而动态部分由后端提供,前端拿到后通过router.addRoutes添加到前端路由中,这里会问一个问题是如果退出登录时如果直接使用push的话再次登录会遇到路由重复添加的问题,处理这个有两种方式:

    // 一、简单粗暴的使用window.location.href 
    window.location.href = '/login'
    // 二、每次添加路由前清空路由信息
    // router.js
    import Vue from 'vue'
    import Router from 'vue-router'
    
    Vue.use(Router)
    
    const createRouter = () => new Router({
      mode: 'history',
      routes: []
    })
    const router = createRouter()
    export function resetRouter () {
      const newRouter = createRouter()
      router.matcher = newRouter.matcher 
    }
    export default router
    // 后续调用addRoutes前先调用resetRouter
    import {resetRouter} from '@/router'
    resetRouter()
    router.addRoutes(routes)
    

    keep-alive

    缓存不活动的组件实例,而不是销毁它们,常见的例子就是tabs来回切换时个tab状态没有被初始化

    // include:值可为字符串、正则
    // 缓存组件名为a、b的组件,其余都不缓存
    <keep-alive include="a,b">
      <component :is="view"></component>
    </keep-alive>
    // exclude值和include相同,意思相反,匹配到的都不缓存
    // max 最多缓存组件数量
    
    // 配合meta
    <keep-alive>
        <router-view v-if="$route.meta.keepAlive"></router-view>
    </keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
    

    以上是keep-alive的使用,接下来一起了解一下keep-alive的实现,keep-alive本身也是个组件,它是Vue内部实现的一个组件,但它不会被渲染,keep-alive内部有一个属性{abstract: true}abstracttrue时会被忽略掉,初次渲染的时候keep-alive组件内部会将不满足条件的则直接返回vnode,满足缓存条件的组件通过对象cachevnode缓存起来,已经缓存过的直接将缓存的vnodecomponentInstance(组件实例)覆盖到目前的vnode上面,下面是代码

    // keep-alive.js
    export default {
        name: 'keep-alive',
        abstract: true,
        props: {
            include: patternTypes,
            exclude: patternTypes,
            max: [String, Number]
        },
        created () {
            this.cache = Object.create(null) // 存储缓存
            this.keys = []
        },
        destroyed () {
            for (const key in this.cache) {
                pruneCacheEntry(this.cache, key, this.keys) // 通过pruneCacheEntry清除缓存
            }
        },
        // 观测缓存变化通过pruneCache更新缓存
        mounted () {
            this.$watch('include', val => {
                pruneCache(this, name => matches(val, name))
            })
            this.$watch('exclude', val => {
                pruneCache(this, name => !matches(val, name))
            })
        },
        render () {
            const slot = this.$slots.default // 获取默认插槽
            const vnode: VNode = getFirstComponentChild(slot) // 拿到第一个元素
            const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
            if (componentOptions) {
            // check pattern
            const name: ?string = getComponentName(componentOptions)
            const { include, exclude } = this
            if (
                // not included
                (include && (!name || !matches(include, name))) ||
                // excluded
                (exclude && name && matches(exclude, name))
            ) {
                return vnode // 无法命中缓存返回vnode
            }
    
            const { cache, keys } = this
            const key: ?string = vnode.key == null
                // same constructor may get registered as different local components
                // so cid alone is not enough (#3269)
                ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
                : vnode.key
            if (cache[key]) { // 缓存已经存在
                vnode.componentInstance = cache[key].componentInstance
                // make current key freshest
                remove(keys, key)
                keys.push(key)
            } else { // 未缓存过
                cache[key] = vnode
                keys.push(key)
                // prune oldest entry
                if (this.max && keys.length > parseInt(this.max)) {
                    pruneCacheEntry(cache, keys[0], keys, this._vnode)
                }
            }
                vnode.data.keepAlive = true
            }
            return vnode || (slot && slot[0])
        }
    }
    // pruneCache
    function pruneCache (cache: VNodeCache, current: VNode, filter: Function) {
        for (const key in cache) {
            const cachedNode: ?VNode = cache[key]
                if (cachedNode) {
                const name: ?string = getComponentName(cachedNode.componentOptions)
                //  无法匹配则清除缓存 
                if (name && !filter(name)) {
                    if (cachedNode !== current) {
                        pruneCacheEntry(cachedNode)
                    }
                    cache[key] = null
                }
            }
        }
    } 
    // pruneCacheEntry
    function pruneCacheEntry (vnode: ?VNode) {
        if (vnode) {
            vnode.componentInstance.$destroy()
        }
    }
    

    第一次渲染时和普通渲染没有什么区别,但是后续渲染时,会将缓存的DOM插入父元素中

    // patch 过程会执行 createComponent
    function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
        let i = vnode.data
        if (isDef(i)) {
            const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
            if (isDef(i = i.hook) && isDef(i = i.init)) {
                i(vnode, false /* hydrating */) 
            }
            // 首次渲染vnode.componentInstance为undefined,后续渲染 vnode.componentInstance === cache[key].componentInstance
            if (isDef(vnode.componentInstance)) {
                initComponent(vnode, insertedVnodeQueue)
                insert(parentElm, vnode.elm, refElm) // 这部将缓存的DOM(vnode.elm)插入父元素中
                if (isTrue(isReactivated)) {
                    reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
                }
                return true
            }
        }
    }
    

    结语

    持续更新...(感觉有些模块可以抽出来细究)


    起源地下载网 » vue杂篇

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元