整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

前端必须懂的设计模式-代理模式

前端必须懂的设计模式-代理模式

  • 由于一个对象不想或者不能直接引用另外一个对象,所以需要通过通过一个称之为“代理”的第三者来实现间接引用
  • 代理模式就是为目标对象创造一个代理对象,在客户端和目标对象之间起到中介的作用
  • 这样就可以在代理对象里增加一些逻辑判断、调用前或调用后执行一些操作,从而实现了扩展目标的功能
  • 并且可以通过代理对象去掉客户不能看到的内容和服务或者添加客户需要的额外服务

通过引入一个新的对象(如小图片和远程代理对象)来实现对真实对象的操作或者将新的对 象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一 个对象,这就是代理模式的模式动机。

定义

代理模式(Proxy Pattern) :给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英 文叫做Proxy或Surrogate,它是一种对象结构型模式。

生活中的案例:

火车票代购、房产中介、律师、海外代购、明星经纪人

类图和时序图

代理模式包含如下角色:

  • Subject: 抽象主题角色
  • Proxy: 代理主题角色
  • RealSubject: 真实主题角色

类图

时序图

一个例子-明星经纪人

abstract class Star {
    abstract answerPhone(): void;
}

class Angelababy extends Star {
    public available: boolean=true;
    answerPhone(): void {
        console.log('你好,我是Angelababy.');
    }
}
class AngelababyAgent extends Star {
    constructor(private angelababy: Angelababy) {
        super();
    }
    answerPhone(): void {
        console.log('你好,我是Angelababy的经纪人.');
        if (this.angelababy.available) {
            this.angelababy.answerPhone();
        }
    }
}
let angelababyAgent=new AngelababyAgent(new Angelababy());
angelababyAgent.answerPhone();

场景

事件委托代理

  • 事件捕获指的是从document到触发事件的那个节点,即自上而下地去触发事件
  • 事件冒泡是自下而上地去触发事件
  • 绑定事件方法的第三个参数,就是控制事件触发顺序是否为事件捕获。true为事件捕获;false为事件冒泡,默认false。

<body>
    <ul id="list">
        <li>1</li>
        <li>2</li>
        <li>3</li>
    </ul>
<script>
  let list=document.querySelector('#list');
  list.addEventListener('click',event=>{
       alert(event.target.innerHTML);
  });     
</script>    
</body>

虚拟代理(图片预加载)

app.js

let express=require('express');
let path=require('path')
let app=express();
app.get('/images/loading.gif',function (req,res) {
    res.sendFile(path.join(__dirname,req.path));
});
app.get('/images/:name',function (req,res) {
    setTimeout(()=> {
        res.sendFile(path.join(__dirname,req.path));
    }, 2000);
});
app.get('/',function (req,res) {
    res.sendFile(path.resolve('index.html'));
});
app.listen(8080);

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        .bg-container {
            width: 600px;
            height: 400px;
            margin: 100px auto;
        }

        .bg-container #bg-image {
            width: 100%;
            height: 100%;
        }
    </style>
</head>

<body>
    <div id="background">
        <button data-src="/images/bg1.jpg">背景1</button>
        <button data-src="/images/bg2.jpg">背景2</button>
    </div>
    <div class="bg-container">
        <img id="bg-image" src="/images/bg1.jpg" />
    </div>
    <script>
        let container=document.querySelector('#background');

        class BackgroundImage {
            constructor() {
                this.bgImage=document.querySelector('#bg-image');
            }
            setSrc(src) {
                this.bgImage.src=src;
            }
        }
        class LoadingBackgroundImage { 
             static LOADING_URL=`/images/loading.gif`;
            constructor() {
                this.backgroundImage=new BackgroundImage();
            }
            setSrc(src) {
                this.backgroundImage.setSrc(LoadingBackgroundImage.LOADING_URL);
                let img=new Image();
                img.onload=()=> {
                    this.backgroundImage.setSrc(src);
                }
                img.src=src;
            }
        }
        let loadingBackgroundImage=new LoadingBackgroundImage();
        container.addEventListener('click', function (event) {
            let src=event.target.dataset.src;
            loadingBackgroundImage.setSrc(src + '?ts=' + Date.now());
        });
    </script>
</body>

</html>

虚拟代理(图片懒加载)

  • 当前可视区域的高度 window.innerHeight || document.documentElement.clientHeight
  • 元素距离可视区域顶部的高度 getBoundingClientRect().top
  • getBoundingClientRect
  • DOMRect 对象包含了一组用于描述边框的只读属性——left、top、right 和 bottom,单位为像素。除了 width 和 height 外的属性都是相对于视口的左上角位置而言的
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Lazy-Load</title>
    <style>
        .image {
            width: 300px;
            height: 200px;
            background-color: #CCC;
        }

        .image img {
            width: 100%;
            height: 100%;
        }
    </style>
</head>

<body>
    <div class="image-container">
        <div class="image">
            <img data-src="/images/bg1.jpg">
        </div>
        <div class="image">
            <img data-src="/images/bg2.jpg">
        </div>
        <div class="image">
            <img data-src="/images/bg1.jpg">
        </div>
        <div class="image">
            <img data-src="/images/bg2.jpg">
        </div>
        <div class="image">
            <img data-src="/images/bg1.jpg">
        </div>
        <div class="image">
            <img data-src="/images/bg2.jpg">
        </div>
        <div class="image">
            <img data-src="/images/bg1.jpg">
        </div>
        <div class="image">
            <img data-src="/images/bg2.jpg">
        </div>
        <div class="image">
            <img data-src="/images/bg1.jpg">
        </div>
        <div class="image">
            <img data-src="/images/bg2.jpg">
        </div>
    </div>
</body>
<script>
    const imgs=document.getElementsByTagName('img');
    const clientHeight=window.innerHeight || document.documentElement.clientHeight;
    let loadedIndex=0;
    function lazyload() {
        for (let i=loadedIndex; i < imgs.length; i++) {
            if (clientHeight - imgs[i].getBoundingClientRect().top > 0) {
                imgs[i].src=imgs[i].dataset.src;
                loadedIndex=i + 1;
            }
        }
    }
    lazyload();
    window.addEventListener('scroll', lazyload, false);
</script>
</html>

缓存代理

有些时候可以用空间换时间

正整数的阶乘(factorial)

一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1

const factorial=function f(num) {
    if (num===1) {
        return 1;
    } else {
        return (num * f(num - 1));
    }
}

const proxy=function (fn) {
    const cache={};  // 缓存对象
    return function (num) {
        if (num in cache) {
            return cache[num];   // 使用缓存代理
        }
        return cache[num]=fn.call(this, num);
    }
}

const proxyFactorial=proxy(factorial);
console.log(proxyFactorial(5));
console.log(proxyFactorial(5));
console.log(proxyFactorial(5));

斐波那契数列(Fibonacci sequence)

指的是这样一个数列:1、1、2、3、5、8、13、21、34。在数学上,斐波那契数列以如下被以递推的方法定义:F(1)=1,F(2)=1,F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)

let count=0;
function fib(n) {
    count++;
    return n <=2 ? 1 : fib(n - 1) + fib(n - 2);
}
var result=fib(10);
console.log(result, count);//55 110
let count=0;
const fibWithCache=(function () {
    let cache={};
    function fib(n) {
        count++;
        if (cache[n]) {
            return cache[n];
        }
        let result=n <=2 ? 1 : fib(n - 1) + fib(n - 2);
        cache[n]=result;
        return result;
    }
    return fib;
})();
var result=fibWithCache(10);
console.log(result, count);//55 17

防抖代理

  • 通过防抖代理优化可以把多次请求合并为一次,提高性能
  • 节流与防抖都是为了减少频繁触发事件回调
  • 节流(Throttle)是在某段时间内不管触发了多少次回调都只认第一个,并在第一次结束后执行回调
  • 防抖(Debounce)就是在某段时间不管触发了多少回调都只看最后一个

节流

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        #container {
            width: 200px;
            height: 400px;
            border: 1px solid red;
            overflow: auto;
        }

        #container .content {
            height: 4000px;
        }
    </style>
</head>

<body>
    <div id="container">
        <div class="content"></div>
    </div>
    <script>
        function throttle(callback, interval) {
            let last;
            return function () {
                let context=this;
                let args=arguments;
                let now=Date.now();
                if (last) {
                    if (now - last >=interval) {
                        last=now;
                        callback.apply(context, args);
                    }
                } else {
                    callback.apply(context, args);
                    last=now;
                }

            }
        }
        let lastTime=Date.now();
        const throttle_scroll=throttle(()=> {
            console.log('触发了滚动事件', (Date.now() - lastTime) / 1000);
        }, 1000);
        document.getElementById('container').addEventListener('scroll', throttle_scroll);
    </script>
</body>

</html>

防抖

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        #container {
            width: 200px;
            height: 400px;
            border: 1px solid red;
            overflow: auto;
        }

        #container .content {
            height: 4000px;
        }
    </style>
</head>

<body>
    <div id="container">
        <div class="content"></div>
    </div>
    <script>
        function debounce(callback, delay) {
            let timer;
            return function () {
                let context=this;
                let args=arguments;
                if (timer)
                    clearTimeout(timer);
                timer=setTimeout(()=> {
                    callback.apply(context, args);
                }, delay);
            }
        }
        let lastTime=Date.now();
        const throttle_scroll=debounce(()=> {
            console.log('触发了滚动事件', (Date.now() - lastTime) / 1000);
        }, 1000);
        document.getElementById('container').addEventListener('scroll', throttle_scroll);
    </script>
