Vue 小結

本次串講的主要目的在於給我們移動端的同學揭祕下目前前端開發的現狀,和一些典型框架或者說是庫的產生背景、以及設計思想和解決了什麼樣的問題。以 Vue.js 爲例。此次講解圍繞以下幾個方面展開:

MV* 框架模式

歷史

最早期的 Web 開發是洪荒時代,開發者可能寫着類似以下的代碼。檢查用戶的輸入合法性,然後提交用戶的表單字段到達服務器。服務器再校驗一遍用戶的合法性

<html>
    <head>
        <meta charset="UTF-8">
        <meta name="description" content="洪荒時代開發Web網頁">
        <title>洪荒時代</title>
        <meta>
    </head>
    <body>
        <form action="http://sdg.com/login" method="POST" οnsubmit="return validate();">
            <label for="username">用戶名</label>
            <input type="text" name="username" id="username" placeholder="請輸入用戶名">
            <label for="password">密碼</label>
            <input type="password" name="password" id="password" placeholder="請輸入密碼">
            <input type="submit">
        </form>
    </body>
    <script>
        /*
        * 判斷字符串是否爲空
        */
        function isNotEmptyStr($str) {
        if($str == "" || $str == undefined || $str == null || $str == "null") {
            return false;
        }
        return true;
        }

        function validate () {
            var username = document.getElementById("username").value;
            var password = document.getElementById("password").value;
            if (!isNotEmptyStr(username)) {
                alert("請輸入用戶名");
                return false;
            }
            if (!isNotEmptyStr(password)) {
                alert("請輸入密碼");
                return false;
            }
        }
    </script>
</html>
$username = addslashes($_REQUEST['username']);
$password = md5($_REQUEST['password']);
//數據表
 $table = "user";

//3.得到連接對象
$PdoMySQL = new PdoMySQL();
if ($action == "login") {
    $salt = "CRO";
    $identidier = md5($salt.md5($username.$salt));
    $token = md5(uniqid(rand(),true));
    $time = time()+60*60*24*7;
    $currentime = time();
    $allrow = $PdoMySQL->find($table,"username='{$username}' and password='{$password}'");
    $PdoMySQL->update(["time"=>$time,"identifier"=>$identidier],$table,"username='{$username}' and password='{$password}'");

    $autoRows = $PdoMySQL->find($table,"username='".$username."' and identifier='".$userid."'");

    if(count($autoRows) == 1){
        if($currentime < $autoRows[0]["time"]){
            setcookie('auth',base64_encode($autoRows[0]["id"]));
            // 跳轉到主頁 
        }else{
            // 給出用戶信息失敗的提示 alert
        }
    }
}

再到後來 Javascript 技術的發展越來越完善,網頁開發有了更復雜的 JS 動畫、CSS的特性也越來越強,讓洪荒時代的 web 開發步入到“火藥文明時代”。一些大型應用的場景,頁面的數據狀態非常多,傳統的頁面開發方式有了一些問題。

  1. 比如頁面一個報錯如果是服務端渲染,那麼 error 信息直接顯示到頁面上。對於用戶而言這些 error 信息很懵逼,體驗很不好
  2. error 信息裏面有你的服務端信息,比如什麼語言,什麼框架,什麼版本,什麼引擎、什麼服務器,這些東西對於不懷好心的 Eve 就可以利用現有漏洞去攻擊服務器
  3. 開發維護方式很不友好。假如你的頁面有報錯信息,你甚至需要前端開發者和服務端開發者一起去排查問題。開發方式就是前端開發者寫模版代碼,寫好之後將代碼交給服務端開發者,服務端開發者根據業務,去操作數據庫執行 SQL ,再通過類似於 JSP、PHP 這種傳統的技術渲染頁面。開發效率極低。

後來誕生了 ajax 技術。通過 ajax 提高一個較好的體驗( 是一種在無需重新加載整個網頁的情況下,能夠更新部分網頁的技術。Ajax 在瀏覽器與 Web 服務器之間使用異步數據傳輸(HTTP 請求),這樣就可使網頁從服務器請求少量的信息,而不是整個頁面)。有了 ajax 賦能前端開發採用了前後端分離的方案,服務端、前端各司其職。前後端開發者通過接口通信,前端開發者專心做提高用戶體驗的前端事情,比如寫酷炫的動畫。傳統的服務端渲染的路子走不通了。在此背景下催生了 REST api 。前端開發人員高興壞了,開發者有了能力去開發大型應用。

再到後來舊版本、性能低、不主動擁抱變化的瀏覽器逐漸淘汰,體驗不好,用戶自然不願意去用,那麼就要淘汰。移動智能設備的誕生讓傳統的 PC 頁面開始在移動端進行嘗試,發現效果還可以。當用戶也越來越挑剔、用戶體驗的要求也越來越高。那麼傳統的開發方式也不能滿足現在的需求了。用戶多了,業務複雜了,那麼 MVC 也滿足不了現在開發者的要求,於是 MVVM 誕生了。當然前端也在搞工程化。

