整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          前端必須懂的設計模式-代理模式

          前端必須懂的設計模式-代理模式

          • 由于一個對象不想或者不能直接引用另外一個對象,所以需要通過通過一個稱之為“代理”的第三者來實現間接引用
          • 代理模式就是為目標對象創造一個代理對象,在客戶端和目標對象之間起到中介的作用
          • 這樣就可以在代理對象里增加一些邏輯判斷、調用前或調用后執行一些操作,從而實現了擴展目標的功能
          • 并且可以通過代理對象去掉客戶不能看到的內容和服務或者添加客戶需要的額外服務

          通過引入一個新的對象(如小圖片和遠程代理對象)來實現對真實對象的操作或者將新的對 象作為真實對象的一個替身,這種實現機制即為代理模式,通過引入代理對象來間接訪問一 個對象,這就是代理模式的模式動機。

          定義

          代理模式(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>

          主站蜘蛛池模板: 国产精品视频一区| 亚洲线精品一区二区三区| 国产精品一区二区四区| 亚洲综合激情五月色一区| 亚洲综合色自拍一区| 丰满少妇内射一区| 亚洲一区二区三区在线视频| 日韩AV在线不卡一区二区三区| 国产精品一区二区电影| 精品一区二区三区四区电影| 日韩一区二区三区视频| 一区二区高清在线| 少妇无码AV无码一区| 奇米精品一区二区三区在| 亚洲中文字幕无码一区二区三区 | 在线日韩麻豆一区| 精品一区二区三区在线播放| 爆乳熟妇一区二区三区| 手机看片福利一区二区三区| 久久精品国产亚洲一区二区| 精品一区二区三区自拍图片区| 亚洲一区二区三区免费在线观看 | 久久人做人爽一区二区三区| 国产福利微拍精品一区二区| 无码人妻精品一区二区三区不卡| 日韩AV无码一区二区三区不卡| 色欲AV无码一区二区三区| 成人h动漫精品一区二区无码| 亚洲日本中文字幕一区二区三区| 91精品国产一区| 一区二区三区中文字幕| 国产午夜精品一区二区三区嫩草| 国产美女精品一区二区三区| 国产在线视频一区| 亚洲国产成人一区二区三区| 欧美日韩精品一区二区在线观看| 无码人妻精品一区二区三区9厂| 亚洲国产精品一区二区九九 | 呦系列视频一区二区三区| 韩国资源视频一区二区三区| 国产精品一区二区香蕉|