</body>

</html>

防抖案例 -未防抖

<body>
    <ul id="todos">
    </ul>
<script>
    let todos=document.querySelector('#todos');
    window.onload=function(){
        fetch('/todos').then(res=>res.json()).then(response=>{
            todos.innerHTML=response.map(item=>`<li "><input value="${item.id}" type="checkbox" ${item.completed?"checked":""}/>${item.text}</li>`).join('');
        });
    }
    function toggle(id){
       fetch(`/toggle/${id}`).then(res=>res.json()).then(response=>{
            console.log('response',response);
        });
    }
    todos.addEventListener('click',function(event){
        let checkbox=event.target;
        let id=checkbox.value;
        toggle(id);
    });
</script>
</body>

app.js

let express=require('express');
let app=express();
app.use(express.static(__dirname));
let todos=[
    {id: 1,text: 'a',completed: false},
    {id: 2,text: 'b',completed: false},
    {id: 3,text: 'c',completed: false},
];
app.get('/todos',function (req,res) {
    res.json(todos);
});
app.get('/toggle/:id',function (req,res) {
    let id=req.params.id;
    todos=todos.map(item=> {
        if (item.id==id) {
            item.completed=!item.completed;
        }
        return item;
    });
    res.json({code:0});
});
app.listen(8080);

防抖案例 -防抖

todos.html

<body>
    <ul id="todos">
    </ul>
    <script>
    let todos=document.querySelector('#todos');
    window.onload=function(){
        fetch('/todos').then(res=>res.json()).then(response=>{
            todos.innerHTML=response.map(item=>`<li "><input value="${item.id}" type="checkbox" ${item.completed?"checked":""}/>${item.text}</li>`).join('');
        });
    }
    function toggle(id){
       fetch(`/toggle/${id}`).then(res=>res.json()).then(response=>{
            console.log('response',response);
        });
    }
    let LazyToggle=(function(id){
        let ids=[];
        let timer;
        return function(id){
            ids.push(id);
            if(timer){
               clearTimeout(timer);
            }
            timer=setTimeout(function(){
                toggle(ids.join(','));
                ids=null;
                clearTimeout(timer);
                timer=null;
            },2000);
        }
    })();
    todos.addEventListener('click',function(event){
        let checkbox=event.target;
        let id=checkbox.value;
        LazyToggle(id);
    });
</script>

app.js

app.get('/toggle/:ids',function (req,res) {
    let ids=req.params.ids;
    ids=ids.split(',').map(item=>parseInt(item));
    todos=todos.map(item=> {
        if (ids.includes(item.id)) {
            item.completed=!item.completed;
        }
        return item;
    });
    res.json({code:0});
});

代理跨域

正向代理

  • 正向代理的对象是客户端,服务器端看不到真正的客户端
  • 通过公司代理服务器上网

反向代理

  • 反向代理的对象的服务端,客户端看不到真正的服务端
  • nginx代理应用服务器

proxy-server.js

const http=require('http');
const httpProxy=require('http-proxy');
//创建一个代理服务
const proxy=httpProxy.createProxyServer();
//创建http服务器并监听8888端口
let server=http.createServer(function (req, res) {
    //将用户的请求转发到本地9999端口上
    proxy.web(req, res, {
        target: 'http://127.0.0.1:9999'
    });
    //监听代理服务错误
    proxy.on('error', function (err) {
        console.log(err);
    });
});
server.listen(8888, '0.0.0.0');

real-server.js

const http=require('http');
let server=http.createServer(function (req, res) {
    res.end('9999');
});
server.listen(9999, '0.0.0.0');

代理跨域

  • nginx代理跨域
  • webpack-dev-server代理跨域
  • 客户端代理跨域
  • 当前的服务启动在origin(3000端口)上,但是调用的接口在target(4000端口)上
  • postMessage方法可以安全地实现跨源通信 otherWindow:其他窗口的一个引用 message:将要发送到其他window的数据message 将要发送到其他window的数据targetOrigin通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI
otherWindow.postMessage(message, targetOrigin, [transfer]);
  • data 从其他window中传递过来的对象
  • origin 调用postMessage时消息发送方窗口的origin
  • source 对发送消息的窗口对象的引用
window.addEventListener("message", receiveMessage, false);

origin.js

let express=require('express');
let app=express();
app.use(express.static(__dirname));
app.listen(3000);

target.js

let express=require('express');
let app=express();
let bodyParser=require('body-parser');
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(__dirname));
let users=[];
app.post('/register', function (req, res) {
    let body=req.body;
    let target=body.target;
    let callback=body.callback;
    let username=body.username;
    let password=body.password;
    let user={ username, password };
    let id=users.length==0 ? 1 : users[users.length - 1].id + 1;
    user.id=id;
    users.push(user);
    res.status(302);
    res.header('Location', `${target}?callback=${callback}&args=${id}`);
    res.end();
});
app.listen(4000);

reg.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <script type="text/javascript">
        window.addEventListener('message', function (event) {
            console.log(event.data);

            if (event.data.receiveId) {
                alert('用户ID=' + event.data.receiveId);
            }
        })
    </script>
    <iframe name="proxyIframe" id="proxyIframe" frameborder="0"></iframe>
    <form action="http://localhost:4000/register" method="POST" target="proxyIframe">
        <input type="hidden" name="callback" value="receiveId">
        <input type="hidden" name="target" value="http://localhost:3000/target.html">
        用户名<input type="text" name="username" />
        密码<input type="text" name="password" />
        <input type="submit" value="提交">
    </form>
</body>

</html>

target.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <script>
        window.onload=function () {
            var query=location.search.substr(1).split('&');
            let callback, args;
            for (let i=0, len=query.length; i < len; i++) {
                let item=query[i].split('=');
                if (item[0]=='callback') {
                    callback=item[1];
                } else if (item[0]=='args') {
                    args=item[1];
                }
            }
            try {
                window.parent.postMessage({ [callback]: args }, '*');
            } catch (error) {
                console.log(error);
            }
        }
    </script>
</body>

</html>

$.proxy

  • 接受一个函数,然后返回一个新函数,并且这个新函数始终保持了特定的上下文语境。
  • jQuery.proxy( function, context ) function为执行的函数,content为函数的上下文this值会被设置成这个object对象
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>jquery proxy</title>
</head>

<body>
    <button id="btn">点我变红</button>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    <script>
        let btn=document.getElementById('btn');
        btn.addEventListener('click', function () {
            setTimeout($.proxy((function () {
                $(this).css('color', 'red');
            }), this), 1000);
        });    
    </script>
</body>

</html>
function proxy(fn, context) {
    return function () {
       return fn.call(context, arguments);
    }
}

Proxy

  • Proxy 用于修改某些操作的默认行为
  • Proxy 可以理解成,在目标对象之前架设一层拦截,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
  • Proxy 这个词的原意是代理,用在这里表示由它来代理某些操作,可以译为代理器
  • Proxy
  • defineProperty
let wang={
    name: 'wanglaoshi',
    age: 29,
    height:165
}
let wangMama=new Proxy(wang,{
    get(target,key) {
        if (key=='age') {
            return wang.age-1;
        } else if (key=='height') {
            return wang.height-5;
        }
        return target[key];
    },
    set(target,key,val) {
        if (key=='boyfriend') {
            let boyfriend=val;
            if (boyfriend.age>40) {
                throw new Error('太老');
            } else if (boyfriend.salary<20000) {
                throw new Error('太穷');
            } else {
                target[key]=val;
                return true;
            }
        }
    }
});
console.log(wangMama.age);
console.log(wangMama.height);
wangMama.boyfriend={
    age: 41,
    salary:3000
}

Vue2和Vue3

Vue2 中的变化侦测实现对 Object 及 Array 分别进行了不同的处理,Object 使用了 Object.defineProperty API,Array使用了拦截器对 Array 原型上的能够改变数据的方法进行拦截。虽然也实现了数据的变化侦测,但存在很多局限 ,比如对象新增属性无法被侦测,以及通过数组下边修改数组内容,也因此在 Vue2 中经常会使用到 $set 这个方法对数据修改,以保证依赖更新。

Vue3 中使用了 es6 的 Proxy API对数据代理,没有像 Vue2 中对原数据进行修改,只是加了代理包装,因此首先性能上会有所改善。其次解决了 Vue2 中变化侦测的局限性,可以不使用 $set 新增的对象属性及通过下标修改数组都能被侦测到。

对比

代理模式 VS 适配器模式

适配器提供不同接口,代理模式提供一模一样的接口

代理模式 VS 装饰器模式

装饰器模式原来的功能不变还可以使用,代理模式改变原来的功能

是解决方案实际上更像是webpack的插件索引。

写这一篇的目的是为了形成一个所以,将来要用时直接来查找即可。

1.自动构建HTML,可压缩空格,可给引用的js加版本号或随机数:html-webpack-plugin

解决方案:使用插件 html-webpack-plugin

webpack.config.js如下:

module.exports={

entry: './src/app.js',

output: {

path: __dirname + '/dist',

filename: 'app.bundle.js'

},

plugins: [new HtmlWebpackPlugin({

template: './src/模板文件.html',

filename: '构建的.html',

minify: {

collapseWhitespace: true,

},

hash: true,

})]

};

注意要有path,因为这个输出的html需要知道输出目录

2.处理CSS:css-loader与style-loader

loader用于对模块的源代码进行预处理转换。

解决方案:使用css-loader,style-loader

看一下项目结构:

此时运行webpack命令会抛出错误:

接下来安装 css-loader 和 style-loader

npm install --save-dev css-loader style-loader