應用越複雜,現有狀況就是數據狀態分散在 model 和 view 中。假如Jquery時代經常將數據隱藏在form表單中只不過是隱藏的。比如 <input class="hidden" id="userId" name="userId"> 點擊按鈕更新用戶信息的時候經常需要將隱藏的數據也提交掉。在此背景下誕生了最早一批的框架,代表有 Backbone、Ember。

MV* 說明(MVC、MVP、MVVM…)

  1. 先不講 MVC 是什麼,先談談軟件設計的一些原則和理念。
    • 可靠性:應用的功能可以正常使用
    • 健壯性:在用戶非正常使用的時候,應用也可以正常反應,不要奔潰
    • 效率性:啓動時間、響應時間、效率等在用戶可以容忍範圍之內

以上3點是表象層的東西,大多數開發者或者團隊都會注意。除了這三點,還有一些東西是需要在工程層面需要注意的方面。

- 可拓展性:軟件不是一次性產品,需要不斷的迭代更新
- 容易理解:代碼易讀、規範
- 可測試性:代碼能夠方便的編寫單元測試和集成測試
- 可複用性:可複用,不需要一次次編寫輪子

於是,軟件設計領域有了幾個通用設計原則幫助我們實現這些目標:單一功能原則、聚合複用原則、接口隔離原則、依賴倒置原則…

基於這些設計目標和理念又有了設計模式:MVC、MVVM 就屬於這個範疇。

  1. MV*

MVC

  • MVC:Model(模型) + View(視圖) + Controller(控制器),主要目的在於分層,各司其職。 View 通過 Controller 來和 Model 聯繫。Controller 用來管理 View 和 Model。View 將事件傳遞給 Controller,Controller 完成業務邏輯後要求 Model 改變,Model 將新的數據發送到 View,用戶得到反饋。

MVP

  • MVP:從 MVC 演變而來,都通過 Presenter/Controller 負責邏輯處理,View 負責界面展示,Model 負責數據。在 MVP 中主要邏輯在 Presenter 中。View 與 Model 不發生聯繫,都通過 Presenter 傳遞。View 層非常薄,不部署任何業務邏輯,沒有任何主動性,而 Presenter非常厚,所有邏輯都部署在那裏。

MVVM

  • MVVM:將 MVP 中的 中,Presenter 變成了 ViewModel,View 的變動會自動同步到 ViewModel,ViewModel 的變化也會同步到 View 上,這種同步的實現是對 ViewModel 中的屬性實現了 Observer,當對屬性存取會觸發 setter 和 getter,都會觸發對應的操作。

Vue.js

對於 Vue.js 來說不只是技術的革新也是開發方式的革新。前端框架和移動端框架的差異:前端框架更像是革命性的革新,連開發方式都是天翻地覆的變化。前端裏面 MVVM 的思想每個庫基本都有實現;移動端的話比較少,幾個大廠纔有實現方式,但是使用起來感覺並不是很美好。
舉個例子:iOS 端的 ReactiveCocoa 使用起來高學習門檻、易出錯、調試困難、風格不統一等被詬病。後來美團自研了 EasyReact。它的誕生是爲了解決 iOS 工程實現 MVVM 架構但沒有對應的框架支撐,而導致的風格不統一、可維護性差、開發效率低等多種問題。而 MVVM 中最重要的一個功能就是綁定,EasyReact 就是爲了讓綁定和響應式的代碼變得 Easy 起來。

什麼是 Vue.js

Vue (讀音 /vjuː/,類似於 view) 是一套用於構建用戶界面的漸進式框架。與其它大型框架不同的是,Vue 被設計爲可以自底向上逐層應用。Vue 的核心庫只關注視圖層,不僅易於上手,還便於與第三方庫或既有項目整合。另一方面,當與現代化的工具鏈以及各種支持類庫結合使用時,Vue 也完全能夠爲複雜的單頁應用提供驅動。

在我看來 Vue.js 的核心思想就是「數據驅動、組件化開發、虛擬Dom」。當然結合它的腳手架讓你開發一個複雜且良好的大型應用變得很容易。下面看一個 Demo 來說明下 Vue.js 的強大威力。

<html>
    <head>
        <title>Vue</title>
        <style>
            div{
                margin: 50px;
            }
            input {
                border: 1px solid cyan;
                height: 30px;
                line-height: 30px;
            }
            p {
                font-size: 30px;
            }
        </style>
    </head>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <body>
       <div id="el">
        <input type="text" v-model="username">
        <p>{ {username} }</p>
        <ul>
            <li v-for="(el,index) in hobby" :id="index">{ {el.msg} }</li>
        </ul> 
       </div> 
    </body>
    <script>
        var vm = new Vue({
            el: '#el',
            data () {
                return {
                    username: '@杭城小劉',
                    hobby:  [
                                {msg: '電影'},
                                {msg: '美食'},
                                {msg: '旅遊'},
                                {msg: '乒乓球'},
                                {msg: '編程'}
                            ]
                }
            }
        })
    </script>
</html>

在上面的代碼中就聲明瞭一個 MVVM 框架的 Web 應用,怎麼體現?你可以在打開 Chrome 的調試界面,快捷鍵爲 Command + Option + i,你可以在 console 中輸入以下指令,可以看到界面會自動更新

