整合營銷服務商

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

          免費咨詢熱線:

          手把手教你D3.js 實現數據可視化極速上手到Vue應用



          D3近年來一直是JavaScript最重要的數據可視化庫之一,在創(chuàng)建者Mike Bostock的維護下,前景依然無量,至少現在沒有能打的:

          • D3與眾多其他庫的區(qū)別在于無限定制的能力(直接操作SVG)。
          • 它的底層API提供對原生SVG元素的直接控制,但它也帶來了高學習曲線的成本。
          • 我們將把D3和Vue結合在一起 - 使用Vue的動態(tài)數據綁定,清晰的語法和模塊化結構,可以充分發(fā)揮D3的最佳性能。 根據廣泛定義,D3可拆分為以下幾種分庫:



          1. 絕大部分的D3課程或書籍,都會著重講解在其DOM操作功能上,但這明顯與近幾年來的web框架理念相違背。
          2. 用于數據可視化的D3,其核心在于使用繪圖指令裝飾數據,從源數據創(chuàng)建新的可繪制數據,生成SVG路徑以及從數據和方法在DOM中創(chuàng)建數據可視化元素(如軸)的功能。
          3. 有許多用于管理DOM的工具,所有這些工具都可以在D3中集成數據可視化功能。這也是D3能與Vue無縫結合的原因之一。

          于此,我們不需要從D3 DOM操作功能開始學起,直接通過實例來入門D3。

          1. D3.js 漸進入門

          以下實例的模版均為以下形式:

          <html>
              <head>
                  <link rel="stylesheet" href="index.css">
                  <title>Learn D3.js</title>
              </head>
              <body>
                  <!--或其它標簽-->
                  <h1>First heading</h1>
                  
                  <script src="https://d3js.org/d3.v4.min.js"></script>
                  <script src="index.js"></script>
              </body>
          </html>
          復制代碼

          1. 選擇和操作



          你需要學習的第一件事是如何使用D3.js選擇和操作DOM元素。該庫在操作DOM方面實際上非常強大,因此理論上可以將其用作jQuery的替代品。以下代碼請逐行添加運行。

          // index.js
          d3.select();
          d3.selectAll();
          
          d3.select('h1').style('color', 'red')
          .attr('class', 'heading')
          .text('Updated h1 tag');
          
          d3.select('body').append('p').text('First Paragraph');
          d3.select('body').append('p').text('Second Paragraph');
          d3.select('body').append('p').text('Third Paragraph');
          
          d3.selectAll('p').style('')
          復制代碼

          2.數據加載和綁定


          當你要創(chuàng)建可視化時,了解如何加載數據以及將其綁定到DOM非常重要。所以在這個實例中,你將學到這兩點。


          let dataset = [1, 2, 3, 4, 5];
          
          d3.select('body')
              .selectAll('p')
              .data(dataset)
              .enter()
              .append('p') // appends paragraph for each data element
              .text('D3 is awesome!!');
              //.text(function(d) { return d; });
          復制代碼

          3.創(chuàng)建一個簡單的柱狀圖



          首先需要添加一個svg標簽

          <h1>Bar Chart using D3.js</h1>
          
          <svg class="bar-chart"></svg>
          復制代碼

          然后在index.js中添加(已添加關鍵注釋):

          // 數據集
          let dataset = [80, 100, 56, 120, 180, 30, 40, 120, 160];
          // 定義svg圖形寬高,以及柱狀圖間距
          let svgWidth = 500, svgHeight = 300, barPadding = 5;
          // 通過圖形計算每個柱狀寬度
          let barWidth = (svgWidth / dataset.length);
          
          // 繪制圖形
          let svg = d3.select('svg')
              .attr("width", svgWidth)
              .attr("height", svgHeight);
          
          // rect,長方形
          // 文檔:http://www.w3school.com.cn/svg/svg_rect.asp
          
          let barChart = svg.selectAll("rect")
              .data(dataset) //綁定數組
              .enter() // 指定選擇集的enter部分
              .append("rect") // 添加足夠數量的矩形
              .attr("y", d => svgHeight - d ) // d為數據集每一項的值, 取y坐標
              .attr("height", d => d) // 設定高度
              .attr("width", barWidth - barPadding) // 設定寬度
              .attr("transform", (d, i) =>  {
                  let translate = [barWidth * i, 0]; 
                  return "translate("+ translate +")";
              }); // 實際是計算每一項值的x坐標
          復制代碼

          4. 在圖形上方顯示數值


          這時就需要在上述代碼中創(chuàng)建svg的 text文本


          let text = svg.selectAll("text")
              .data(dataset)
              .enter()
              .append("text")
              .text(d => d)
              .attr("y", (d, i) => svgHeight - d - 2)
              .attr("x", (d, i) =>  barWidth * i)
              .attr("fill", "#A64C38");
          復制代碼

          過程比較簡單,就是返回文本,計算x/y坐標,并填充顏色。

          5. scales: 比例尺函數

          D3中有個重要的概念就是比例尺。比例尺就是把一組輸入域映射到輸出域的函數。映射就是兩個數據集之間元素相互對應的關系。比如輸入是1,輸出是100,輸入是5,輸出是10000,那么這其中的映射關系就是你所定義的比例尺。

          D3中有各種比例尺函數,有連續(xù)性的,有非連續(xù)性的,在本例子中,你將學到d3.scaleLinear() ,線性比例尺

          5.1 d3.scaleLinear(),線性比例尺

          使用d3.scaleLinear()創(chuàng)造一個線性比例尺,其中:

          • domain()是輸入域
          • range()是輸出域
          • 相當于將domain中的數據集映射到range的數據集中。
          let scale = d3.scaleLinear().domain([1,5]).range([0,100])
          復制代碼

          映射關系:



          值得注意的是,上述代碼只是定義了一個映射規(guī)則,映射的輸入值并不局限于domain()中的輸入域。

          scale(1) // 輸出:0
          scale(4) // 輸出:75
          scale(5) // 輸出:100
          scale(-1) // 輸出:-50
          scale(10) // 輸出:225
          復制代碼

          于是我們來改造3~4的例子:

          let dataset = [1,2,3,4,5];
          
          let svgWidth = 500, svgHeight = 300, barPadding = 5;
          let barWidth = (svgWidth / dataset.length);
          
          
          let svg = d3.select('svg')
              .attr("width", svgWidth)
              .attr("height", svgHeight);
              
          let yScale = d3.scaleLinear()
              .domain([0, d3.max(dataset)])
              .range([0, svgHeight]);
                  
          let barChart = svg.selectAll("rect")
              .data(dataset)
              .enter()
              .append("rect")
              .attr("y", d => svgHeight - yScale(d))
              .attr("height", d => yScale(d))
              .attr("width", barWidth - barPadding)
              .attr("transform", (d, i) => {
                  let translate = [barWidth * i, 0]; 
                  return "translate("+ translate +")";
              });
          復制代碼

          然后就會得到以下圖形:



          6. Axes:軸



          軸是任何圖表的組成部分,本例子中將會用到上面講到的比例尺函數。

          let data= [80, 100, 56, 120, 180, 30, 40, 120, 160];
          
          let svgWidth = 500, svgHeight = 300;
          
          let svg = d3.select('svg')
              .attr("width", svgWidth)
              .attr("height", svgHeight);
          
          // 首先是拿最大值構建x軸坐標
          let xScale = d3.scaleLinear()
              .domain([0, d3.max(data)])
              .range([0, svgWidth]);
                   
          // 接下來是反轉值,用作y軸坐標。
          let yScale = d3.scaleLinear()
              .domain([0, d3.max(data)])
              .range([svgHeight, 0]);
          
          // 橫軸的API使用
          let x_axis = d3.axisBottom()
              .scale(xScale);
              
          // 縱軸的API使用
          let y_axis = d3.axisLeft()
              .scale(yScale);
              
          // 在svg中提供了如g元素這樣的將多個元素組織在一起的元素。
          // 由g元素編組在一起的可以設置相同的顏色,可以進行坐標變換等,類似于Vue中的 <template>
          
          svg.append("g")
              .attr("transform", "translate(50, 10)")
              .call(y_axis);
                   
          let xAxisTranslate = svgHeight - 20;
                   
          svg.append("g")
              .attr("transform", "translate(50, " + xAxisTranslate  +")")
              .call(x_axis);
          復制代碼

          7. 創(chuàng)建簡易的SVG元素


          在這里面,你會創(chuàng)建<rect>,<circle>和<line>元素


          let svgWidth = 600, svgHeight = 500;
          let svg = d3.select("svg")
              .attr("width", svgWidth)
              .attr("height", svgHeight)
              .attr("class", "svg-container")
              
          let line = svg.append("line")
              .attr("x1", 100)
              .attr("x2", 500)
              .attr("y1", 50)
              .attr("y2", 50)
              .attr("stroke", "red");
              
          let rect = svg.append("rect")
              .attr("x", 100)
              .attr("y", 100)
              .attr("width", 200)
              .attr("height", 100)
              .attr("fill", "#9B95FF");
              
          let circle = svg.append("circle")
              .attr("cx", 200)
              .attr("cy", 300)
              .attr("r", 80)
              .attr("fill", "#7CE8D5");
          復制代碼

          8. 創(chuàng)建餅圖



          let data = [
              {"platform": "Android", "percentage": 40.11}, 
              {"platform": "Windows", "percentage": 36.69},
              {"platform": "iOS", "percentage": 13.06}
          ];
          
          let svgWidth = 500, svgHeight = 300, radius =  Math.min(svgWidth, svgHeight) / 2;
          let svg = d3.select('svg')
              .attr("width", svgWidth)
              .attr("height", svgHeight);
          
          //Create group element to hold pie chart    
          let g = svg.append("g")
              .attr("transform", "translate(" + radius + "," + radius + ")") ;
          
          // d3.scaleOrdinal() 序數比例尺
          // schemeCategory10, 顏色比例尺
          // D3提供了一些顏色比例尺,10就是10種顏色,20就是20種:
          let color = d3.scaleOrdinal(d3.schemeCategory10);
          
          let pie = d3.pie().value(d => d.percentage);
          
          let path = d3.arc()
              .outerRadius(radius)
              .innerRadius(0);
           
          let arc = g.selectAll("arc")
              .data(pie(data))
              .enter()
              .append("g");
          
          arc.append("path")
              .attr("d", path)
              .attr("fill", d => color(d.data.percentage));
                  
          let label = d3.arc()
              .outerRadius(radius)
              .innerRadius(0);
                      
          arc.append("text")
              .attr("transform",  d => `translate(${label.centroid(d)})`)
              .attr("text-anchor", "middle")
              .text(d => `${d.data.platform}:${d.data.percentage}%`);
          復制代碼

          9. 創(chuàng)建折線圖



          最后,你將學習如何創(chuàng)建折線圖以顯示近四個月的比特幣價格。要獲取數據,你將使用外部API。這個項目還將你在整個課程中學到的很多概念結合在一起,所以這是一個很好的可視化課程結束。

          // 外部API,注意日期記得補零
          const api = 'https://api.coindesk.com/v1/bpi/historical/close.json?start=2019-03-31&end=2019-07-01';
          
          /**
           * dom內容加載完畢時,從API中加載數據
           */
          document.addEventListener("DOMContentLoaded", function(event) {
          fetch(api)
              .then(response => response.json())
              .then(data => {
                  let parsedData = parseData(data);
                  drawChart(parsedData);
              })
              .catch(err =>  console.log(err))
          });
          
          /**
           * 將數據解析為鍵值對
           */
          parseData = data =>{
              let arr = [];
              for (let i in data.bpi) {
                  arr.push({
                      date: new Date(i), //date
                      value: +data.bpi[i] //convert string to number
                  });
              }
              return arr;
          }
          
          /**
           * 創(chuàng)建圖表
           */
          drawChart  = data => {
          let svgWidth = 600, svgHeight = 400;
          let margin = { top: 20, right: 20, bottom: 30, left: 50 };
          let width = svgWidth - margin.left - margin.right;
          let height = svgHeight - margin.top - margin.bottom;
          
          let svg = d3.select('svg')
              .attr("width", svgWidth)
              .attr("height", svgHeight);
              
          let g = svg.append("g")
              .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
          
          let x = d3.scaleTime()
              .rangeRound([0, width]);
          
          let y = d3.scaleLinear()
              .rangeRound([height, 0]);
          
          let line = d3.line()
              .x(d=> x(d.date))
              .y(d=> y(d.value))
              x.domain(d3.extent(data, function(d) { return d.date }));
              y.domain(d3.extent(data, function(d) { return d.value }));
          
          g.append("g")
              .attr("transform", "translate(0," + height + ")")
              .call(d3.axisBottom(x))
              .select(".domain")
              .remove();
          
          g.append("g")
              .call(d3.axisLeft(y))
              .append("text")
              .attr("fill", "#000")
              .attr("transform", "rotate(-90)")
              .attr("y", 6)
              .attr("dy", "0.71em")
              .attr("text-anchor", "end")
              .text("Price ($)");
          
          g.append("path")
              .datum(data)
              .attr("fill", "none")
              .attr("stroke", "steelblue")
              .attr("stroke-linejoin", "round")
              .attr("stroke-linecap", "round")
              .attr("stroke-width", 1.5)
              .attr("d", line);
          }
          
          復制代碼

          以上原實例均來自:Learn D3 for free。

          源碼地址:https://scrimba.com/g/gd3js

          scrimba是一個非常神奇的網站。它是使用交互式編碼截屏工具構建的。


          所有的操作都是:


          暫停截屏視頻 → 編輯代碼 → 運行它! → 查看更改

          非常值得安利一波。接下來進入第二部分:Vue中使用D3.js的正確姿勢

          2. Vue中使用D3.js的正確姿勢

          我們將使用D3和Vue構建一個基本的柱狀圖組件。網上有一堆例子,但我們將專注于寫Vue,而不是濫用D3。

          1. 安裝依賴

          首先,我們需要為項目安裝依賴項。我們可以簡單地安裝和使用D3整庫:

          npm i d3
          復制代碼

          但我在前面講到,實際上D3是幾個分庫的集合,考慮到項目的優(yōu)化,我們只安裝所需的模塊。



          使用Vue Cli 初始化項目即可。

          2. 創(chuàng)建柱狀圖



          3. 柱狀圖模塊導入



          4. 創(chuàng)建svg元素



          因Vue數據響應的特性,我們不需要用到D3操作DOM的那套鏈式創(chuàng)建。

          5. 數據與窗口大小響應


          在mounted鉤子中,我們將為窗口調整大小事件添加一個監(jiān)聽器,它將觸發(fā)繪制動畫,并將<svg>大小設置為新窗口的比例。我們不會立即渲染,而是等待300毫秒,以確保完全調整窗口大小。


          以下是完整的BarChart.vue,請配合注釋食用:

          <template>
            <div id="container" class="svg-container" align="center">
              <h1>{{ title }}</h1>
              <svg v-if="redrawToggle === true" :width="svgWidth" :height="svgHeight">
                <g>
                  <rect
                    v-for="item in data"
                    class="bar-positive"
                    :key="item[xKey]"
                    :x="xScale(item[xKey])"
                    :y="yScale(0)"
                    :width="xScale.bandwidth()"
                    :height="0"
                  ></rect>
                </g>
              </svg>
            </div>
          </template>
          
          <script>
          import { scaleLinear, scaleBand } from "d3-scale";
          import { max, min } from "d3-array";
          import { selectAll } from "d3-selection";
          import { transition } from "d3-transition";
          
          export default {
            name: "BarChart",
            props: {
              title: String,
              xKey: String,
              yKey: String,
              data: Array
            },
            mounted() {
              this.svgWidth = document.getElementById("container").offsetWidth * 0.75;
              this.AddResizeListener();
              this.AnimateLoad();
            },
            data: () => ({
              svgWidth: 0,
              redrawToggle: true
            }),
            methods: {
              // 繪制柱形
              AnimateLoad() {
                selectAll("rect")
                  .data(this.data)
                  .transition()
                  .delay((d, i) => {
                    return i * 150;
                  })
                  .duration(1000)
                  .attr("y", d => {
                    return this.yScale(d[this.yKey]);
                  })
                  .attr("height", d => {
                    return this.svgHeight - this.yScale(d[this.yKey]);
                  });
              },
              // 調整窗口大小后300毫秒重新繪制圖表
              // 即響應式繪制
              AddResizeListener() {
                window.addEventListener("resize", () => {
                  this.$data.redrawToggle = false;
                  setTimeout(() => {
                    this.$data.redrawToggle = true;
                    this.$data.svgWidth =
                      document.getElementById("container").offsetWidth * 0.75;
                    this.AnimateLoad();
                  }, 300);
                });
              }
            },
            computed: {
              dataMax() {
                return max(this.data, d => {
                  return d[this.yKey];
                });
              },
              dataMin() {
                return min(this.data, d => {
                  return d[this.yKey];
                });
              },
              xScale() {
                return scaleBand()
                  .rangeRound([0, this.svgWidth])
                  .padding(0.1)
                  .domain(
                    this.data.map(d => {
                      return d[this.xKey];
                    })
                  );
              },
              // 通過線性比例尺自動生成
              yScale() {
                return scaleLinear()
                  .rangeRound([this.svgHeight, 0])
                  .domain([this.dataMin > 0 ? 0 : this.dataMin, this.dataMax]);
              },
              svgHeight() {
                return this.svgWidth / 1.61803398875; // 黃金比例
              }
            }
          };
          </script>
          
          <style scoped>
          .bar-positive {
            fill: steelblue;
            transition: r 0.2s ease-in-out;
          }
          
          .bar-positive:hover {
            fill: brown;
          }
          
          .svg-container {
            display: inline-block;
            position: relative;
            width: 100%;
            padding-bottom: 1%;
            vertical-align: top;
            overflow: hidden;
          }
          </style>
          復制代碼

          我們將從父組件App.vue獲取數據:

          <template>
            <div id="app">
              <BarChart title="Bar Chart" xKey="name" yKey="amount" :data="barChartData"/>
            </div>
          </template>
          
          <script>
          import BarChart from "./components/BarChart.vue";
          
          export default {
            name: "App",
            components: {
              BarChart
            },
            data: () => ({
              barChartData: [
                {
                  name: "張三",
                  amount: 25
                },
                {
                  name: "李四",
                  amount: 40
                },
                {
                  name: "老王",
                  amount: 15
                },
                {
                  name: "老賴",
                  amount: 9
                }
              ]
            })
          };
          </script>
          
          <style>
          #app {
            font-family: "Open Sans", Helvetica, Arial, sans-serif;
            -webkit-font-smoothing: antialiased;
            -moz-osx-font-smoothing: grayscale;
            text-align: center;
            color: #282f36;
            margin-top: 30px;
          }
          </style>
          復制代碼

          這時候yarn run serve后將會看到:



          好像還缺點顯示數值,考慮到該圖高度是根據比例尺生成,我們調整下y坐標:

          yScale() {
            return scaleLinear()
              .rangeRound([this.svgHeight, 0])
              .domain([this.dataMin > 0 ? 0 : this.dataMin + 2, this.dataMax + 2]);
          },
          復制代碼

          在AnimateLoad()末尾添加:

          selectAll("text")
            .data(this.data)
            .enter()
          復制代碼

          最后在<g>元素中添加:

          <text
            v-for="item in data"
            :key="item[xKey].amount"
            :x="xScale(item[xKey]) + 30"
            :y="yScale(item[yKey]) - 2"
            fill="red"
          >{{ item[xKey]}} {{ item[yKey]}}
          </text>
          復制代碼



          3. 參考文章



          • The Hitchhiker’s Guide to d3.js
          • D3 is not a Data Visualization Library
          • D3中常用的比例尺
          • D3 vs G2 vs Echarts
          • Dynamic Data Visualizations With Vue.js and D3

          4. 總結

          該庫幾乎憑 Mike Bostock 一人之力完成,且在學術界、專業(yè)團隊中享有極大聲譽。



          • D3更接近底層,與 g2、echarts 不同,d3 能直接操作 svg,所以擁有極大的自由度,幾乎可以實現任何 2d 的設計需求。
          • 正如其名 Data Driven Documents,其本質是將數據與 DOM 綁定,并將數據映射至 DOM 屬性上。
          • D3 長于可視化,而不止于可視化,還提供了數據處理、數據分析、DOM 操作等諸多功能。
          • 如果有想深耕數據可視化方面的前端,D3不得不學。



          掌握 D3 后,限制作品水平的只會是想象力而不再是技術。

          源碼地址:https://github.com/roger-hiro/d3-bar-chart-vuejs

          ?? 看完三件事

          如果你覺得這篇內容對你挺有啟發(fā),我想邀請你幫我三個小忙:

          1. 點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-)
          2. 關注公眾號「前端勸退師」,不定期分享原創(chuàng)知識。
          3. 原地址:https://juejin.im/post/5d1e074af265da1bca51f8ec

          animate.css是一堆很酷,有趣且跨瀏覽器的動畫,供你在項目中使用。非常適合強調,主頁,滑塊和一般的加水效果。



          animate.css v4正在進行許多改進和重大更改,包括CSS自定義屬性支持(又稱CSS變量)和類前綴,以確保安全使用。感興趣的小伙伴可以上github關注進展以及提供反饋!

          Github

          animate.css的受歡迎程度毋庸置疑,在Github上star數高達接近63k,這是一個非常可觀的數據,我相信其實大多數人或多或少都用過它

          https://daneden.github.io/animate.css/



          安裝使用

          • 使用npm安裝
          $ npm install animate.css --save
          

          或者 yarn:

          $ yarn add animate.css
          

          要在你網站中使用animate.css,只需將樣式表放入文檔的<head>中,然后將動畫類(animated)與任何動畫名稱一起添加到元素中,那么一個簡單的動畫效果就實現了,一下就是一個最簡單的例子:

          <head>
           <link rel="stylesheet" href="animate.min.css">
          </head>
          

          <h1 class="animated infinite bounce delay-2s">Example</h1>
          

          以下是你可以使用的所用動畫效果class



          可以更改動畫的持續(xù)時間,添加延遲或更改動畫播放的次數:

          .yourElement {
           animation-duration: 3s;
           animation-delay: 2s;
           animation-iteration-count: infinite;
          }
          


          • JavaScript的用法:

          將animate.css與Javascript結合使用時,可以使用animate.css進行大量其他工作。一個簡單的例子:

          const element = document.querySelector('.my-element')
          element.classList.add('animated', 'bounceOutLeft')
          

          還可以檢測動畫何時結束:

          const element = document.querySelector('.my-element')
          element.classList.add('animated', 'bounceOutLeft')
          element.addEventListener('animationend', function() { doSomething() })
          

          可以使用以下簡單功能來添加和刪除動畫:

          function animateCSS(element, animationName, callback) {
           const node = document.querySelector(element)
           node.classList.add('animated', animationName)
           function handleAnimationEnd() {
           node.classList.remove('animated', animationName)
           node.removeEventListener('animationend', handleAnimationEnd)
           if (typeof callback === 'function') callback()
           }
           node.addEventListener('animationend', handleAnimationEnd)
          }
          

          并像這樣使用它:

          animateCSS('.my-element', 'bounce')
          
          // or
          animateCSS('.my-element', 'bounce', function() {
           // Do something after animation
          })
          

          注意,這些示例使用的是ES6的const聲明,不再支持IE10和某些古老的瀏覽器。



          • 設定延遲和速度:

          可以直接在元素的class屬性上添加延遲,如下所示:

          <div class="animated bounce delay-2s">Example</div>
          


          • 快慢class

          通過添加這些類,可以控制動畫的速度,如下所示:

          <div class="animated bounce faster">Example</div>
          


          • 自定義構建

          Animate.css由gulp.js提供支持,這意味著你可以輕松創(chuàng)建自定義版本。

          總結

          有些時候你看到別人的網站,感覺速度也不是很快,但是很自然,那么很有可能是使用了動畫,使用動畫不會加快網站的訪問速度,但是可以讓網頁瀏覽器來更加的平滑、更加的自然,使用起來會感覺很舒適,不會給人卡頓的感覺!

          文首發(fā)自「慕課網」,想了解更多IT干貨內容,程序員圈內熱聞,歡迎關注!

          前端框架發(fā)展到今天,已經出現了很多被廣泛認同的理念,也可以叫作它們的特征。在所有的特征中,最具代表性的特征之一即是數據驅動。

          用戶交互的對象——界面

          不論 web 應用的內部邏輯如何組織,最終與用戶產生交互的仍然是界面(User Interface,簡稱 UI)。對 web 應用來說,界面則主要由 DOM 元素來呈現。因此,歸根到底,為了讓用戶通過 web 應用完成操作,DOM 元素需要根據實際需要來不斷變化。

          例如,用戶通過我們的頁面來查詢當天蘋果的價格,頁面上有一個“查詢”按鈕,還有一個顯示蘋果價格的區(qū)域,此外,在查詢過程中,還有一個加載中的提示。

          <button>查詢</button>
          <div class="price">今日蘋果價格為 10.00/千克</div>
          <div class="loading">加載中</div>
          

          在查詢并顯示結果的過程中,DOM 實際上會經歷這樣幾個狀態(tài):

          1. 鼠標移動到按鈕上時,按鈕的樣式改變(通過 CSS:hover或者通過 JS)。
          2. 按鈕按下時,按鈕的樣式改變(通過 CSS:active或者通過 JS)。
          3. 清空價格區(qū)域的顯示內容。
          4. 將加載中的提示顯示出來。
          5. 待查詢到數據后,將數據拼裝成文字“今日蘋果價格為 10.00/千克”,放入價格區(qū)域中。
          6. 將加載中的提示隱藏。

          命令式編程實現界面變化

          如果將 1 和 2 使用 CSS 來處理,那么早期我們的 JavaScript 代碼大概是這樣:

          // 3. 清空價格區(qū)域的顯示內容
          price.innerHTML = '';
          // 4. 將加載中的提示顯示出來
          loading.style.display = 'block';
          // 查詢價格
          queryPrice(function(data){
              // 5. 待查詢到數據后,將數據拼裝成文字“今日蘋果價格為 10.00/千克”,放入價格區(qū)域中
              price.innerHTML = '今日蘋果價格為 ' + data.price + '/千克';
              // 6. 將加載中的提示隱藏
              loading.style.display = 'none';
          });
          

          即使是引入 jQuery 之類的庫,整個過程仍然不會有實質性的變化,主要靠每一行 JavaScript 代碼命令式地修改 DOM 元素來達到修改界面的目的。

          數據驅動界面改動

          既然需要不斷修改界面,而命令式修改界面又如此繁瑣,是否有別的方法來修改界面呢?數據驅動的概念應運而生。它的基本思想是:使用數據來描述應用的狀態(tài),將界面的修改與數據的修改綁定起來,從而實現數據的任何修改都直接反映到界面的修改。

          如果用一個公式來表示的話,可以寫成UI=F(state),UI 即指用戶界面,state指應用的狀態(tài),而F則是應用狀態(tài)與用戶界面的映射關系定義。它最直觀的理解是“應用的任何狀態(tài),都可以通過一種確定的映射關系,反映到界面的某種狀態(tài)上”,因此只要狀態(tài)發(fā)生變化,界面也會發(fā)生變化。

          上述例子使用 Vue 來編寫:

          <template>
              <button @click="query">查詢</button>
              <div class="price">今日蘋果價格為 {{price}}/千克</div>
              <div class="loading" v-show="isLoading">加載中</div>
          </template>
          <script>
              export default {
                  data(){
                      return {
                          price: 0,
                          isLoading: false
                      }
                  },
                  methods: {
                      query(){
                          this.isLoading = true;
                          queryPrice((data) => {
                              this.price = data.price;
                              this.isLoading = false;
                          });
                      }
                  }
              }
          </script>
          

          在上述代碼中,應用的狀態(tài)就是data,包括兩個值priceisLoading。我們的邏輯代碼只需要對這兩個值進行操作,即可以完成整個應用功能的界面變化。

          而狀態(tài)和界面的映射則由<template>來定義,例如今日蘋果價格為 {{price}}/千克表示狀態(tài)中的price變量需要顯示在此處,v-show="isLoading"則表示是否顯示加載中完全由變量值isloading決定。

          事實上當前前端主流的幾大框架,在界面的渲染上都不約而同選擇了數據驅動的方式,深入理解這一模式有助于我們更好地理解前端框架。

          數據驅動帶來的好處

          數據驅動界面變更能迅速被廣泛接受,也可以從側面說明它確實為開發(fā)者帶來了一些好處。

          首先,因為開發(fā)者僅需要管理數據,使得關于界面細節(jié)控制的代碼不再需要開發(fā)者編寫。同時,由于狀態(tài)被抽象出來,同一個變量值在界面上的多處變化全部由映射關系來決定,而不需要開發(fā)者手工修改每一處變化。這兩者結合起來使得開發(fā)者的心智負擔大大減少,需要關注的代碼量也大大減少,從而使得開發(fā)效率得以大幅提升,出現 bug 的概率也大大減少。

          其次,專門將應用狀態(tài)抽象出來,使得開發(fā)者必須認真思考代碼的組織方式,而因為界面相關細節(jié)的消失,大部分的代碼都變成了邏輯代碼,使得傳統(tǒng)編程中的模式都可以被應用到前端代碼中,從而使得前端代碼能夠支持更大規(guī)模的應用,也能更好地組織前端代碼本身,使得代碼更容易閱讀和維護。

          狀態(tài)的抽象也使得開發(fā)者可以精準地保存和還原任意一個界面狀態(tài)。因為界面的每一時刻的界面表現都是由這一時刻的應用狀態(tài)決定,因此只要能夠將此時的應用狀態(tài)進行保存,就能在另一個時間、空間中重現應用此時的界面表現。

          這個特性在某一些場景下非常好用,例如線上 bug 的排查。如果我們有辦法取到用戶的當前狀態(tài),就有辦法完全還原用戶的界面表現,從而快速復現應用碰到的 bug,而不用再苦苦和用戶溝通詳細的操作步驟,一點點地確認應用可能是哪里出了問題。除此之外,這個特性還可以用于實現“時間旅行”效果,即應用界面的回放。我們只需要將狀態(tài)的變更都記錄下來,就能看到應用從初始化一直到最終狀態(tài)中間發(fā)生的完整事情。它本身可以作為一個效果來使用,也可以用來支持一些功能(例如撤銷/重做)。

          因為應用界面完全由應用狀態(tài)決定,而狀態(tài)映射到界面的操作一般由框架來幫忙我們完成,因此在測試的時候,就有機會將重點放在狀態(tài)的測試上。即在很多情況下,我們只需要測試邏輯和數據,確保應用狀態(tài)是正確的,即可大概認為界面是正確的。

          因為界面測試的成本要遠高于邏輯和數據測試,如果我們能在不做界面測試的情況下也保證應用邏輯和狀態(tài)是正確的,將大大提升測試效率。

          小結

          時至今日,數據驅動已經成為絕大部分前端開發(fā)者的共識,Vue 也是這一理論的實踐者,后面我們將看到 Vue 是如何實現這一重要特征的。

          歡迎關注「慕課網」,發(fā)現更多IT圈優(yōu)質內容,分享干貨知識,幫助你成為更好的程序員!


          主站蜘蛛池模板: 亚洲日韩一区精品射精| 欧美日本精品一区二区三区| 久久久人妻精品无码一区| 亚洲一区二区三区无码国产| 伦精品一区二区三区视频| 成人免费观看一区二区| 波多野结衣一区在线| 久久久国产精品无码一区二区三区| 精品国产日韩亚洲一区在线| 精品一区二区三区视频在线观看| 精品国产日韩亚洲一区| 无码人妻久久一区二区三区免费丨| 国产AV一区二区精品凹凸| 国产精品一区在线麻豆 | 日韩一区二区三区免费播放| 亚洲色无码一区二区三区| 欧美激情国产精品视频一区二区 | 无码人妻aⅴ一区二区三区| 免费萌白酱国产一区二区| 亚洲高清毛片一区二区| 久久se精品一区二区| 国产一区二区三区在线视頻| 国产精品乱码一区二区三| 国产香蕉一区二区精品视频 | 国产伦理一区二区| 中文字幕永久一区二区三区在线观看 | 中文字幕一区二区三区在线观看 | 国产精品毛片VA一区二区三区| 日本中文字幕在线视频一区| 国产麻豆剧果冻传媒一区| 亚洲色精品三区二区一区| 国产精品视频分类一区| 亚洲av无码一区二区三区人妖| 影音先锋中文无码一区| 麻豆AV天堂一区二区香蕉| 精品91一区二区三区| 精品日本一区二区三区在线观看| 一区二区三区无码高清| 精品久久一区二区三区| 精品熟人妻一区二区三区四区不卡 | 日韩一区二区三区免费体验|