再修改webpack.config.js为:

这其中rules数组就是loader用来的匹配和转换资源的规则数组。

test代表匹配需转换文件的正则表达式,而图中表示匹配所有以css结尾的文件。

而use数组代表用哪些loader去处理这些匹配到的文件。

此时再运行webpack,打包后的文件bundle.js就包含了css代码。

其中css-loader负责加载css,打包css到js中。

而style-loader负责生成:在js运行时,将css代码通过style标签注入到dom中。

3.处理LESS:less-loade与less

解决方案:使用less-loader

但是用less-loader只是将LESS代码转换为css代码。如果要打包文件到js中,还是需要用到上面提到的css-loader和style-loader。

看一下项目结构:

然后app.js的代码为:

import styles from './app.less';

console.info('我是一个js文件123')

为了解决这种情况,首先要安装 less-loader,而less-loader是基于less的,所以也要安装less。

npm i --save-dev less less-loader

修改webpack.config.js为:

module: {

rules: [

{

test: /\.less$/,

use: [ 'style-loader', 'css-loader', 'less-loader' ]

}

]

}

4.提取css代码到css文件中: extract-text-webpack-plugin

很多时候我们想要的效果并不是想要把几个LESS或者CSS处理好后,打包到一个js中,而是想要把它打包到一个css文件中。

此时就有了插件 extract-text-webpack-plugin。

首先进行安装

npm i --save-dev extract-text-webpack-plugin

然后修改webpack.config.js为:

红框中为新加或修改的配置

与原配置对比可以发现,比html-webpack-plugin这个插件多做了一步,就是在匹配和转换规则里面的use中使用了ExtractTextPlugin.extract。

注意这里的fallback表示,在提取文件失败后,将继续使用style-loader去打包到js中。

此时运行webpack

可以发现输出目录build下生成了一个style.css文件,也就是我们在webpack.config.js中期望生成的文件,并且在生成的demo.html中被引用了。

5.开发环境下的服务器搭建:webpack-dev-server

webpack-dev-server可以在本地搭建一个简单的开发环境用的服务器,自动打开浏览器,而且还可以达到webpack -watch的效果。

首先安装一下:

npm i -g webpack-dev-server

npm i --save-dev webpack-dev-server

这里不需要改动webpack.config.js,直接运行命令

webpack-dev-server

查看控制台输出:

控制台输出

显示项目运行在http://localhost:8080/

webpack的输出目录的路径在/下面

并且这个服务器会自动识别输出目录下名为index的HTML文件,而我们之前输出的文件名为demo.html。

所以还需要将之前html-webpack-plugin中配置的filename改为index.html,或者直接用http://localhost:8080/demo.html也行。

当我们修改了源代码后,打开的网页还会自动更新。

为了更灵活的应用开发环境的服务器,也可以在webpack.config.js中加入如下代码:

devServer配置功能

port修改端口为8787,而不是默认的8080。

open为true表示会自动打开浏览器,而不是需要我们再手动打开浏览器并在里面输入http://localhost:8080。

compress对本地server返回的文件提供gzip压缩

index指定网站首页映射的文件,默认为index.html

6.解析ES6代码:babel-core babel-preset-env babel-loader

这里说是ES6,实际上可以认为是ECMAScript的高版本代码,只是代指而已。

babel的作用是将浏览器还未支持的这些高版本js代码转换成可以被指定浏览器支持的js代码。

这里列出可以转换的大致语法:

babel-preset-env支持的转换

那么首先就需要安装babel

npm install babel-core babel-preset-env --save-dev

然后,为了和webpack结合起来,要用到babel-loader

npm install babel-loader --save-dev

然后在webpack.config.js的rules数组中增加以下代码:

{

test: /\.js$/,

exclude: /(node_modules)/,

use: {

loader: 'babel-loader',

options: {

presets: ['env']

}

}

}

这行代码的意思是用babel-loader解析除了node_modules文件下的所有js文件。

而babel-loader就是用babel去解析js代码。

options的内容类似于.babelrc文件的配置,有了这个就不需要.babelrc文件了。

presets表示预处理器,现在的babel不像以前需要很多预处理器了,只需要env这一个就够了。

修改之前的app.js中的代码为:

console.info('我是一个js文件123')

const doSomething=()=> {

console.info('do do do')

}

使用webpack命令后,可以看到我们最后的打包js文件中代码变成了这样:

7.解析ES6新增的对象函数:babel-polyfill

以下为这些新增函数:

安装:

npm install --save-dev babel-polyfill

为了确保babel-polyfill被最先加载和解析,所以一般都是讲babel-polyfill在最开始的脚本中引入。

而在webpack中,就是在放到entry中,所以需要修改webpack.config.js中的配置为:

8.解析react的jsx语法:babel-preset-react

安装

npm install --save-dev babel-preset-react

配置:

这里是匹配所有以js或者jsx结尾的文件,并用 babel-preset-env和babel-preset-react进行解析

9.转换相对路径到绝度路径:nodejs的path模块

这里首先介绍一下nodejs的path模块的一个功能:resolve。

将相对路径转换为绝对路径。

在最开始引用path模块

var path=require('path');

然后可以在输出设置那里修改代码为:

output: {

path: path.resolve(__dirname, 'build'),

filename: 'bundle.js'

},

和我们原来的代码没有任何区别。

10.给文件加上hash值:[chunkhash],[hash]

hash和chunkhash有区别,hash的话输出的文件用的都是同一个hash值,而chunkhash的话是根据模块来计算的,每个输出文件的hash值都不一样。

直接将输出文件改为

output: {

path: path.resolve(__dirname, 'build'),

filename: 'bundle.[chunkhash].js'

},

[chunkhash]就代表一串随机的hash值

11.清空输出文件夹之前的输出文件:clean-webpack-plugin

当我们像上面一样不断改变输出文件时,之前的输出文件并没有去掉。

为了解决这个问题就需要clean-webpack-plugin。

首先安装

npm i clean-webpack-plugin --save-dev

然后引用插件,并声明每次生成输出需要清空的文件夹

var CleanWebpackPlugin=require('clean-webpack-plugin');

var pathsToClean=[

'build',

]

再在插件配置中加入:

new CleanWebpackPlugin(pathsToClean)

12.模块热替换:NamedModulesPlugin和HotModuleReplacementPlugin

之前的webpack-dev-server提供了监听功能,只要代码改变,浏览器就会刷新。

但是模块热替换是不会刷新浏览器,只刷新修改到的那部分模块。

模块热替换无需安装。

首先需要引入模块

var webpack=require('webpack')

其实插件中加入:

new webpack.NamedModulesPlugin(),

new webpack.HotModuleReplacementPlugin()

此时运行webpack可能会报错,我们需要把之前在输出环境中写的[chunkhash]改为[hash]

13.环境变量

可以在脚本中这么写:

"scripts": {

"dev": "webpack-dev-server",

"prod": "set NODE_ENV=production && webpack -p"

},

这样在webpack.config.js中这样修改上面的东西:

if (isProduction) {

config.output.filename='bundle.[chunkhash].js'

} else {

config.plugins.push(new webpack.NamedModulesPlugin())

config.plugins.push(new webpack.HotModuleReplacementPlugin())

}

这样就可以根据环境的不同来运行不同的配置

14.跨平台使用环境变量: cross-env

上述设置环境变量的脚本中只有在window下才有效,在linux和mac上需要使用

"prod": "NODE_ENV=production webpack -p"

为了解决这个问题,使得不同平台的人能公用一套代码,我们可以使用cross-env。

首先进行安装:

npm i --save-dev cross-env

然后命令直接使用类似于mac上的用法即可

"prod": "cross-env NODE_ENV=production webpack -p"

15.处理图片路径: file-loader和html-loader

file-loader可以用来处理图片和字体文件在css文件中的路径问题,输出的css文件中会引用输出的文件地址。

html-loader可以用来处理html中,比如img元素的图片路径问题。

首先安装

npm i --save-dev file-loader html-loader

配置:

{

test: /\.(gif|png|jpe?g|svg)$/i,

use: {

loader: 'file-loader',

options: {

name: '[name].[ext]',

outputPath: 'src/images/'

}

}

},

{

test: /\.html$/,

use: [{

loader: 'html-loader',

options: {

minimize: true

}

}],

}

16.图片压缩:image-webpack-loader

安装:

npm i --save-dev image-webpack-loader

配置:

{

test: /\.(gif|png|jpe?g|svg)$/i,

use: [{

loader: 'file-loader',

options: {

name: '[name].[ext]',

outputPath: 'images/'

}

},

{

loader: 'image-webpack-loader',

options: {

bypassOnDebug: true,

}

}

]

},

这里的options中也可以具体配置各个图片类型的压缩质量

17.定位源文件代码:source-map

如果我们用web-dev-server运行我们的输出文件,发现其中有些BUG,然后打开开发者工具取定位文件的时候,只会定位到我们的输出文件。

而这些输出文件是经过处理的,我们只有找到我们的源文件代码,然后进行相应的修改才能解决问题。

于是这里我们需要用到source-map。

很简单,在webpack.config.js中加入如下配置即可:

devtool: 'source-map',

就这么简单,还不需要安装什么插件。

但是这只对js有效,如果我们的css出现错误了呢,答案就是如下配置:

在这些loader后面加上?sourceMap即可

18.分离生产环境和开发环境的配置文件

之前我们通过在命令中设置环境变量,并且通过环境变量来判断环境来进行不同的配置。

现在我们用官方推荐的方法来分离生产环境和开发环境的配置文件。