vm.$data.username = '劉斌鵬'
vm.username = '劉斌鵬'
vm._data.username = '劉斌鵬'

vm.$data.hobby.push({msg: '探索本質'})  
vm.$data.hobby.pop()
vm.$data.hobby.shift()

爲什麼呢?底層實現原理是通過 new Vue({}) 聲明瞭一個 MVVM 對象,綁定的 View 通過 el 獲取到,數據就是原生的 Javascript 對象,這個 ViewModel 將 View 和 Model 綁定在一起, View 和 Model 不直接聯繫,但是 v-model="username" 是個什麼鬼? v-model 是 Vue.js 中的一個指令,底層實現就是 Vue.js 將該 input 的值和 Model 中的 username 進行了綁定,代碼如下

<input v-bind:value="username" v-on:input="sth=$event.target.value">

我們通過 ViewModel 操縱的是 Model 當 Model 中的數據改變,假如通過 vm.$data.username 就會觸發屬性的 getter,如果通過 vm.$data.username = '劉斌鵬' 訪問的就是屬性的 setter,Vue 觀察到屬性變化會自動操作 View 的響應式變化。

如何學習(前置條件)

  • npm
    npm其實是Node.js的包管理工具(package manager)。開發時,會用到很多別人寫的JavaScript代碼。如果我們要使用別人寫的某個包,每次都根據名稱搜索一下官方網站,下載代碼,解壓,再使用,非常繁瑣。於是一個集中管理的工具應運而生:大家都把自己開發的模塊打包後放到 npm 官網上,如果要使用,直接通過npm安裝就可以直接用,不用管代碼存在哪,應該從哪下載。更重要的是,如果我們要使用模塊A,而模塊A又依賴於模塊B,模塊B又依賴於模塊X和模塊Y,npm可以根據依賴關係,把所有依賴的包都下載下來並管理起來。否則,靠我們自己手動管理,肯定既麻煩又容易出錯。

  • AMD、CommonJS、CMD 等規範

    1. CommonJS 規範
      由於爲了編寫大型應用程序,代碼不可能編寫在一個文件裏,所以代碼(函數、變量)分散在多個文件裏面,每個應用程序都有相應的解決方案,在 Node 中就是“模塊”。模塊的好處也是不言而喻的,當你編寫好某個功能拓展的時候可以很方便的集成到其他的模塊中去引用。那麼 Node 如何實現模塊?由於 Javascript 是函數式編程語言,所以可以利用閉包實現。將我們的代碼用閉包實現起來就可以實現將“變量”只在當前代碼內有效,外部無法訪問,實現了模塊的隔離。所以我們可以將需要暴露出去的東西暴露給外部,這樣子就可以組織大型應用程序的開發

      模擬 CommonJS 的實現

      // 準備module對象:
      var module = {
          id: 'hello',
          exports: {}
      };
      var load = function (module) {
          // 讀取的hello.js代碼:
          function greet(name) {
              console.log('Hello, ' + name + '!');
          }
      
          module.exports = greet;
          // hello.js代碼結束
          return module.exports;
      };
      var exported = load(module);
      // 保存module:
      save(module, exported);
      

      上述代碼就可以實現將所需要的東西實現模塊。CommonJS 規範使用步驟:1. 編寫代碼邏輯,通過 module.export = 變量; 暴露給外部;2. 調用者通過 let 變量名 = require('模塊名') 來導入所需要的模塊,用一個變量去承接,然後訪問屬性和方法
      2.由於 CommonJS 中的規範針對於 Node 很適合,因爲代碼文件是放在服務端磁盤,所以是同步的,讀取速度很快,代碼同步執行沒問題。但是要在瀏覽器端使用這套規範顯然是行不通的。爲什麼?看看下面代碼有什麼問題?

    let Hello = require('./Hello');
    Hello.sayHi()
    

    用戶訪問頁面後卡死了?因爲瀏覽器的環境下代碼資源都需要通過網絡獲取,所以會比較慢,如果是同步用戶訪問的話基本上不會去第二次訪問你的網站了。在此背景下產生了針對瀏覽器環境下的模塊問題的 AMD 規範(Asynchronous Module Definition),想一想如果是你的話如何設計?採用異步加載的方式,模塊的加載不影響後續代碼的執行,如果遇到的代碼是依賴於模塊,那麼這些代碼都會被放到一個回調函數中,等模塊加載完畢纔會去執行回調函數裏面的內容。AMD 也採用 require() 語句,不同於 CommonJS 它要求2個參數。

    reuqire([module], callback)
    

    說明:第一個參數是一個數組,裏面是要加載模塊;第二個參數 callback 是加載成功的回調函數。比如

    require(['./Hello'], () => {
        Hello.sayHi()
    })
    
  • Webpack
    查看以前的文章 Webpackwebpack-dev-server

  • ES6
    幾個概念:ES、JS、CoffeeScript、TypeScript
    ES(ECMAScript):標準
    JS:瀏覽器對其的實現
    CoffeeScript:可以編譯爲 Javascript,拋棄 JS 中一些不好的設計
    TypeScript 是現今對 JavaScript 的改進中,唯一完全兼容並作爲它的超集存在的解決方案

  • Flexbox
    傳統佈局解決方案比如盒模型在實現一些效果的時候不是很方便,所以 W3C 在2009年提出了 Flex 佈局系統。
    Flex參考資料

  • html、CSS
    MDN

