從Vue.js談談前端開發的技術棧演變

Vue 小結

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

MV* 框架模式<div id='1'></div>

歷史

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

<html>
    <head>
        <meta charset="UTF-8">
        <meta name="description" content="洪荒時代開發Web網頁">
        <title>洪荒時代</title>
        <meta>
    </head>
    <body>
        <form action="http://sdg.com/login" method="POST" onsubmit="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 開發步入到「火藥文明時代」。一些大型應用的場景,頁面的數據狀態很是多,傳統的頁面開發方式有了一些問題。css

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

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

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

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

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

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

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

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

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

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

  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 <div id="2"></div>

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

什麼是 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 實現原理 <div id='3'></div>

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

看看下面的代碼

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 之間通訊的橋樑,主要作的事情是: 一、在自身實例化時往屬性訂閱器(dep)裏面添加本身 二、自身必須有一個 update() 方法 三、待屬性變更 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 的對比 <div id='4'></div>

先看看如下代碼,針對同一個字符串反轉的功能,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 <div id='5'></div>

  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 模式調試原生應用的能力。

相關文章
相關標籤/搜索