我们将webpack.config.js分为三个文件

  • webpack.common.js

  • webpack.dev.js

  • webpack.prod.js

其中webpack.common.config.js为生产环境和开发环境共有的配置,dev为开发环境独有的配置,prod为生成环境独有的配置。

而想要合成真正的配置文件,还需要一个工具:webpack-merge。

npm install --save-dev webpack-merge

以下是我们之前的webpack.config.js代码:

var ExtractTextPlugin=require('extract-text-webpack-plugin')

var HtmlWebpackPlugin=require('html-webpack-plugin')

var CleanWebpackPlugin=require('clean-webpack-plugin')

var path=require('path')

var webpack=require('webpack')

var pathsToClean=[

'build',

]

var isProduction=process.env.NODE_ENV==='production'

var config={

entry: ['babel-polyfill', './src/app.js'],

output: {

path: path.resolve(__dirname, 'build'),

filename: '[name].[hash].js'

},

devtool: 'source-map',

devServer: {

port: 8787,

open: true,

compress: true,

index: 'demo.html'

},

plugins: [

new HtmlWebpackPlugin({

template: './template/index.html',

filename: 'demo.html',

minify: {

collapseWhitespace: true,

},

hash: true

}),

new ExtractTextPlugin({ filename: 'style.css', allChunks: false }),

new CleanWebpackPlugin(pathsToClean)

],

module: {

rules: [{

test: /\.css$/,

use: ExtractTextPlugin.extract({

fallback: 'style-loader',

use: ['css-loader?sourceMap']

})

},

{

test: /\.less$/,

use: ExtractTextPlugin.extract({

fallback: 'style-loader',

use: ['css-loader?sourceMap', 'less-loader?sourceMap']

})

},

{

test: /\.jsx?$/,

exclude: /(node_modules)/,

use: {

loader: 'babel-loader',

options: {

presets: ['env', 'react']

}

}

},

{

test: /\.(gif|png|jpe?g|svg)$/i,

use: [{

loader: 'file-loader',

options: {

name: '[name].[ext]',

outputPath: 'images/'

}

},

{

loader: 'image-webpack-loader',

options: {

bypassOnDebug: true,

}

}

]

},

{

test: /\.html$/,

use: [{

loader: 'html-loader',

options: {

minimize: true

}

}],

}

]

}

};

if (isProduction) {

config.output.filename='[name].[chunkhash].js'

} else {

config.plugins.push(new webpack.NamedModulesPlugin())

config.plugins.push(new webpack.HotModuleReplacementPlugin())

}

module.exports=config

接下来分为三个文件,webpack.common.js:

var ExtractTextPlugin=require('extract-text-webpack-plugin')

var HtmlWebpackPlugin=require('html-webpack-plugin')

var CleanWebpackPlugin=require('clean-webpack-plugin')

var path=require('path')

var webpack=require('webpack')

var pathsToClean=[

'build',

]

var isProduction=process.env.NODE_ENV==='production'

module.exports={

entry: ['babel-polyfill', './src/app.js'],

output: {

path: path.resolve(__dirname, 'build'),

filename: '[name].[chunkhash].js'

},

plugins: [

new HtmlWebpackPlugin({

template: './template/index.html',

filename: 'demo.html',

minify: {

collapseWhitespace: true,

},

hash: isProduction

}),

new ExtractTextPlugin({ filename: '[name].[contenthash].css', allChunks: false }),

new CleanWebpackPlugin(pathsToClean)

],

module: {

rules: [{

test: /\.jsx?$/,

exclude: /(node_modules)/,

use: {

loader: 'babel-loader',

options: {

presets: ['env', 'react']

}

}

},

{

test: /\.(gif|png|jpe?g|svg)$/i,

use: [{

loader: 'file-loader',

options: {

name: '[name].[ext]',

outputPath: 'images/'

}

},

{

loader: 'image-webpack-loader',

options: {

bypassOnDebug: true,

}

}

]

},

{

test: /\.html$/,

use: [{

loader: 'html-loader',

options: {

minimize: true

}

}],

}

]

}

};

然后是webpack.dev.js:

const merge=require('webpack-merge');

const common=require('./webpack.common.js');

const webpack=require('webpack');

const ExtractTextPlugin=require('extract-text-webpack-plugin')

module.exports=merge(common, {

output: {

filename: '[name].[hash].js'

},

devtool: 'source-map',

devServer: {

port: 8787,

open: true,

compress: true,

index: 'demo.html'

},

plugins: [

new webpack.NamedModulesPlugin(),

new webpack.HotModuleReplacementPlugin()

],

module: {

rules: [{

test: /\.css$/,

use: ExtractTextPlugin.extract({

fallback: 'style-loader',

use: ['css-loader?sourceMap']

})

},

{

test: /\.less$/,

use: ExtractTextPlugin.extract({

fallback: 'style-loader',

use: ['css-loader?sourceMap', 'less-loader?sourceMap']

})

}

]

}

});

最后是webpack.prod.js:

const merge=require('webpack-merge');

const common=require('./webpack.common.js');

const ExtractTextPlugin=require('extract-text-webpack-plugin')

module.exports=merge(common, {

module: {

rules: [{

test: /\.css$/,

use: ExtractTextPlugin.extract({

fallback: 'style-loader',

use: ['css-loader']

})

},

{

test: /\.less$/,

use: ExtractTextPlugin.extract({

fallback: 'style-loader',

use: ['css-loader', 'less-loader']

})

}

]

}

});

然后修改一下package.json中的脚本即可。

"scripts": {

"dev": "webpack-dev-server --config webpack.dev.js",

"prod": "cross-env NODE_ENV=production webpack -p --config webpack.prod.js"

},

总结:

各个插件以及loader的玩法还有很多,这里不具体介绍。

对于MVVM的理解

MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。

  • MVVM 是 Model-View-ViewModel 的缩写
  • Model: 代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。我们可以把Model称为数据层,因为它仅仅关注数据本身,不关心任何行为
  • View: 用户操作界面。当ViewModel对Model进行更新的时候,会通过数据绑定更新到View
  • ViewModel:业务逻辑层,View需要什么数据,ViewModel要提供这个数据;View有某些操作,ViewModel就要响应这些操作,所以可以说它是Model for View.
  • 总结:MVVM模式简化了界面与业务的依赖,解决了数据频繁更新。MVVM 在使用当中,利用双向绑定技术,使得 Model 变化时,ViewModel 会自动更新,而 ViewModel 变化时,View 也会自动变化。

2 请详细说下你对vue生命周期的理解

答:总共分为8个阶段创建前/后,载入前/后,更新前/后,销毁前/后

生命周期是什么

Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是Vue的生命周期

各个生命周期的作用

生命周期描述beforeCreate组件实例被创建之初,组件的属性生效之前created组件实例已经完全创建,属性也绑定,但真实dom还没有生成,$el还不可用beforeMount在挂载开始之前被调用:相关的 render 函数首次被调用mountedel 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子beforeUpdate组件数据更新之前调用,发生在虚拟 DOM 打补丁之前update组件数据更新之后activitedkeep-alive专属,组件被激活时调用deadctivatedkeep-alive专属,组件被销毁时调用beforeDestory组件销毁前调用destoryed组件销毁后调用

image

由于Vue会在初始化实例时对属性执行getter/setter转化,所以属性必须在data对象上存在才能让Vue将它转换为响应式的。Vue提供了$set方法用来触发视图更新

export default {
    data(){
        return {
            obj: {
                name: 'fei'
            }
        }
    },
    mounted(){
        this.$set(this.obj, 'sex', 'man')
    }

}

什么是vue生命周期?

  • 答:Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。

vue生命周期的作用是什么?

  • 答:它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。

vue生命周期总共有几个阶段?

  • 答:它可以总共分为8个阶段:创建前/后、载入前/后、更新前/后、销毁前/销毁后。

第一次页面加载会触发哪几个钩子?

  • 答:会触发下面这几个beforeCreatecreatedbeforeMountmounted

DOM 渲染在哪个周期中就已经完成?

  • 答:DOM 渲染在 mounted 中就已经完成了

3 Vue实现数据双向绑定的原理:Object.defineProperty()

  • vue实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() 来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty() 将它们转为 getter/setter。用户看不到 getter/setter,但是在内部它们让 Vue追踪依赖,在属性被访问和修改时通知变化。
  • vue的数据双向绑定 将MVVM作为数据绑定的入口,整合ObserverCompileWatcher三者,通过Observer来监听自己的model的数据变化,通过Compile来解析编译模板指令(vue中是用来解析 {{}}),最终利用watcher搭起observerCompile之间的通信桥梁,达到数据变化 —>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。

4 Vue组件间的参数传递

父组件与子组件传值

父组件传给子组件:子组件通过props方法接受数据;

  • 子组件传给父组件:$emit 方法传递参数

非父子组件间的数据传递,兄弟组件传值

eventBus,就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。项目比较小时,用这个比较合适(虽然也有不少人推荐直接用VUEX,具体来说看需求)

5 Vue的路由实现:hash模式 和 history模式

  • hash模式:在浏览器中符号“#”,#以及#后面的字符称之为hash,用 window.location.hash 读取。特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。
  • history模式:history采用HTML5的新特性;且提供了两个新方法:pushState()replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更

5 vue路由的钩子函数

首页可以控制导航跳转,beforeEachafterEach等,一般用于页面title的修改。一些需要登录才能调整页面的重定向功能。

  • beforeEach主要有3个参数tofromnext
  • toroute即将进入的目标路由对象。
  • fromroute当前导航正要离开的路由。
  • nextfunction一定要调用该方法resolve这个钩子。执行效果依赖next方法的调用参数。可以控制网页的跳转