如何學習、進階

學習

- 看着 [Vue官方文檔](https://cn.vuejs.org/v2/guide/) 邊看邊寫,因爲在你 coding 的時候是拿着鍵盤寫代碼的,也需要感覺,所以平時多敲代碼,邊思考
- 對於沒有接觸過 ES6 和 Webpack 的童鞋來說,不建議直接用官方的腳手架 **vue-cli** 構件項目。所以先花點時間去學習下 ES6 的威力和 Webpack 解決了什麼樣的問題和它的簡單用法
- 瞭解下 npm 的概念和解決了什麼樣的問題
- 一些 CSS 的知識
- 等適應了 Vue-cli 和工程構建方式以及代碼組織方式後可以看看 Vue-Router、Vuex
- Vue-Router、Vuex 應用到工程項目中去,做一個 TodoList 項目
- 項目結束覆盤、review 下
- [項目 Vue 小結](./2.17.md)

進階

- [Vue 代碼風格指南](https://cn.vuejs.org/v2/style-guide/#避免-v-if-和-v-for-用在一起-必要)
- ES6 喫透(萬變不離其宗,不要一昧追求新技術,掌握本質核心)
- 封裝高階組件(slot 等技術點)
- 設計優秀良好的組件(比如用 TS 書寫代碼類型更爲安全)
- 封裝公司或者業務線或者產品爲核心點的組件庫
- 關注代碼實現原理
- 關注前端的技術社區:[segmentfault](https://segmentfault.com)...
- 思考 Vue 框架設計的思想。類比其他框架甚至是大前端如何實現或者有沒有類似的問題
- 嘗試找到應用的性能癥結所在,分析問題,給出解決方案並優化
- 參加行業的大會。VueConf、ReactConf

MVVM 實現原理

幾種實現雙向綁定的實現原理。

看看下面的代碼

var Book = {};
    var name = '';
    Object.defineProperty(Book, 'name', {
        set: function (value) {
            name = value;
            console.log('本書名稱叫做:' + value);
        },
        get: function () {
            return '<' + name + '>';
        }
    });
Book.name = 'Vue.js 權威指北'
console.log(`我買了本書叫做${Book.name}`);

Object.defineProperty

發現打印出來的東西和 Vue console 中輸出基本一直,所以猜想 Vue 的實現也是依賴 Object.defineProperty

目前主流的框架基本都實現了單向數據綁定,在我看來雙向數據綁定無非就是在單項數據綁定的基礎上實現了給可輸入元素(input、textarea)添加了 change(input)事件來動態修改 Model 和 View,所以我們的注意力不需要注意雙向還是單向數據綁定。Vue 支持單雙向數據綁定。

實現數據綁定的做法大致有如下幾種方式:

  • 發佈者-訂閱者模式:Backbone.js。不去討論
  • 髒值檢查:Angular.js。基本通過 DOM 事件、比如用戶輸入、按鈕點擊、XHR 響應事件、瀏覽器 Location 變更事件、Timer、apply 等
  • 數據劫持:Vue.js。通過數據劫持結合發佈者-訂閱者模式實現。Object.defineProperty() 攔截屬性的 setter 和 getter。在數據變動的時候發佈消息給訂閱者、觸發相應的監聽回調。

思路整理:

  • 實現一個屬性監聽器 Observer,能夠對數據對象的所有屬性進行監聽,如果有變動則將最新的值通知給訂閱者
  • 實現一個指令解析 Compiler,對每個元素節點進行掃描和解析,根據指令模版替換數據,以及綁定相應的更新函數
  • 實現一個 Wacther,作爲連接 Observer 和 Compiler 的橋樑,能夠訂閱並觀察到每個屬性的變化通知,執行指令綁定的相應回調,從而更新視圖
  • MVVM 入口函數,整合Observer、Compiler、Wacther

MVVM

看幾個屬性:Object.defineProperty 中的 writable 和 configurable 和 enumerable 的理解
configurable 如果爲 false 則不可以修改, 不可以刪除。writable 如果設置爲 false 則不可以採用數據運算符進行賦值
做個實驗看看特殊情況。如果 writable 爲 true 的時候, configurable 爲 false 結果如何?

var o = {}; // 創建一個新對象
Object.defineProperty(o, "a", {
  value : "original",
  writable : false, // 這個地方爲 false
  enumerable : true,
  configurable : true
});
o.a = 'LBP'; 
console.log(o.a) // "original" 此時候, 是更改不了 a 的.

var o = {}; // 創建一個新對象
Object.defineProperty(o, "a", {
  value : "original",
  writable : true,
  enumerable : true,
  configurable : false //這裏爲false
});
o.a = "LBP";
console.log(o.a) //LBP.此時候, a 進行了改變

delete o.a // 返回 false

結論:onfigurable 控制是否可以刪除; writable 控制是否可以修改(賦值); enumerable 控制是否可以枚舉

  1. 實現 Observer
    可以利用 Obeject.defineProperty() 來監聽屬性變動,將需要 Observe 的數據對象進行遞歸遍歷,包括子屬性對象的屬性,都加上 setter 和 getter
    給這個對象的某個值賦值就會觸發setter,那麼就能監聽到了數據變化。
var data = {name: '杭城小劉'};
observe(data);
data.name = 'LBP'; 

function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    // 取出所有屬性遍歷
    Object.keys(data).forEach(function(key) {
	    defineReactive(data, key, data[key]);
	});
};

function defineReactive(data, key, val) {
    observe(val); // 監聽子屬性
    Object.defineProperty(data, key, {
        enumerable: true, // 可枚舉
        configurable: false, //不能再delete
        get: function() {
            return val;
        },
        set: function(newVal) {
            console.log('哈哈哈,監聽到值變化了 ', val, ' --> ', newVal);
            val = newVal;
        }
    });
}

這樣我們已經可以監聽每個數據的變化了,那麼監聽到變化之後就是怎麼通知訂閱者了,所以接下來我們需要實現一個消息訂閱器,很簡單,維護一個數組,用來收集訂閱者,數據變動觸發 notify,再調用訂閱者的 update 方法,代碼改善之後是這樣:

...
function defineReactive(data, key, val) {
	var dep = new Dep();
    observe(val); // 監聽子屬性

    Object.defineProperty(data, key, {
        ... 
        set: function(newVal) {
        	if (val === newVal) return;
            console.log('哈哈哈,監聽到值變化了 ', val, ' --> ', newVal);
            val = newVal;
            dep.notify(); // 通知所有訂閱者
        }
    });
}

function Dep() {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};

那麼問題來了,誰是訂閱者?怎麼往訂閱器添加訂閱者?
沒錯,上面的思路整理中我們已經明確訂閱者應該是 Watcher, 而且 var dep = new Dep() 是在 defineReactive 方法內部定義的,所以想通過 dep 添加訂閱者,就必須要在閉包內操作,所以我們可以在 getter裏面動手腳:

// Observer.js
...
Object.defineProperty(data, key, {
	get: function() {
		// 由於需要在閉包內添加watcher,所以通過Dep定義一個全局target屬性,暫存watcher, 添加完移除
		Dep.target && dep.addDep(Dep.target);
		return val;
	}
    ... 
});

// Watcher.js
Watcher.prototype = {
	get: function(key) {
		Dep.target = this;
		this.value = data[key];	// 這裏會觸發屬性的getter,從而添加訂閱者
		Dep.target = null;
	}
}

這裏已經實現了一個 Observer 了,已經具備了監聽數據和數據變化通知訂閱者的功能

  1. 實現 Compile

compile 主要做的事情是解析模板指令,將模板中的變量替換成數據,然後初始化渲染頁面視圖,並將每個指令對應的節點綁定更新函數,添加監聽數據的訂閱者,一旦數據有變動,收到通知,更新視圖,如圖所示:
MVVM-Compile

因爲遍歷解析的過程有多次操作dom節點,爲提高性能和效率,會先將根節點 el 轉換成文檔碎片 fragment 進行解析編譯操作,解析完成,再將 fragment 添加回原來的真實dom節點中

function Compile(el) {
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}
Compile.prototype = {
	init: function() { this.compileElement(this.$fragment); },
    node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(), child;
        // 將原生節點拷貝到fragment
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }
        return fragment;
    }
};