6 vuex是什么?怎么使用?哪种功能场景使用它?

  • 只用来读取的状态集中放在store中;改变状态的方式是提交mutations,这是个同步的事物;异步逻辑应该封装在action中。
  • main.js引入store,注入。新建了一个目录store… export
  • 场景有:单页应用中,组件之间的状态、音乐播放、登录状态、加入购物车

image

  • stateVuex 使用单一状态树,即每个应用将仅仅包含一个store 实例,但单一状态树和模块化并不冲突。存放的数据状态,不可以直接修改里面的数据。
  • mutationsmutations定义的方法动态修改Vuexstore 中的状态或数据
  • getters:类似vue的计算属性,主要用来过滤一些数据。
  • actionactions可以理解为通过将mutations里面处里数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据。view 层通过 store.dispath 来分发 action

image

modules:项目特别复杂的时候,可以让每一个模块拥有自己的statemutationactiongetters,使得结构非常清晰,方便管理

image

7 v-if 和 v-show 区别

  • 答:v-if按照条件是否渲染,v-showdisplayblocknone

8$route和$router的区别

  • $route是“路由信息对象”,包括pathparamshashqueryfullPathmatchedname等路由信息参数。
  • $router是“路由实例”对象包括了路由的跳转方法,钩子函数等

9 如何让CSS只在当前组件中起作用?

将当前组件的<style>修改为<style scoped>

10<keep-alive></keep-alive>的作用是什么?

keep-alive可以实现组件缓存,当组件切换时不会对当前组件进行卸载

  • <keep-alive></keep-alive> 包裹动态组件时,会缓存不活动的组件实例,主要用于保留组件状态或避免重新渲染

比如有一个列表和一个详情,那么用户就会经常执行打开详情=>返回列表=>打开详情…这样的话列表和详情都是一个频率很高的页面,那么就可以对列表组件使用<keep-alive></keep-alive>进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是重新渲染

  • 常用的两个属性include/exclude,允许组件有条件的进行缓存
  • 两个生命周期activated/deactivated,用来得知当前组件是否处于活跃状态

11 指令v-el的作用是什么?

提供一个在页面上已存在的 DOM元素作为 Vue实例的挂载目标.可以是 CSS 选择器,也可以是一个 HTMLElement 实例,

12 在Vue中使用插件的步骤

  • 采用ES6import ... from ...语法或CommonJSrequire()方法引入插件
  • 使用全局方法Vue.use( plugin )使用插件,可以传入一个选项对象Vue.use(MyPlugin, { someOption: true })

13 请列举出3个Vue中常用的生命周期钩子函数?

  • created: 实例已经创建完成之后调用,在这一步,实例已经完成数据观测, 属性和方法的运算, watch/event事件回调. 然而, 挂载阶段还没有开始, $el属性目前还不可见
  • mounted: el被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。如果 root 实例挂载了一个文档内元素,当 mounted被调用时 vm.$el 也在文档内。
  • activated: keep-alive组件激活时调用

14 vue-cli 工程技术集合介绍

问题一:构建的 vue-cli 工程都到了哪些技术,它们的作用分别是什么?

  • vue.jsvue-cli工程的核心,主要特点是 双向数据绑定 和 组件系统。
  • vue-routervue官方推荐使用的路由框架。
  • vuex:专为 Vue.js 应用项目开发的状态管理器,主要用于维护vue组件间共用的一些 变量 和 方法。
  • axios( 或者 fetchajax ):用于发起 GET 、或 POSThttp请求,基于 Promise 设计。
  • vuex等:一个专为vue设计的移动端UI组件库。
  • 创建一个emit.js文件,用于vue事件机制的管理。
  • webpack:模块加载和vue-cli工程打包器。

问题二:vue-cli 工程常用的 npm 命令有哪些?

  • 下载 node_modules 资源包的命令:
npm install
  • 启动 vue-cli 开发环境的 npm命令:
npm run dev
  • vue-cli 生成 生产环境部署资源 的 npm命令:
npm run build
  • 用于查看 vue-cli 生产环境部署资源文件大小的 npm命令:
npm run build --report

在浏览器上自动弹出一个 展示 vue-cli 工程打包后 app.jsmanifest.jsvendor.js 文件里面所包含代码的页面。可以具此优化 vue-cli 生产环境部署的静态资源,提升 页面 的加载速度

15 NextTick

nextTick可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM

16 vue的优点是什么?

  • 低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变
  • 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑
  • 可测试。界面素来是比较难于测试的,而现在测试可以针对ViewModel来写

17 路由之间跳转?

声明式(标签跳转)

<router-link :to="index">

编程式( js跳转)

router.push('index')

18 实现 Vue SSR

其基本实现原理

  • app.js 作为客户端与服务端的公用入口,导出 Vue 根实例,供客户端 entry 与服务端 entry 使用。客户端 entry 主要作用挂载到 DOM 上,服务端 entry 除了创建和返回实例,还进行路由匹配与数据预获取。
  • webpack 为客服端打包一个 Client Bundle ,为服务端打包一个 Server Bundle
  • 服务器接收请求时,会根据 url,加载相应组件,获取和解析异步数据,创建一个读取 Server BundleBundleRenderer,然后生成 html 发送给客户端。
  • 客户端混合,客户端收到从服务端传来的 DOM 与自己的生成的 DOM 进行对比,把不相同的 DOM 激活,使其可以能够响应后续变化,这个过程称为客户端激活 。为确保混合成功,客户端与服务器端需要共享同一套数据。在服务端,可以在渲染之前获取数据,填充到 stroe 里,这样,在客户端挂载到 DOM 之前,可以直接从 store里取数据。首屏的动态数据通过 window.__INITIAL_STATE__发送到客户端

Vue SSR 的实现,主要就是把 Vue 的组件输出成一个完整 HTML, vue-server-renderer 就是干这事的

  • Vue SSR需要做的事多点(输出完整 HTML),除了complier -> vnode,还需如数据获取填充至 HTML、客户端混合(hydration)、缓存等等。相比于其他模板引擎(ejs, jade 等),最终要实现的目的是一样的,性能上可能要差点

19 Vue 组件 data 为什么必须是函数

  • 每个组件都是 Vue 的实例。
  • 组件共享 data 属性,当 data 的值是同一个引用类型的值时,改变其中一个会影响其他

20 Vue computed 实现

  • 建立与其他属性(如:dataStore)的联系;
  • 属性改变后,通知计算属性重新计算

实现时,主要如下

  • 初始化 data, 使用 Object.defineProperty 把这些属性全部转为 getter/setter
  • 初始化 computed, 遍历 computed 里的每个属性,每个 computed 属性都是一个 watch 实例。每个属性提供的函数作为属性的 getter,使用 Object.defineProperty 转化。
  • Object.defineProperty getter 依赖收集。用于依赖发生变化时,触发属性重新计算。
  • 若出现当前 computed 计算属性嵌套其他 computed 计算属性时,先进行其他的依赖收集

21 Vue complier 实现

  • 模板解析这种事,本质是将数据转化为一段 html ,最开始出现在后端,经过各种处理吐给前端。随着各种 mv* 的兴起,模板解析交由前端处理。
  • 总的来说,Vue complier 是将 template 转化成一个 render 字符串。

可以简单理解成以下步骤:

  • parse 过程,将 template 利用正则转化成AST 抽象语法树。
  • optimize 过程,标记静态节点,后 diff 过程跳过静态节点,提升性能。
  • generate 过程,生成 render 字符串

22 怎么快速定位哪个组件出现性能问题

timeline 工具。大意是通过 timeline 来查看每个函数的调用时常,定位出哪个函数的问题,从而能判断哪个组件出了问题

23 开发中常用的指令有哪些

  • v-model :一般用在表达输入,很轻松的实现表单控件和数据的双向绑定
  • v-html: 更新元素的 innerHTML
  • v-showv-if: 条件渲染, 注意二者区别

使用了v-if的时候,如果值为false,那么页面将不会有这个html标签生成。v-show则是不管值为true还是false,html元素都会存在,只是CSS中的display显示或隐藏

  • v-on : click: 可以简写为@click,@绑定一个事件。如果事件触发了,就可以指定事件的处理函数
  • v-for:基于源数据多次渲染元素或模板块
  • v-bind: 当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM

语法:v-bind:title="msg"简写::title="msg"

24 Proxy 相比于 defineProperty 的优势

Object.defineProperty() 的问题主要有三个:

  • 不能监听数组的变化
  • 必须遍历对象的每个属性
  • 必须深层遍历嵌套的对象

Proxy 在 ES2015 规范中被正式加入,它有以下几个特点

  • 针对对象:针对整个对象,而不是对象的某个属性,所以也就不需要对 keys 进行遍历。这解决了上述 Object.defineProperty() 第二个问题
  • 支持数组:Proxy 不需要对数组的方法进行重载,省去了众多 hack,减少代码量等于减少了维护成本,而且标准的就是最好的。

除了上述两点之外,Proxy 还拥有以下优势:

  • Proxy 的第二个参数可以有 13 种拦截方法,这比起 Object.defineProperty() 要更加丰富
  • Proxy 作为新标准受到浏览器厂商的重点关注和性能优化,相比之下 Object.defineProperty() 是一个已有的老方法。

25 vue-router 有哪几种导航守卫?

  • 全局守卫
  • 路由独享守卫
  • 路由组件内的守卫

全局守卫

vue-router全局有三个守卫

  • router.beforeEach 全局前置守卫 进入路由之前
  • router.beforeResolve 全局解析守卫(2.5.0+) 在beforeRouteEnter调用之后调用
  • router.afterEach 全局后置钩子 进入路由之后
// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next)=> { 
  next();
});
router.beforeResolve((to, from, next)=> {
  next();
});
router.afterEach((to, from)=> {
  console.log('afterEach 全局后置钩子');
});

路由独享守卫

如果你不想全局配置守卫的话,你可以为某些路由单独配置守卫

const router=new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next)=> { 
        // 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
        // ...
      }
    }
  ]
})

路由组件内的守卫

  • beforeRouteEnter 进入路由前, 在路由独享守卫后调用 不能 获取组件实例 this,组件实例还没被创建
  • beforeRouteUpdate (2.2) 路由复用同一个组件时, 在当前路由改变,但是该组件被复用时调用 可以访问组件实例 this
  • beforeRouteLeave 离开当前路由时, 导航离开该组件的对应路由时调用,可以访问组件实例 this

26 组件之间的传值通信

组件之间通讯分为三种: 父传子、子传父、兄弟组件之间的通讯

1. 父组件给子组件传值

  • 使用props,父组件可以使用props向子组件传递数据。
  • 父组件vue模板father.vue:
<template>
    <child :msg="message"></child>
</template>

<script>
import child from './child.vue';
export default {
    components: {
        child
    },
    data () {
        return {
            message: 'father message';
        }
    }
}
</script>

子组件vue模板child.vue:

<template>
    <div>{{msg}}</div>
</template>

<script>
export default {
    props: {
        msg: {
            type: String,
            required: true
        }
    }
}
</script>

2. 子组件向父组件通信

父组件向子组件传递事件方法,子组件通过$emit触发事件,回调给父组件

父组件vue模板father.vue:

<template>
    <child @msgFunc="func"></child>
</template>

<script>
import child from './child.vue';
export default {
    components: {
        child
    },
    methods: {
        func (msg) {
            console.log(msg);
        }
    }
}
</script>

子组件vue模板child.vue:

<template>
    <button @click="handleClick">点我</button>
</template>

<script>
export default {
    props: {
        msg: {
            type: String,
            required: true
        }
    },
    methods () {
        handleClick () {
            //........
            this.$emit('msgFunc');
        }
    }
}
</script>

3. 非父子, 兄弟组件之间通信

vue2中废弃了broadcast广播和分发事件的方法。父子组件中可以用props和$emit()。如何实现非父子组件间的通信,可以通过实例一个vue实例Bus作为媒介,要相互通信的兄弟组件之中,都引入Bus,然后通过分别调用Bus事件触发和监听来实现通信和参数传递。Bus.js可以是这样:

import Vue from 'vue'
export default new Vue()

在需要通信的组件都引入Bus.js:

<template>
 <button @click="toBus">子组件传给兄弟组件</button>
</template>

<script>
import Bus from '../common/js/bus.js'
export default{
 methods: {
     toBus () {
         Bus.$emit('on', '来自兄弟组件')
     }
   }
}
</script>

另一个组件也import Bus.js 在钩子函数中监听on事件

import Bus from '../common/js/bus.js'
export default {
    data() {
      return {
        message: ''
      }
    },
    mounted() {
       Bus.$on('on', (msg)=> {
         this.message=msg
       })
     }
   }

27 Vue与Angular以及React的区别?

Vue与AngularJS的区别

  • Angular采用TypeScript开发, 而Vue可以使用javascript也可以使用TypeScript
  • AngularJS依赖对数据做脏检查,所以Watcher越多越慢;Vue.js使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。
  • AngularJS社区完善, Vue的学习成本较小

Vue与React的区别

  • vue组件分为全局注册和局部注册,在react中都是通过import相应组件,然后模版中引用;
  • props是可以动态变化的,子组件也实时更新,在react中官方建议props要像纯函数那样,输入输出一致对应,而且不太建议通过props来更改视图;
  • 子组件一般要显示地调用props选项来声明它期待获得的数据。而在react中不必需,另两者都有props校验机制;
  • 每个Vue实例都实现了事件接口,方便父子组件通信,小型项目中不需要引入状态管理机制,而react必需自己实现;
  • 使用插槽分发内容,使得可以混合父组件的内容与子组件自己的模板;
  • 多了指令系统,让模版可以实现更丰富的功能,而React只能使用JSX语法;
  • Vue增加的语法糖computed和watch,而在React中需要自己写一套逻辑来实现;
  • react的思路是all in js,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component、jss等;而 vue是把html,css,js组合到一起,用各自的处理方式,vue有单文件组件,可以把html、css、js写到一个文件中,html提供了模板引擎来处理。
  • react做的事情很少,很多都交给社区去做,vue很多东西都是内置的,写起来确实方便一些, 比如 redux的combineReducer就对应vuex的modules, 比如reselect就对应vuex的getter和vue组件的computed, vuex的mutation是直接改变的原始数据,而redux的reducer是返回一个全新的state,所以redux结合immutable来优化性能,vue不需要。
  • react是整体的思路的就是函数式,所以推崇纯组件,数据不可变,单向数据流,当然需要双向的地方也可以做到,比如结合redux-form,组件的横向拆分一般是通过高阶组件。而vue是数据可变的,双向绑定,声明式的写法,vue组件的横向拆分很多情况下用mixin

28 vuex是什么?怎么使用?哪种功能场景使用它?

  • vuex 就是一个仓库,仓库里放了很多对象。其中 state 就是数据源存放地,对应于一般 vue 对象里面的 data
  • state 里面存放的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据发生改变,依赖这相数据的组件也会发生更新
  • 它通过 mapState 把全局的 state 和 getters 映射到当前组件的 computed 计算属性

vuex的使用借助官方提供的一张图来说明:

image

Vuex有5种属性: 分别是 state、getter、mutation、action、module;

state

Vuex 使用单一状态树,即每个应用将仅仅包含一个store 实例,但单一状态树和模块化并不冲突。存放的数据状态,不可以直接修改里面的数据

mutations

mutations定义的方法动态修改Vuex 的 store 中的状态或数据。

getters

类似vue的计算属性,主要用来过滤一些数据

action

  • actions可以理解为通过将mutations里面处里数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据。view 层通过 store.dispath 来分发 action。
  • vuex 一般用于中大型 web 单页应用中对应用的状态进行管理,对于一些组件间关系较为简单的小型应用,使用 vuex 的必要性不是很大,因为完全可以用组件 prop 属性或者事件来完成父子组件之间的通信,vuex 更多地用于解决跨组件通信以及作为数据中心集中式存储数据。
  • 使用Vuex解决非父子组件之间通信问题 vuex 是通过将 state 作为数据中心、各个组件共享 state 实现跨组件通信的,此时的数据完全独立于组件,因此将组件间共享的数据置于 State 中能有效解决多层级组件嵌套的跨组件通信问题
  • vuex 作为数据存储中心 vuex 的 State 在单页应用的开发中本身具有一个“数据库”的作用,可以将组件中用到的数据存储在 State 中,并在 Action 中封装数据读写的逻辑。这时候存在一个问题,一般什么样的数据会放在 State 中呢?目前主要有两种数据会使用 vuex 进行管理:1、组件之间全局共享的数据 2、通过后端异步请求的数据 比如做加入购物车、登录状态等都可以使用Vuex来管理数据状态

一般面试官问到这里vue基本知识就差不多了, 如果更深入的研究就是和你探讨关于vue的底层源码;或者是具体在项目中遇到的问题,下面列举几个项目中可能遇到的问题:

  • 开发时,改变数组或者对象的数据,但是页面没有更新如何解决?
  • vue弹窗后如何禁止滚动条滚动?
  • 如何在 vue 项目里正确地引用 jquery 和 jquery-ui的插件

28 watch与computed的区别

computed:

  • computed是计算属性,也就是计算值,它更多用于计算值的场景
  • computed具有缓存性,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算 computed适用于计算比较消耗性能的计算场景

watch:

  • 更多的是「观察」的作用,类似于某些数据的监听回调,用于观察props $emit或者本组件的值,当数据变化时来执行回调进行后续操作
  • 无缓存性,页面重新渲染时值不变化也会执行

小结:

  • 当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为computed
  • 如果你需要在某个数据变化时做一些事情,使用watch来观察这个数据变化

29、Vue是如何实现双向绑定的?

利用Object.defineProperty劫持对象的访问器,在属性值发生变化时我们可以获取变化,然后根据变化进行后续响应,在vue3.0中通过Proxy代理对象进行类似的操作。

// 这是将要被劫持的对象
const data={
  name: '',
};

function say(name) {
  if (name==='古天乐') {
    console.log('给大家推荐一款超好玩的游戏');
  } else if (name==='渣渣辉') {
    console.log('戏我演过很多,可游戏我只玩贪玩懒月');
  } else {
    console.log('来做我的兄弟');
  }
}

// 遍历对象,对其属性值进行劫持
Object.keys(data).forEach(function(key) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      console.log('get');
    },
    set: function(newVal) {
      // 当属性值发生变化时我们可以进行额外操作
      console.log(`大家好,我系${newVal}`);
      say(newVal);
    },
  });
});

data.name='渣渣辉';
//大家好,我系渣渣辉
//戏我演过很多,可游戏我只玩贪玩懒月

29 Vue2.x 响应式原理

Vue 采用数据劫持结合发布—订阅模式的方法,通过 Object.defineProperty() 来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

image

  • Observer 遍历数据对象,给所有属性加上 settergetter,监听数据的变化
  • compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