compileElement 方法將遍歷所有節點及其子節點,進行掃描解析編譯,調用對應的指令渲染函數進行數據渲染,並調用對應的指令更新函數進行綁定,詳看代碼及註釋說明:

Compile.prototype = {
	... 
	compileElement: function(el) {
        var childNodes = el.childNodes, me = this;
        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;	// 表達式文本
            // 按元素節點方式編譯
            if (me.isElementNode(node)) {
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) {
                me.compileText(node, RegExp.$1);
            }
            // 遍歷編譯子節點
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },

    compile: function(node) {
        var nodeAttrs = node.attributes, me = this;
        [].slice.call(nodeAttrs).forEach(function(attr) {
            // 規定:指令以 v-xxx 命名
            // 如 <span v-text="content"></span> 中指令爲 v-text
            var attrName = attr.name;	// v-text
            if (me.isDirective(attrName)) {
                var exp = attr.value; // content
                var dir = attrName.substring(2);	// text
                if (me.isEventDirective(dir)) {
                	// 事件指令, 如 v-on:click
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                	// 普通指令
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }
            }
        });
    }
};

// 指令處理集合
var compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
    ...
    bind: function(node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater'];
        // 第一次初始化視圖
        updaterFn && updaterFn(node, vm[exp]);
        // 實例化訂閱者,此操作會在對應的屬性消息訂閱器中添加了該訂閱者watcher
        new Watcher(vm, exp, function(value, oldValue) {
        	// 一旦屬性值有變化,會收到通知執行此更新函數,更新視圖
            updaterFn && updaterFn(node, value, oldValue);
        });
    }
};

// 更新函數
var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    }
    ...
};