Watcher 订阅者是 ObserverCompile 之间通信的桥梁,主要做的事情

  • 在自身实例化时往属性订阅器 (dep) 里面添加自己
  • 待属性变动 dep.notice() 通知时,调用自身的 update() 方法,并触发 Compile 中绑定的回调

Vue3.x响应式数据原理

Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。

Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?

判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。

监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?

我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger

30 v-model双向绑定原理

v-model本质上是语法糖,v-model在内部为不同的输入元素使用不同的属性并抛出不同的事件

  • texttextarea 元素使用 value 属性和 input 事件
  • checkboxradio 使用 checked 属性和 change 事件
  • select 字段将 value 作为 prop 并将 change 作为事件

所以我们可以v-model进行如下改写:

<input v-model="sth" />
//  等同于
<input :value="sth" @input="sth=$event.target.value" />
  • 这个语法糖必须是固定的,也就是说属性必须为value,方法名必须为:input
  • 知道了v-model的原理,我们可以在自定义组件上实现v-model
//Parent
<template>
    {{num}}
    <Child v-model="num">
</template>
export default {
    data(){
        return {
            num: 0
        }
    }
}

//Child
<template>
    <div @click="add">Add</div>
</template>
export default {
    props: ['value'],
    methods:{
        add(){
            this.$emit('input', this.value + 1)
        }
    }
}

31 scoped样式穿透

scoped虽然避免了组件间样式污染,但是很多时候我们需要修改组件中的某个样式,但是又不想去除scoped属性

  1. 使用/deep/
//Parent
<template>
<div class="wrap">
    <Child />
</div>
</template>

<style lang="scss" scoped>
.wrap /deep/ .box{
    background: red;
}
</style>

//Child
<template>
    <div class="box"></div>
</template>
  1. 使用两个style标签
//Parent
<template>
<div class="wrap">
    <Child />
</div>
</template>

<style lang="scss" scoped>
//其他样式
</style>
<style lang="scss">
.wrap .box{
    background: red;
}
</style>

//Child
<template>
    <div class="box"></div>
</template>

32 ref的作用

  • 获取dom元素this.$refs.box
  • 获取子组件中的datathis.$refs.box.msg
  • 调用子组件中的方法this.$refs.box.open()

33 computed和watch区别

  1. 当页面中有某些数据依赖其他数据进行变动的时候,可以使用计算属性computed

Computed本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理

image

<template>{{fullName}}</template>
export default {
    data(){
        return {
            firstName: 'xie',
            lastName: 'yu fei',
        }
    },
    computed:{
        fullName: function(){
            return this.firstName + ' ' + this.lastName
        }
    }
}
  1. watch用于观察和监听页面上的vue实例,如果要在数据变化的同时进行异步操作或者是比较大的开销,那么watch为最佳选择

Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销

image

<template>{{fullName}}</template>
export default {
    data(){
        return {
            firstName: 'xie',
            lastName: 'xiao fei',
            fullName: 'xie xiao fei'
        }
    },
    watch:{
        firstName(val) {
            this.fullName=val + ' ' + this.lastName
        },
        lastName(val) {
            this.fullName=this.firstName + ' ' + val
        }
    }
}

34 vue-router守卫

导航守卫 router.beforeEach 全局前置守卫

  • to: Route: 即将要进入的目标(路由对象)
  • from: Route: 当前导航正要离开的路由
  • next: Function: 一定要调用该方法来 resolve 这个钩子。(一定要用这个函数才能去到下一个路由,如果不用就拦截)
  • 执行效果依赖 next 方法的调用参数。
  • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
  • next(false):取消进入路由,url地址重置为from路由地址(也就是将要离开的路由地址)
// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next)=> { 
  next();
});
router.beforeResolve((to, from, next)=> {
  next();
});
router.afterEach((to, from)=> {
  console.log('afterEach 全局后置钩子');
});

路由独享的守卫 你可以在路由配置上直接定义 beforeEnter 守卫

const router=new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next)=> {
        // ...
      }
    }
  ]
})

组件内的守卫你可以在路由组件内直接定义以下路由导航守卫

const Foo={
  template: `...`,
  beforeRouteEnter (to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
  },
  beforeRouteUpdate (to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave (to, from, next) {
    // 导航离开该组件的对应路由时调用,我们用它来禁止用户离开
    // 可以访问组件实例 `this`
    // 比如还未保存草稿,或者在用户离开前,
    将setInterval销毁,防止离开之后,定时器还在调用。
  }
}

35 vue修饰符

  • stop:阻止事件的冒泡
  • prevent:阻止事件的默认行为
  • once:只触发一次
  • self:只触发自己的事件行为时,才会执行

36 vue项目中的性能优化

  • 不要在模板里面写过多表达式
  • 循环调用子组件时添加key
  • 频繁切换的使用v-show,不频繁切换的使用v-if
  • 尽量少用float,可以用flex
  • 按需加载,可以用require或者import()按需加载需要的组件
  • 路由懒加载

37 vue.extend和vue.component

  • extend是构造一个组件的语法器。然后这个组件你可以作用到Vue.component这个全局注册方法里还可以在任意vue模板里使用组件。也可以作用到vue实例或者某个组件中的components属性中并在内部使用apple组件。
  • Vue.component你可以创建 ,也可以取组件。

38 Vue的SPA 如何优化加载速度

  • 减少入口文件体积
  • 静态资源本地缓存
  • 开启Gzip压缩
  • 使用SSR,nuxt.js

39 移动端如何设计一个比较友好的Header组件?

当时的思路是头部(Header)一般分为左、中、右三个部分,分为三个区域来设计,中间为主标题,每个页面的标题肯定不同,所以可以通过vue props的方式做成可配置对外进行暴露,左侧大部分页面可能都是回退按钮,但是样式和内容不尽相同,右侧一般都是具有功能性的操作按钮,所以左右两侧可以通过vue slot插槽的方式对外暴露以实现多样化,同时也可以提供default slot默认插槽来统一页面风格

40 Proxy与Object.defineProperty的优劣对比?

Proxy的优势如下:

  • Proxy可以直接监听对象而非属性
  • Proxy可以直接监听数组的变化
  • Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的
  • Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改
  • Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利

Object.defineProperty的优势如下:

兼容性好,支持IE9

41 你是如何理解Vue的响应式系统的?

image

响应式系统简述:

  • 任何一个 Vue Component 都有一个与之对应的 Watcher 实例。
  • Vue 的 data 上的属性会被添加 getter 和 setter 属性。
  • 当 Vue Component render 函数被执行的时候, data 上会被 触碰(touch), 即被读, getter 方法会被调用, 此时 Vue 会去记录此 Vue component 所依赖的所有 data。(这一过程被称为依赖收集)
  • data 被改动时(主要是用户操作), 即被写, setter 方法会被调用, 此时 Vue 会去通知所有依赖于此 data 的组件去调用他们的 render 函数进行更新。

42 既然Vue通过数据劫持可以精准探测数据变化,为什么还需要虚拟DOM进行diff检测差异?

现代前端框架有两种方式侦测变化,一种是pull一种是push

  • pull: 其代表为React,我们可以回忆一下React是如何侦测到变化的,我们通常会用setStateAPI显式更新,然后React会进行一层层的Virtual Dom Diff操作找出差异,然后Patch到DOM上,React从一开始就不知道到底是哪发生了变化,只是知道「有变化了」,然后再进行比较暴力的Diff操作查找「哪发生变化了」,另外一个代表就是Angular的脏检查操作。
  • push: Vue的响应式系统则是push的代表,当Vue程序初始化的时候就会对数据data进行依赖的收集,一但数据发生变化,响应式系统就会立刻得知,因此Vue是一开始就知道是「在哪发生变化了」,但是这又会产生一个问题,如果你熟悉Vue的响应式系统就知道,通常一个绑定一个数据就需要一个Watcher,一但我们的绑定细粒度过高就会产生大量的Watcher,这会带来内存以及依赖追踪的开销,而细粒度过低会无法精准侦测变化,因此Vue的设计是选择中等细粒度的方案,在组件级别进行push侦测的方式,也就是那套响应式系统,通常我们会第一时间侦测到发生变化的组件,然后在组件内部进行Virtual Dom Diff获取更加具体的差异,而Virtual Dom Diff则是pull操作,Vue是push+pull结合的方式进行变化侦测的

43 Vue为什么没有类似于React中shouldComponentUpdate的生命周期?

考点: Vue的变化侦测原理

前置知识: 依赖收集、虚拟DOM、响应式系统

根本原因是Vue与React的变化侦测方式有所不同

  • React是pull的方式侦测变化,当React知道发生变化后,会使用Virtual Dom Diff进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要用shouldComponentUpdate进行手动操作来减少diff,从而提高程序整体的性能.
  • Vue是pull+push的方式侦测变化的,在一开始就知道那个组件发生了变化,因此在push的阶段并不需要手动控制diff,而组件内部采用的diff方式实际上是可以引入类似于shouldComponentUpdate相关生命周期的,但是通常合理大小的组件不会有过量的diff,手动优化的价值有限,因此目前Vue并没有考虑引入shouldComponentUpdate这种手动优化的生命周期.

44 Vue中的key到底有什么用?

  • key是为Vue中的vnode标记的唯一id,通过这个key,我们的diff操作可以更准确、更快速
  • diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行比对,然后超出差异.

diff程可以概括为:oldCh和newCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一个已经遍历完了,就会结束比较,这四种比较方式就是首、尾、旧尾新头、旧头新尾.

准确: 如果不加key,那么vue会选择复用节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug. 快速: key的唯一性可以被Map数据结构充分利用,相比于遍历查找的时间复杂度O(n),Map的时间复杂度仅仅为O(1).

image

45 vue 项目性能优化

代码层面:

  • 合理使用 v-ifv-show
  • 区分 computedwatch 的使用
  • v-for 遍历为 item 添加 key
  • v-for 遍历避免同时使用 v-if
  • 通过 addEventListener添加的事件在组件销毁时要用 removeEventListener 手动移除这些事件的监听
  • 图片懒加载
  • 路由懒加载
  • 第三方插件按需引入
  • SSR服务端渲染,首屏加载速度快,SEO效果好

Webpack 层面优化:

  • 对图片进行压缩
  • 使用 CommonsChunkPlugin 插件提取公共代码
  • 提取组件的 CSS
  • 优化 SourceMap
  • 构建结果输出分析,利用 webpack-bundle-analyzer 可视化分析工具

46 nextTick

nextTick 可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM

nextTick主要使用了宏任务和微任务。根据执行环境分别尝试采用

  • Promise
  • MutationObserver
  • setImmediate
  • 如果以上都不行则采用setTimeout

定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列

47 说一下vue2.x中如何监测数组变化

使用了函数劫持的方式,重写了数组的方法,Vuedata中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。

48 你的接口请求一般放在哪个生命周期中

接口请求一般放在mounted中,但需要注意的是服务端渲染时不支持mounted,需要放到created

49 组件中的data为什么是一个函数

一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数

50 说一下v-model的原理

v-model本质就是一个语法糖,可以看成是value + input方法的语法糖。可以通过model属性的propevent属性来进行自定义。原生的v-model,会根据标签的不同生成不同的事件和属性

51 Vue事件绑定原理说一下

原生事件绑定是通过addEventListener绑定给真实元素的,组件事件绑定是通过Vue自定义的$on实现的

52 Vue模版编译原理知道吗,能简单说一下吗?

简单说,Vue的编译过程就是将template转化为render函数的过程。会经历以下阶段:

  • 生成AST
  • 优化
  • codegen
  • 首先解析模版,生成AST语法树(一种用JavaScript对象的形式来描述整个模板)。使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
  • Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用。
  • 编译的最后一步是将优化后的AST树转换为可执行的代码

53 Vue2.x和Vue3.x渲染器的diff算法分别说一下

简单来说,diff算法有以下过程

  • 同级比较,再比较子节点
  • 先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
  • 比较都有子节点的情况(核心diff)
  • 递归比较子节点
  • 正常Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以VueDiff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
  • Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比ReactDiff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅
  • 在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升

54 再说一下虚拟Dom以及key属性的作用

  • 由于在浏览器中操作DOM是很昂贵的。频繁的操作DOM,会产生一定的性能问题。这就是虚拟Dom的产生原因
  • Virtual DOM本质就是用一个原生的JS对象去描述一个DOM节点。是对真实DOM的一层抽象
  • VirtualDOM映射到真实DOM要经历VNodecreatediffpatch等阶段

key的作用是尽可能的复用 DOM 元素

  • 新旧 children 中的节点只有顺序是不同的时候,最佳的操作应该是通过移动元素的位置来达到更新的目的
  • 需要在新旧 children 的节点中保存映射关系,以便能够在旧 children 的节点中找到可复用的节点。key也就是children中节点的唯一标识

55 Vue中组件生命周期调用顺序说一下

  • 组件的调用顺序都是先父后子,渲染完成的顺序是先子后父。
  • 组件的销毁操作是先父后子,销毁完成的顺序是先子后父。

加载渲染过程

父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted

子组件更新过程

父beforeUpdate->子beforeUpdate->子updated->父updated

父组件更新过程

父 beforeUpdate -> 父 updated

销毁过程

父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

56 SSR了解吗

SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端

SSR有着更好的SEO、并且首屏加载速度更快等优点。不过它也有一些缺点,比如我们的开发条件会受到限制,服务器端渲染只支持beforeCreatecreated两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境。还有就是服务器会有更大的负载需求

57 你都做过哪些Vue的性能优化

编码阶段

  • 尽量减少data中的数据,data中的数据都会增加gettersetter,会收集对应的watcher
  • v-ifv-for不能连用
  • 如果需要使用v-for给每项元素绑定事件时使用事件代理
  • SPA 页面采用keep-alive缓存组件
  • 在更多的情况下,使用v-if替代v-show
  • key保证唯一
  • 使用路由懒加载、异步组件
  • 防抖、节流
  • 第三方模块按需导入
  • 长列表滚动到可视区域动态加载
  • 图片懒加载

SEO优化

  • 预渲染
  • 服务端渲染SSR

打包优化

  • 压缩代码
  • Tree Shaking/Scope Hoisting
  • 使用cdn加载第三方模块
  • 多线程打包happypack
  • splitChunks抽离公共文件
  • sourceMap优化

用户体验

  • 骨架屏
  • PWA

还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。

58 Vue.js特点

  • 简洁:页面由HTML模板+Json数据+Vue实例组成
  • 数据驱动:自动计算属性和追踪依赖的模板表达式
  • 组件化:用可复用、解耦的组件来构造页面
  • 轻量:代码量小,不依赖其他库
  • 快速:精确有效批量DOM更新
  • 模板友好:可通过npm,bower等多种方式安装,很容易融入

59 请说出vue.cli项目中src目录每个文件夹和文件的用法

  • assets文件夹是放静态资源;
  • components是放组件;
  • router是定义路由相关的配置;
  • view视图;
  • app.vue是一个应用主组件;
  • main.js是入口文件

60 vue路由传参数

  • 使用query方法传入的参数使用this.$route.query接受
  • 使用params方式传入的参数使用this.$route.params接受

61 vuex 是什么?有哪几种属性?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。

有 5 种,分别是 stategettermutationactionmodule

  • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
  • 有 5 种,分别是 stategettermutationactionmodule
  • vuexstore 是什么?
  • vuex 就是一个仓库,仓库里放了很多对象。其中 state 就是数据源存放地,对应于一般 vue 对象里面的 datastate 里面存放的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据发生改变,依赖这相数据的组件也会发生更新它通过 mapState 把全局的 stategetters 映射到当前组件的 computed 计算属性

vuex 的 getter 是什么?

  • getter 可以对 state 进行计算操作,它就是 store 的计算属性虽然在组件内也可以做计算属性,但是 getters 可以在多给件之间复用如果一个状态只在一个组件内使用,是可以不用 getters

vuex 的 mutation 是什么?

  • 更改Vuexstore中的状态的唯一方法是提交mutation

vuex 的 action 是什么?

  • action 类似于 muation, 不同在于:action 提交的是 mutation,而不是直接变更状态action 可以包含任意异步操作
  • vueajax 请求代码应该写在组件的 methods 中还是 vuexaction
  • vuexmodule 是什么?

面对复杂的应用程序,当管理的状态比较多时;我们需要将vuexstore对象分割成模块(modules)。

如果请求来的数据不是要被其他组件公用,仅仅在请求的组件内使用,就不需要放入 vuexstate 里如果被其他地方复用,请将请求放入 action 里,方便复用,并包装成 promise 返回

62 如何让CSS只在当前组件中起作用?

将当前组件的<style>修改为<style scoped>

63 delete和Vue.delete删除数组的区别?

  • delete只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。
  • Vue.delete直接删除了数组 改变了数组的键值。
var a=[1,2,3,4]
var b=[1,2,3,4]
delete a[0]
console.log(a)  //[empty,2,3,4]
this.$delete(b,0)
console.log(b)  //[2,3,4]

64 v-on可以监听多个方法吗?

可以

<input type="text" :value="name" @input="onInput" @focus="onFocus" @blur="onBlur" />

v-on 常用修饰符

  • .stop 该修饰符将阻止事件向上冒泡。同理于调用 event.stopPropagation() 方法
  • .prevent 该修饰符会阻止当前事件的默认行为。同理于调用 event.preventDefault() 方法
  • .self 该指令只当事件是从事件绑定的元素本身触发时才触发回调
  • .once 该修饰符表示绑定的事件只会被触发一次

65 Vue子组件调用父组件的方法

  • 第一种方法是直接在子组件中通过this.$parent.event来调用父组件的方法
  • 第二种方法是在子组件里用$emit向父组件触发一个事件,父组件监听这个事件就行了。

66 vue如何兼容ie的问题

babel-polyfill插件

67 Vue 改变数组触发视图更新

以下方法调用会改变原始数组:push(), pop(), shift(), unshift(), splice(), sort(), reverse(),Vue.set( target, key, value )

  • 调用方法:Vue.set( target, key, value )
    • target:要更改的数据源(可以是对象或者数组)
    • key:要更改的具体数据
    • value :重新赋的值

68 DOM 渲染在哪个周期中就已经完成?

mounted

注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted

mounted: function () {
  this.$nextTick(function () {
    // Code that will run only after the
    // entire view has been rendered
  })
}

69 简述每个周期具体适合哪些场景

  • beforecreate : 可以在这加个loading事件,在加载实例时触发
  • created : 初始化完成时的事件写在这里,如在这结束loading事件,异步请求也适宜在这里调用
  • mounted : 挂载元素,获取到DOM节点 updated : 如果对数据统一处理,在这里写上相应函数
  • beforeDestroy : 可以做一个确认停止事件的确认框

第一次加载会触发哪几个钩子

会触发beforeCreate , created ,beforeMount ,mounted

70 动态绑定class

active classnameisActive 变量

<div :class="{ active: isActive }"></div>