這裏通過遞歸遍歷保證了每個節點及子節點都會解析編譯到,包括了{ {} }表達式聲明的文本節點。指令的聲明規定是通過特定前綴的節點屬性來標記,如 <span v-text="content" other-attrv-text 便是指令,而 other-attr 不是指令,只是普通的屬性。
監聽數據、綁定更新函數的處理是在compileUtil.bind() 這個方法中,通過 new Watcher() 添加回調來接收數據變化的通知

  1. 實現Watcher

Watcher 訂閱者作爲 Observer 和 Compile 之間通信的橋樑,主要做的事情是:
1、在自身實例化時往屬性訂閱器(dep)裏面添加自己
2、自身必須有一個 update() 方法
3、待屬性變動 dep.notice() 通知時,能調用自身的 update() 方法,並觸發 Compile 中綁定的回調,則功成身退。

function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    // 此處爲了觸發屬性的getter,從而在dep添加自己,結合Observer更易理解
    this.value = this.get(); 
}
Watcher.prototype = {
    update: function() {
        this.run();	// 屬性值變化收到通知
    },
    run: function() {
        var value = this.get(); // 取到最新值
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal); // 執行Compile中綁定的回調,更新視圖
        }
    },
    get: function() {
        Dep.target = this;	// 將當前訂閱者指向自己
        var value = this.vm[exp];	// 觸發getter,添加自己到屬性訂閱器中
        Dep.target = null;	// 添加完畢,重置
        return value;
    }
};
// 這裏再次列出Observer和Dep,方便理解
Object.defineProperty(data, key, {
	get: function() {
		// 由於需要在閉包內添加watcher,所以可以在Dep定義一個全局target屬性,暫存watcher, 添加完移除
		Dep.target && dep.addDep(Dep.target);
		return val;
	}
    ...
});
Dep.prototype = {
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update(); // 調用訂閱者的update方法,通知變化
        });
    }
};

實例化 Watcher 的時候,調用 get() 方法,通過 Dep.target = watcherInstance 標記訂閱者是當前watcher實例,強行觸發屬性定義的 getter 方法,getter 方法執行的時候,就會在屬性的訂閱器 dep 添加當前 watcher 實例,從而在屬性值有變化的時候,watcherInstance 就能收到更新通知。

  1. 實現MVVM

MVVM 作爲數據綁定的入口,整合 Observer、Compile、Watcher 三者,通過 Observer 來監聽自己的 Model 數據變化,通過Compile 來解析編譯模板指令,最終利用 Watcher 搭起 Observer 和 Compile 之間的通信橋樑,達到數據變化 -> 視圖更新;視圖交互變化(input) -> 數據 Model 變更的雙向綁定效果。

一個簡單的 MVVM 構造器是這樣子:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data;
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}

但是這裏有個問題,從代碼中可看出監聽的數據對象是 options.data,每次需要更新視圖,則必須通過 var vm = new MVVM({data:{name: '杭城小劉'} }); vm._data.name = 'LBP'; 這樣的方式來改變數據。

顯然不符合我們一開始的期望,我們所期望的調用方式應該是這樣的:
var vm = new MVVM({data: {name: '杭城小劉'} }); vm.name = 'LBP';

所以這裏需要給 MVVM 實例添加一個屬性代理的方法,使訪問 vm 的屬性代理爲訪問 vm._data 的屬性,改造後的代碼如下:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data, me = this;
    // 屬性代理,實現 vm.xxx -> vm._data.xxx
    Object.keys(data).forEach(function(key) {
        me._proxy(key);
    });
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
	_proxy: function(key) {
		var me = this;
        Object.defineProperty(me, key, {
            configurable: false,
            enumerable: true,
            get: function proxyGetter() {
                return me._data[key];
            },
            set: function proxySetter(newVal) {
                me._data[key] = newVal;
            }
        });
	}
};

這裏主要還是利用了 Object.defineProperty() 這個方法來劫持了 vm 實例對象的屬性的讀寫權,使讀寫 vm 實例的屬性轉成讀寫了 vm._data 的屬性值,達到魚目混珠的效果

  1. 什麼是單向綁定和雙向綁定?
    單向綁定:將 Model 綁定到 View 上。當我們通過接口或者事件操作 Model 的改變的時候那麼 View 的改變會自動觸發,View 自動刷新改變。
  2. 雙向綁定:將 Model 綁定到 View 上,通過也將 View 綁定到 Model 上。這樣 View 的改變會觸發 Model 的改變,Model 的改變也會自動觸發 View 的自動更新。
  3. Vue 中如何實現單項數據綁定?
    • 通過插值表達式。通過 { {data} } 的形式將數據 Model 中的某個屬性綁定到 Dom 節點上
    • 通過 v-bind 指令。通過 v-bind:class="hasError" 將某個 Model 的屬性綁定到對應的屬性上。這樣 Vue 在識別到 v-bind 指令的時候會自動將屬性跟 Model 綁定起來,這樣就可以通過 ViewModel 操作 Model 來動態的更新 View 層。
  4. Vue 中實現雙向綁定
    Vue 中通過 v-model 實現雙向綁定。可以實現 View 到 Model 的雙向綁定。View 變動了 Model 會跟着變, Model 變了 View 會自動更新。

Vue 與 React 的對比

先看看以下代碼,針對同一個字符串反轉的功能,2個庫如何實現

<div id="app">
  <p>{ { message } }</p>
  <button v-on:click="reverseMessage">Reverse Message</button>
</div>

new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue.js!
  },
  methods: {
    reverseMessage: function () {
      this.message = this.message.split('').reverse().join('');
    }
  }
});
 class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      message: 'Hello React.js!'
    };
  }
  reverseMessage() {
    this.setState({ 
      message: this.state.message.split('').reverse().join('') 
    });
  }
  render() {
    return (
      <div>
        <p>{this.state.message}</p>
        <button onClick={() => this.reverseMessage()}>
          Reverse Message
        </button>
      </div>
    )
  }
}
ReactDOM.render(App, document.getElementById('app'));

相似之處:

  • 都有非常多的 star,開發者非常擁護
  • 都使用 Virtua DOM
  • 提供了響應式和組件化的視圖組件
  • 將注意力放在覈心實現,將其他功能比如路由、全局狀態管理交給相關的庫

差別:

  • React 嚴格上只針對 MVC 的 view 層,Vue 則是 MVVM 模式

  • 數據綁定: Vue 實現了數據的雙向綁定,React 數據流動是單向的
    單向數據流是指數據的流向只能由父組件通過props將數據傳遞給子組件,不能由子組件向父組件傳遞數據,要想實現數據的雙向綁定,只能由子組件接收父組件props傳過來的方法去改變父組件的數據,而不是直接將子組件的數據傳遞給父組件。
    單向數據量組件props是父級往下傳遞,你不能向上去修改父組件的數據,並且也不能在自身組件中修改props的值。React不算mvvm,雖然可以實現雙向綁定,在React中實現雙向綁定通過state屬性,但如果將state綁定到視圖中時,直接修改state屬性是不可的,需要通過調用setState去觸發更新視圖,反過來視圖如果要更新也需要監聽視圖變化 然後調用setState去同步state狀態。標準MVVM應該屬於給視圖綁定數據後,操作數據即是更新視圖

  • virtual DOM 不一樣,Vue 會跟蹤每一個組件的依賴關係,不需要重新渲染整個組件樹.而對於 React 而言,每當應用的狀態被改變時,全部組件都會重新渲染,所以 React 中會需要 shouldComponentUpdate 這個生命週期函數方法來進行控制

  • 組件寫法不一樣, React推薦的做法是 JSX + inline style, 也就是把 HTML 和 CSS 全都寫進 JavaScript 中,即 ‘all in js’; Vue 推薦的做法是 webpack+vue-loader 的單文件組件格式,即 html,css,JS 寫在同一個文件

  • 代碼書寫方式
    使用 Vue 你可以很方便的將現有的工程遷移或者接入 Vue,因爲工程現有的 HTML 就是 Vue 中的視圖模版,你只需要做一些 Webpack 配置化的東西,代碼改動成本低,後期不用 Vue 了你更換框架的成本也比較低。
    但是使用 React 你如果需要對現有工程接入的話成本很高,你甚至是重寫代碼,代碼組織方式,工程處理方式基本也改變了。開發者可能需要適應一段時間,門檻稍高。

  • 運行時性能
    在 React 中當某個組件的狀態發生變化的時候,它會以該組件爲根,將所有的子組件樹進行更新。對於如果知道不需要更新的組件可能需要使用 PureComponent 或者手動實現 shouldComponentUpdate 方法。Vue 中不需要額外注意這些事情,默認實現的。使得開發者專心做業務開發。
    Vue.js使用基於依賴追蹤的觀察並且使用異步隊列更新。輕量,高性能

  • 開發方式
    在 React 中組件的渲染功能都依賴於 JSX(Javascript的一種語法糖,儘管這種方式對於 Javascript 來說很爽,但是對於已有業務進行重構是很麻煩的,爲什麼?你需要將你頁面的東西拆分爲組件,但是在 React 中組件的輸出是靠 render 函數,render 函數內部不能直接寫 HTML,而是需要 JSX 語法糖。
    Vue.js 在這方面就比較友好,對於已經有的項目可以低成本的接入,因爲已有的 HTML 代碼就是模版代碼,然後將業務寫入到 Script 標籤,操作 ViewModel。雖然 Vue.js 的組件也支持 JSX 的方法來寫代碼,爲的就是讓 React 開發者很快上手。

    import React, { Component } from 'react';
    import { Image, ScrollView, Text } from 'react-native';
    
    class AwkwardScrollingImageWithText extends Component {
        render() {
            return (
            <ScrollView>
                <Image
                source={ {uri: 'https://i.chzbgr.com/full/7345954048/h7E2C65F9/'} }
                style={ {width: 320, height:180} } />
                <Text>
                    在iOS上,React Native的ScrollView組件封裝的是原生的UIScrollView。
                    在Android上,封裝的則是原生的ScrollView。
                    在iOS上,React Native的Image組件封裝的是原生的UIImageView。
                    在Android上,封裝的則是原生的ImageView。
                    React Native封裝了這些基礎的原生組件,使你在得到媲美原生應用性能的同時,還能受益於React優雅的架構設計。 
                </Text>
            </ScrollView>
            );
        }
    }
    
  • 組件作用域內的 CSS
    React 中的 css 是通過 css-in-JS 來實現的,和傳統書寫 CSS 是有區別的,不是無縫對接的,
    Vue 中的 css 編寫和傳統的開發是一致的,你可以在  .vue 文件中對標籤添加 scoped 屬性來告訴 css-loader 這些 css 規則只在該模塊內有效。

    <style scoped>
    @media (min-width: 250px) {
        .list-container:hover {
        background: orange;
        }
    }
    </style>
    

    這個屬性的作用就是會自動添加一個屬性,爲組件內的 css 指定作用域,編譯成 .list-container[data-v-21e5b78]:hover

  • 向上拓展
    React 和 Vue 都提供路由、全局狀態管理的解決方案,區別在於 Vue 是官方維護的,React 則是社區維護的。(Vuex、Redux、Vue-Router)
    都有腳手架,Vue-cli 允許你自定義一些設備而 React 不支持。

  • 向下拓展
    React 學習曲線比較陡峭、也可以說對現有的工程改造門檻較高,需要大範圍改寫,相比 Vue 則較爲友善點,侵入性低。可以像 jQuery 一樣引入一個核心的 min.js 文件就可以改造接入現有工程。

  • 原生渲染
    React 有 React Native 一個較爲成熟的方案,Vue 則有阿里的 Weex。差別在於你寫了 React Native 應用則不能在瀏覽器運行,而 Weex 可以在瀏覽器中和移動設備上運行。所謂多端運行的能力,

  • 開發缺點
    Vue 中不能檢測到屬性的添加、刪除,所以可以用類似 React 中的 set 方法。

有 Vue 基礎如何快速上手 Weex

  1. quick demo

  2. 雖然都是採用 Vue.js 開發,但是存在 Weex 與平臺的差異:上下文、DOM、樣式、事件(Weex 不支持事件冒泡和捕獲)、樣式(Weex支持單個類選擇器、並且只支持 CSS 規則的子集)、Vue 網頁端的一些配置、鉤子、在 Weex 中不支持

    • html 標籤
      目前 Weex 支持了基本容器(div)、文本(text)、圖片(image)、視頻(video)等組件,但是需要注意是組件而不是標籤,雖然寫起來跟標籤一樣很像,但是寫其他的標籤必須和這些組合起來使用。類比 Native 的視圖層級

    • Weex 中不存在 Dom
      Weex 解析 Vue 得到的不是 dom,而是原生布局樹

    • 支持有限的事件
      因爲在移動端中所有有些網頁端的事件是不支持的,請查看支持的事件列表

    • 沒有 BOM,但可以調用原生 Api
      DOM?BOM?
      javascript組成:ECMAScript 基本語法;BOM(Borwser Object Model:瀏覽器對象模型,使用對象模擬了瀏覽器的各個部分內容);DOM(Document Object Model:文檔對象模型:瀏覽器加載顯示網頁的時候瀏覽器會爲每個標籤都創建一個對應的對象描述該標籤的所有信息)

      在 Weex 中能夠調用原生設備的 api,使用方法是通過註冊、調用模塊來實現的,其中一些模塊是 Weex 內置的,比如 clipboard、navigator、storage 等。爲了保持框架的通用性,Weex 內置的原生模塊很有限,不過 Weex 提供了橫向拓展的能力,可以拓展原生模塊。具體參考 Androi 拓展iOS 拓展

    • 樣式差異
      Weex 中的樣式是由原生渲染器解析的,出於性能和功能複雜角度的考慮,Weex 對於 css 特性做了一些取捨。(Weex 中只支持單個類名選擇器,不支持關係選擇器、也不知支持屬性選擇器;組件級別的作用域,爲了保持 Web 和 Native 的一致性,需要使用 style scoped 的寫法;支持基本的盒模型和 flexbox 的寫法,box-sizing 默認爲 border-box,margin,padding,border 屬性不支持合併簡寫;不支持 display:none;可以用 display: 0; 代替,display < 0.01 的時候可以點擊穿透;樣式屬性不支持簡寫、提高解析效率;css 不支持 3D 變化)

      • 單位
        Weex 中所有的 css 屬性值單位爲 px,也可以省略不寫
      • Flexbox 支持不完全
        align-items: baseline;align-content:space-around;align-self:wrap_revserse;
      • 顯隱性
        在 Weex 中的 iOS 和 Android 端不支持 display:none; 所以 v-show 條件渲染寫法也是不支持的,可以用 v-if 代替,或者 display:0; 模擬。由於移動端的渲染特點是當 opacity < 0.01 的時候 view 是可以點擊穿透,所以 Weex 中當元素 display < 0.01 的時候元素看不見,但是佔位空間還在,但用戶無法與之交互,同樣點擊時會發生穿透的效果。
      • css3
        相比 React Native 不能用 css3,Weex 的 css3 的支持程度算比較高,但是有一些 css3 的屬性還是不支持的。transform 支持 2D;font-family 支持 ttf 和 woff 字體格式的自定義的字體;liner-gradient 只支持雙色漸變
    • 調試方式
      如果說 React Native 的調試方式解放了原生開發調試、那麼 Weex 就是賦予了 web 模式調試原生應用的能力。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章