UI狀態(tài)同步簡史 --換種角度輕松理解現(xiàn)代前端框架

你是否對MVVM多少有點(diǎn)不解?
你是奇怪Jquery忽然成為“過時(shí)”的技術(shù)镣丑?
你是否想寫出一個(gè)類似Vue的簡單框架宣谈?

只要你會(huì)用原生JS, 不需要掌握Vue肄梨,react等高深技能阻荒,本文換種角度讓你窮死理解現(xiàn)代前端框架。

0. 關(guān)于UI狀態(tài)同步

有沒有想過众羡,為何使用現(xiàn)代前端框架侨赡?

為何使用前端框架?.png

React, Vue, Angular等提供很有意思的東西粱侣,如組件化羊壹,第三方UI組件,單網(wǎng)頁支持齐婴,腳手加等工具油猫。然而這些不是根本原因,《現(xiàn)代js 框架存在的根本原因》 給出本質(zhì)原因:

現(xiàn)代前端框架支持UI狀態(tài)同步尔店。

所謂UI狀態(tài)同步是指瀏覽器能實(shí)時(shí)顯示JS中的數(shù)據(jù)眨攘,比如js中 name: '張三',則瀏覽器頁面中顯示張三

張三.png

如果js中 `naee = '李四’, 則頁面自動(dòng)變?yōu)槔钏?/p>

李四.png

在以前Jquery以前的時(shí)代嚣州。想實(shí)現(xiàn)這一操作困難重重鲫售,需要不斷的更新dom, 不僅性能首限该肴,而且零碎的dom操作代碼容易導(dǎo)致代碼混亂情竹。如何決這個(gè)問題呢? 按前端發(fā)展的歷程匀哄,分為四步:

  1. 觀察者
  2. 臟檢查
  3. 描述屬性符
  4. 代理
時(shí)間軸.png

1. 觀察者

你沒看錯(cuò)秦效,這里的觀察者就是N大設(shè)計(jì)模式中的觀察者模式雏蛮。在網(wǎng)頁中,觀察者即UI中顯示的內(nèi)容阱州,被觀察者就是JS中存儲(chǔ)的數(shù)據(jù)挑秉。


觀察者1對1.png

JS中存儲(chǔ)的數(shù)據(jù)通常會(huì)在頁面多個(gè)地方顯示, 一個(gè)被觀察者可以對應(yīng)多個(gè)觀察者苔货。


觀察者 一對一.png

我們需要JS中數(shù)據(jù)的變動(dòng)引起頁面的變動(dòng)犀概,即被觀察者變動(dòng),引起對應(yīng)的觀察者A夜惭、觀察者B等變動(dòng)姻灶,這就是觀察者模式。


觀察者變動(dòng).png

實(shí)現(xiàn)此過程很簡單诈茧,把被觀察者存在一個(gè)稱為訂閱池的數(shù)組中产喉,觀察者變動(dòng)時(shí)循環(huán)遍歷訂閱池?cái)?shù)組,更新觀察者即可敢会。


觀察者 訂閱池.png

1.1 最簡單的例子

下面用最簡單的例子展示觀察者曾沈。該例子簡單到用日志輸出console.log() 代表UI變動(dòng)。

var uiName1 = function (val) {
    console.log('Name#1 become:' + val)
};
var uiName2 = function (val) {
    console.log('Name#2 become:' + val)
};

var subjects = [];

subjects.push(uiName1);
subjects.push(uiName2);

function set (val) {
    subjects.forEach(item => {
        item(val)
        })
}
  1. 建立第一個(gè)觀察者uiName1走触, 表示頁面中有一個(gè)地方顯示姓名晦譬。
var uiName1 = function (val) {
   console.log('Name#1 become:' + val)
};

2.建立第二個(gè)觀察者uiName1, 表示頁面中另一個(gè)地方顯示姓名互广。

...
var uiName2 = function (val) {
    console.log('Name#2 become:' + val)
};
  1. 創(chuàng)建一個(gè)數(shù)組敛腌,表示訂閱池 (有些地方也寫作watchers)。訂閱池是本文最重要的三個(gè)概念之一惫皱。
...
var subjects = []
  1. 將兩個(gè)觀察者放入訂閱池中
...
subjects.push(uiName1);
subjects.push(uiName2);
  1. 寫一個(gè)set函數(shù)表示觀察者的變動(dòng)像樊, 參數(shù)val表示姓名值
...
function set (val) {
    subjects.forEach(item => {
        item(val)
        })
}

打開瀏覽器命令行,打入set('Tom') 旅敷,便可看到Name發(fā)生變化

觀察者 演示.png

1.2 多個(gè)被觀察者

上面的例子中只有一個(gè)被觀察者Name生棍,而實(shí)際中有多個(gè)被觀察數(shù)據(jù)。修改上例媳谁,增加一個(gè)被觀察者Age

var uiName1 = function (val) {
    console.log('Name#1 become:' + val)
};
var uiName2 = function (val) {
    console.log('Name#2 become:' + val)
};
//  增加一個(gè)新的被觀察者Age
var uiAge = function (val) {
    console.log('Age become:' + val)
};
//  改造訂閱池
var subjects = {
    name: [uiName1, uiName2],
    age: [uiAge]
};
//  改造set函數(shù)
function set (key, val) {
    subjects[key].forEach(item => {
        item(val)
    })
}
  1. 增加一個(gè)新的被觀察者Age涂滴,通樣用console.log 表示變動(dòng)
...
var uiAge = function (val) {
    console.log('Age become:' + val)
};
...
  1. 改造訂閱池,用對象的Key表示被觀察者晴音,Value為相應(yīng)的觀察者
...
var subjects = {
    name: [uiName1, uiName2],
    age: [uiAge]
};
...
  1. 改造set函數(shù)柔纵,遍歷指定被觀察者的觀察者, 參數(shù)key表示被觀察者nameage, val表示樣變成的值
...
function set (key, val) {
    subjects[key].forEach(item => {
        item(val)
    })
}
...

在命令行控制臺輸入set('name','Tom'), 會(huì)發(fā)現(xiàn)只有name發(fā)生改變, 輸入set('age',18)z則只有age發(fā)生改變

觀察者 多個(gè)被觀察者.png

1.3在頁面顯示

總用命令行日志代碼UI是不行的锤躁,我們把觀察者模式用于網(wǎng)頁搁料。將上例中的

var uiName1 = function (val) {
    console.log('Name#1 become:' + val)
};
var uiName2 = function (val) {
    console.log('Name#2 become:' + val)
};

的觀察者用下方的網(wǎng)頁模版表示:

    <p> {{name}} </p>
    <p> {{name}} </p>
    <p> {{age}} </p>

這種模版非常優(yōu)雅,為簡單起見,我們用如下方式呈現(xiàn)數(shù)據(jù):

    <p my-value='name'> </p>
    <p my-value='name'> </p>
    <p my-value='age'> </p>

我們需要將模版轉(zhuǎn)為上例中觀察者郭计,這一個(gè)過程叫做模版解析compile, 這是本文最重要的三個(gè)概念之二
為確定渲染的范圍霸琴,增加id='app',全部的html代碼如下:

<html>
    <head></head>
    <body>
      <p my-value='name'> </p>
      <p my-value='name'> </p>
      <p my-value='age'> </p>
   </body>
</html>

下面增加JS部分的代碼。

  1. 首先聲明JS的數(shù)據(jù)昭伸,也就是前端框架常說的狀態(tài)State:
var data = {
    name: 'mike',
    age: 1
};
  1. 創(chuàng)建訂閱池和set函數(shù)梧乘,和上例幾乎一樣。只是需要把需要變的值賦值給data
var subjects = {};

function set(key, val) {
    data[key] = val
    subjects[key].forEach(item=> {
        item()
    })
}
  1. 下面需做模版解析庐杨,即把模版解析成觀察者宋下。 參數(shù)id表示只解析id‘a(chǎn)pp’)范圍內(nèi)的html代碼:
function compile (id) {

}
compile('app')
  1. 下面我們補(bǔ)充comile()解析函數(shù)
    4.1 獲取節(jié)點(diǎn)的全部子元素,nodes 的值為[<p my-value='name'> </p>, <p my-value='age'> </p>...]
function compile (id) {
    var nodes = document.getElementById(id).children;
}

4.2 遍歷子節(jié)點(diǎn)辑莫,node 的值為<p my-value='name'> </p>,<p my-value='age'> </p>

function compile (id) {
    var nodes = document.getElementById(id).children;
    for (let i = 0; i < nodes.length; i ++ ) {
        let node = nodes[i];
    }
}

4.3 如果包含屬性my-value則獲取該值,property 的值為 nameage罩引,表示被觀察者各吨。

...
let node = nodes[i]
if (node.hasAttribute('my-value')) {
      let property = node.getAttribute('my-value');
...

4.4 如果訂閱池中沒有被觀察者則放入被觀察者

...
let property = node.getAttribute('my-value');
if (!subjects.hasOwnProperty(property)) {
        subjects[property] = []
      }

4.5 推入觀察者至訂閱池

...
if (!subjects.hasOwnProperty(property)) {
        subjects[property] = []
      }
subjects[property].push(()=>{
        node.innerHTML = data[property]
      })
...

4.6 修改Dom的顯示

...
node.innerHTML = data[property]
...

完整JS代碼如下

var data = {
    name: 'mike',
    age: 1
};

var subjects = {};
compile('app')

function set(key, val) {
    data[key] = val
    subjects[key].forEach(item=> {
        item()
    })
}

function compile(id) {
  var nodes = document.getElementById(id).children;
  for (let i = 0; i < nodes.length; i ++ ) {
    let node = nodes[i];
    if (node.hasAttribute('my-value')) {
      let property = node.getAttribute('my-value');
      if (!subjects.hasOwnProperty(property)) {
        subjects[property] = []
      }
      subjects[property].push(()=>{
        node.innerHTML = data[property]
      })
      node.innerHTML = data[property]
    }
  }
}

打開頁面顯示如下:


觀察者 mike.png

在命令行輸入set('name', 'Jim') 會(huì)發(fā)現(xiàn)頁面相應(yīng)改變

觀察者 JIm.png

輸入`set('age', 99) 會(huì)改變年齡


觀察者 99.png

1.4 使用者

emberJs, 微信小程序,react等都在使用觀察者模式袁铐,利用set揭蜒,setData或類似函數(shù)更新數(shù)據(jù),很常見剔桨。

觀察者 使用者.png

2.臟檢查

觀察者通過模版解析和訂閱池實(shí)現(xiàn)了UI狀態(tài)同步屉更,然而想更新被觀察者,需要手動(dòng)的調(diào)用set(key, value)函數(shù)洒缀,并不方便瑰谜,如果JS中的狀態(tài)變化能自動(dòng)調(diào)用set函數(shù)就好啦堰怨。
為解決這個(gè)痛點(diǎn)痢法,Angular1.0 提出臟檢查這一概念。當(dāng)觸發(fā)了某些條件卦碾,比如頁面加載完成饺饭,用戶點(diǎn)擊渤早,或者一些數(shù)據(jù)發(fā)生改變后,會(huì)遍歷所有的數(shù)據(jù)進(jìn)行檢查瘫俊,如果發(fā)現(xiàn)有變化的地方則更新鹊杖。

臟檢查.png

Angular雖然對臟檢查做了很多優(yōu)化,深入了解可以閱讀angular臟檢查原理及偽代碼實(shí)現(xiàn)扛芽。但由于經(jīng)常要遍歷全部數(shù)據(jù)骂蓖,對現(xiàn)在的大型網(wǎng)頁應(yīng)用而言,效率太慢胸哥。當(dāng)Angular維護(hù)的狀態(tài)達(dá)到數(shù)百后涯竟,可能會(huì)出現(xiàn)卡頓現(xiàn)象。

3 屬性描述符

如何能搞效的進(jìn)行UI狀態(tài)同步? 屬性描述符(或稱為對象定義屬性)defineProperty庐船,給出答案银酬。我們利用defineProperty的getter 和 setter劫持?jǐn)?shù)據(jù)對象,當(dāng)數(shù)據(jù)變動(dòng)時(shí)會(huì)自動(dòng)調(diào)用setter中的方法筐钟,進(jìn)而改變頁面揩瞪。


屬性描述符 劫持.png

Object.defineProperty 可以豐富對象的取值和賦值操作,語法如下:

Object.defineProperty(obj, prop, descriptor)

obj是目標(biāo)對象篓冲, prop是屬性名即鍵值李破,descriptor是目標(biāo)屬性所擁有的特性。返回值是被傳遞給函數(shù)的對象壹将, 簡言之一個(gè)對象嗤攻。具體語法參見理解Object.defineProperty的作用

再看下面的例子

var data = {}  // 被劫持的對象
Object.defineProperty(data,   ‘name’, {
   enumerable: true,  // 可枚舉
   configurable: true,  // 可忽略
   get () {                    // 攔截取值
        return val
    },
   set (newVal) {        // 攔截賦值操作
        val = newVal
        console.log('我被劫持了') 
    }
})

當(dāng)你在命令行執(zhí)行 data.name = 'Tom'時(shí),會(huì)發(fā)現(xiàn)輸出一條日志我被劫持了

3.1 用defineProperty做UI狀態(tài)同步

仍然用之前的代碼诽俯,只是增加對象的劫持操作

<div id=‘a(chǎn)pp’>
    <p> {{name}} </p>
    <p> {{name}} </p>
    <p> {{age}} </p>
</div>
<script>
    var data = {
        name: 'mike',
        age: 1
    };

    var subjects = {}
    compile(‘a(chǎn)pp’)
    obverser(data) // 注意這里妇菱,我媽劫持data啦

      function set(key, val) {/* 同前例.. */}
      function compile(el) {* 同前例.. */}

      function obverser(data) {
         // 注意這里,補(bǔ)全obverse函數(shù)
      }
    </script>

注意obverser(data)這一行暴区,obverse劫持對象闯团,這是本文三個(gè)重點(diǎn)之三,參數(shù)data是被劫持的數(shù)據(jù)仙粱。

obverser()函數(shù)寫起來也很簡單房交,首先遍歷data的每一個(gè)屬性。Object.keys能把對象的鍵轉(zhuǎn)為一個(gè)數(shù)組如['name','age'], forEach遍歷這個(gè)數(shù)組伐割。

...
function obverser(data) {
   Object.keys(data).forEach(key=>{
       let value = data[key]

之后添加getter選項(xiàng)候味,直接返回?cái)?shù)據(jù)的值即可。

...
function obverser(data) {
   Object.keys(data).forEach(key=>{
       let value = data[key]

       Object.defineProperty(data, key, {
           get () {
               return value
           },

最后增加getter攔截函數(shù)

...
       Object.defineProperty(data, key, {
           get () {
               return value
           },
           set (newValue) {
                if (value != newValue) { // 只有在賦不同值后才起作用隔心,避免循環(huán)調(diào)用
                   // console.log('我被劫持啦')
           value = newValue
                   set(key, value) // 以前需手動(dòng)寫的set函數(shù)负溪,現(xiàn)在可以自動(dòng)運(yùn)行
                }     
            }  
         }) 
    }) 
}

至此完工,打開瀏覽器看看效果济炎。


屬性描述符 運(yùn)行1.png

在Console中輸入data.name= 'Jim'試試川抡? 看,不需要手動(dòng)寫set函數(shù)

屬性描述符 運(yùn)行2.png

再輸入data.age = 99 改下年齡

屬性描述符 運(yùn)行3.png

3.2 小結(jié)

再理下思路须尚,不外乎三點(diǎn):

  • UI中崖堤,對模版進(jìn)行解析compile,產(chǎn)生觀察者
  • JS中耐床,對狀態(tài)state進(jìn)行劫持(或稱作觀察)observer密幔,產(chǎn)生被觀察者
  • 通過訂閱池Watchers進(jìn)行連接


    屬性描述符 小節(jié).png

本文的例子非常簡單,只解釋概念撩轰,如果繼續(xù)完善下去胯甩,比如增加對表單onChange事件的監(jiān)聽昧廷,可以做出一個(gè)類似Vue的MVVM的框架,有興趣可以閱讀《剖析vue實(shí)現(xiàn)原理偎箫,自己動(dòng)手實(shí)現(xiàn)mvvm》

這就是《一種基于訪問器劫持的前端數(shù)據(jù)雙向綁定實(shí)現(xiàn)方法》木柬,你沒看錯(cuò),這竟然被注冊成專利淹办,有興趣可以深入閱讀《雙向綁定也能申請專利》

描述屬性符 專利.png

Vue眉枕,Angular 2以后的版本,以及國人出品的框架avalon在使用這種技術(shù)


屬性描述符 使用.png

4代理

事情往往并不完美怜森,屬性描述符defineProperty也是如此速挑。在聲明對象屬性后,defineProperty才能對該屬性進(jìn)行劫持副硅,于是vue中我們還需要寫this.$set(data, key,val)以添加新的屬性姥宝。本節(jié)講的代理將能完美解決definePropery的缺點(diǎn)。
代理Proxy, 作為ES6的新特性可能會(huì)遇到瀏覽器兼容問題恐疲。又由于profill的降級對代理幾乎沒用伶授,很少有人將代理用于時(shí)間開發(fā)中,相信隨著現(xiàn)代瀏覽器的普及這一現(xiàn)狀將得到改變流纹。使用代理Proxy前最好檢查下瀏覽器的兼容問題,參加《Can I use proxy ?》

代理 兼容.png

4.1 簡明代理語法

Proxy代理违诗, 可以理解在目標(biāo)對象之前架設(shè)一層“攔截”漱凝,外界對該對象的訪問,都必須先通過這層攔截诸迟,因此提供了一種機(jī)制茸炒,可以對外界的訪問進(jìn)行過濾和改寫。
比如阵苇,用proxy攔截取值操作:

var proxy = new Proxy({}, {
     get: function () { // 攔截取值壁公,類似getter
        return 1;
    }
}
proxy.name // 1
proxy.book //1

Proxy的寫法都如此, 語法如下:

var proxy =  new Proxy(target, handler)

new Proxy表示生成一個(gè)Proxy實(shí)例绅项, taget參數(shù)表示所要攔截的目標(biāo)對象紊册。目標(biāo)對象可以是js中的對象,數(shù)組快耿,函數(shù)甚至另一個(gè)代理囊陡。handler參數(shù)也是一個(gè)對象,用來定制攔截行為掀亥。返回值是Proxy對象, new Proxy是穩(wěn)定操作撞反,不會(huì)對target有任何影響。

常見handler 的有:

  • get: 攔截取值
  • set: 攔截賦值
  • deleteProperty: 攔截刪除
  • apply: 攔截函數(shù)執(zhí)行
  • defineProperty: 攔截defineProperty操作
    更多Proxy操作可閱讀《ECMAScript6入門》
代理 語法.png

代理給JS編程打開了一扇門搪花,靈活快速遏片,可稱是對JS的“元編程”嘹害。代理的用途很廣泛,比如表單驗(yàn)證吮便,圖片懶加載笔呀,異步隊(duì)列,等等线衫,有興趣可以閱讀(《使用 Javascript 原生的 Proxy 優(yōu)化應(yīng)用》)[https://juejin.im/post/5a3cb0846fb9a044fb07f36c]

4.2 狀態(tài)同步代理版

用代理做UI狀態(tài)同步非常簡單凿可,我們還是用上例的代碼,只需修改observer函數(shù)即可授账。


<div id=‘a(chǎn)pp’>
    <p> {{name}} </p>
    <p> {{name}} </p>
    <p> {{age}} </p>
    <p value='phone'> </p> <!-- phone 是用來做什么的枯跑? 最后說 -->
</div>
<script>
    var data = {
        name: 'mike',
        age: 1
    };

    var subjects = {}
    compile(‘a(chǎn)pp’)
    obverser(data) // 注意這里,我媽劫持data啦

    function set(key, val) { /* 同前例.. */ }
    function compile(el) { /* 同前例.. */ }

    function obverser(state) {
         // 注意這里白热,重寫obverser函數(shù)
      }
</script>

1.創(chuàng)建代理敛助, 注意為避免變量重復(fù),這里把函數(shù)參數(shù)改為state

function obverser(state) {
    data = new Proxy(data, {
        
    })
}
  1. 攔截取值操作
function obverser(target) {
    data = new Proxy(target, {
        get (target, property) {
            return target[property]
        },
    })
}

其中targert表示目標(biāo)對象屋确, property表示目標(biāo)對象的屬性, return target[property] 相當(dāng)于把原對象的值直接返回

  1. 攔截賦值操作
function obverser(target) {
    data = new Proxy(target, {
        get (target, property) {
            return target[property]
        },
        set (target, property, newValue) {
            target[property] = newValue
        set(property, newValue)}
    })
}

其中targert表示目標(biāo)對象纳击, property表示目標(biāo)對象的屬性, newValue顧名思義是新設(shè)置的值攻臀。target[property] = newValue 是賦值操作焕数。 set(property, newValue) 就是前面眾多例子中的set()函數(shù)。
至此刨啸,結(jié)束堡赔。
打開網(wǎng)頁看下效果,注意日志中清晰簡潔的呈現(xiàn)數(shù)據(jù)设联。

代理 演示.png

輸入data.name = 'Jim', 會(huì)發(fā)現(xiàn)名字由Mike變?yōu)镴im

代理 name.png

輸入data.age= 99, 會(huì)發(fā)現(xiàn)年齡由99變?yōu)?

![代理 phone.png](https://upload-images.jianshu.io/upload_images/1902062-2f6bc1d150bde1bb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

輸入data.phone= 15442258, 會(huì)發(fā)頁面多出了電話號碼善已。

請注意在

var data = {
        name: 'mike',
        age: 1
    };

data的聲明中我們并沒有寫phone, 只是在模版中寫有

<p value='phone'> </p> <!-- phone 是用來做什么的离例? 現(xiàn)在說 -->

可以看到代理可以對沒有聲明的屬性進(jìn)行監(jiān)聽换团,完美解決描述屬性符的問題。

附: 完整代碼

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>5代理</title>
</head>
<body>
<div id ='app'>
    <p value='name'> </p>
    <p value='name'> </p>
    <p value='age'> </p>
    <p value='phone'> </p>
</div>
<script>

var data = {
    name: 'mike',
    age: 1
};

var subjects = {};
function set(key, val) {
    subjects[key].forEach(item=> {
        item()
    })
}

function compile(el) {
  var nodes = document.getElementById(el).children;
  for (let i = 0; i < nodes.length; i ++ ) {
    let node = nodes[i];
    if (node.hasAttribute('value')) {
      let property = node.getAttribute('value');
      if (!subjects.hasOwnProperty(property)) {
        subjects[property] = []
      }
      subjects[property].push(()=>{
        node.innerHTML = data[property]
      });
      node.innerHTML = data[property] || ''
    }
  }
}
compile('app');

obverser(data);
function obverser(state) {
  data = new Proxy(state, {
    get (target, property) {
      return target[property]
    },
    set (target, property, newValue) {
      target[property] = newValue;
      set(property, newValue);
    }
  })
}
</script>
</body>
</html>

4.3 更進(jìn)一步:雙向綁定

我們用代理做狀態(tài)同步宫蛆,再進(jìn)一步艘包,我們可以用代理做雙向綁定。實(shí)現(xiàn)原理很簡單耀盗,仍用前例的代碼辑甜,只是更改compile函數(shù),增加對輸入框的監(jiān)聽

      node.addEventListener('input', () => {
        // 利用代理的set攔截袍冷。
        // 相當(dāng)于在瀏覽器console中輸入data.name = 'Jim'
        data[property] = node.value
      })

看下效果磷醋。


代理 雙向綁定.png

這部分內(nèi)容已經(jīng)超出本文的范圍,有興趣可以直接閱讀下面的代碼和注釋胡诗。本例可能是行數(shù)最少的雙向綁定代碼邓线,只是比前面的例子增加了幾行代碼淌友。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>6代理雙向綁定</title>
</head>
<body>
<div id ='app'>
    <p value='name'> </p>
    <input type="text" model = 'age'> <!-- 增加文本輸入框 -->
    <p value='age'> </p>
</div>
<script>

var data = {
    name: 'mike',
    age: 1
};

var subjects = {};
function set(key) {
    subjects[key].forEach(item=> {
        item()
    })
}

/*
* 只重寫compile函數(shù),其余JS代碼均沒變化
* */
function compile(el) {
  var nodes = document.getElementById(el).children;
  // 遍歷子節(jié)點(diǎn)骇陈,同前例
  for (let i = 0; i < nodes.length; i ++ ) {  
    let node = nodes[i];
    // 為模版綁定值震庭,同前例讓js的數(shù)據(jù)顯示在頁面上
    if (node.hasAttribute('value')) {
      let property = node.getAttribute('value');
      if (!subjects.hasOwnProperty(property)) {
        subjects[property] = []
      }
      subjects[property].push(()=>{
        node.innerHTML = data[property]
      });
      node.innerHTML = data[property] || ''
    // 新增部分: 當(dāng)遇到`model'屬性,表示雙向綁定,  
    } else if (node.hasAttribute('model')) {
      // 為模版綁定值你雌,讓js的數(shù)據(jù)顯示在頁面上
      let property = node.getAttribute('model');
      if (!subjects.hasOwnProperty(property)) {
        subjects[property] = []
      }
      subjects[property].push(()=>{
        node.value = data[property]
      });
      node.value = data[property] || ''
      // 關(guān)鍵:增加監(jiān)聽器联。當(dāng)文本框變動(dòng)時(shí)觸發(fā)
      node.addEventListener('input', () => {
        /*
        * 關(guān)鍵中的關(guān)鍵: 利用代理的set攔截。
        * 相當(dāng)于在瀏覽器console中輸入data.name = 'Jim'
        */
        data[property] = node.value
      })
    }
  }
}
compile('app');

obverser(data);
function obverser(state) {
  data = new Proxy(state, {
    get (target, property) {
      return target[property]
    },
    set (target, property, newValue) {
      target[property] = newValue;
      set(property);
    }
  })
}
</script>

</body>
</html>

4.4 再進(jìn)一步婿崭,再進(jìn)一步

本文的例子很簡單拨拓,不大可能用于實(shí)踐,還有很多工作要做氓栈。

如果要寫如下的嵌套模版怎么辦渣磷?給Compile加層遞歸循環(huán)吧?

<div id ='app'>
    <p>
        姓名:
        <span value='name'> </span>
    </p>
    <p>
        年齡:
        <input type="text" model = 'age'>
    </p>
    <p>
        年齡:
        <span value='age'> </span>
    </p>
</div>

下方compile函數(shù)又丑又長怎么辦? 向Vue一樣 用watcher和dap改造吧授瘦!

...
      node.innerHTML = data[property] || ''
    } else if (node.hasAttribute('model')) {
      let property = node.getAttribute('model');
      if (!subjects.hasOwnProperty(property)) {
        subjects[property] = []
      }
      subjects[property].push(()=>{
...

有興趣閱讀《用proxy實(shí)現(xiàn)一個(gè)更優(yōu)雅Vue》

5.番外篇: 虛擬渲染

即使用代理進(jìn)行雙向綁定醋界,也需要操作DOM,而操作DOM是耗時(shí)不高效的提完。
React另辟蹊徑形纺,不用代理,使用尋渲染做UI狀態(tài)同步徒欣。
詳細(xì)原理可閱讀《如何理解虛擬DOM?》逐样,大致原理如下:

  1. 創(chuàng)建虛擬DOM,即把HTML中的模版轉(zhuǎn)為js顯示帚称。比如:
// html代碼
<ul id='list'>
  <li class='item'>Item 1</li>
  <li class='item'>Item 2</li>
  <li class='item'>Item 3</li>
</ul>

//轉(zhuǎn)為JS
var tree = h('ul', {id: 'list'}, [
  h('li', {class: 'item'}, ['Item 1']),
  h('li', {class: 'item'}, ['Item 2']),
  h('li', {class: 'item'}, ['Item 3'])
])
  1. 通過render渲染函數(shù)將虛擬DOM轉(zhuǎn)為真正的DOM并加載在頁面上
var root = tree.render() 
document.body.appendChild(root)
  1. 如果JS發(fā)生改變,直接生成新的尋DOM秽澳,比如更改Item的名稱
var newTree = h('ul', {id: 'list'}, [
  h('li', {class: 'item'}, ['A']),
  h('li', {class: 'item'}, ['B']),
  h('li', {class: 'item'}, ['C])
])
  1. 用DIff算法比較新就DOM樹闯睹,并將不同點(diǎn)存在變量pathces中
var patches = diff(tree, newTree)])
// patches 內(nèi)容類似如下:
[{node: 'li', old: 'Item 1, new 'A'} , {node: ...} ....]
  1. 在真正的DOM樹中變更
patch(root, patches)

// DOM將變?yōu)?
<ul id='list'>
  <li class='item'>A</li>
  <li class='item'>B</li>
  <li class='item'>C</li>
</ul>

至此,結(jié)束担神。

6. 結(jié)語

UI狀態(tài)同步簡史楼吃,有兩條科技線。一條是通過觀察者模式對數(shù)據(jù)的觀察妄讯,一條是虛擬函數(shù)用JS代替HTML孩锡。


科技線.png

而到今天,兩條科技線早已相互結(jié)合亥贸,互相吸取優(yōu)點(diǎn)躬窜。于是有了今天的React, Vue等。
不過故事仍沒結(jié)束炕置,在UI狀態(tài)同步的路上荣挨,優(yōu)化無止境男韧。


優(yōu)化無止境.png
感謝.png

本文源于公司的一次內(nèi)部分享

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市默垄,隨后出現(xiàn)的幾起案子此虑,更是在濱河造成了極大的恐慌,老刑警劉巖口锭,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件朦前,死亡現(xiàn)場離奇詭異,居然都是意外死亡鹃操,警方通過查閱死者的電腦和手機(jī)韭寸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來组民,“玉大人棒仍,你說我怎么就攤上這事〕羰ぃ” “怎么了莫其?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長耸三。 經(jīng)常有香客問我乱陡,道長,這世上最難降的妖魔是什么仪壮? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任憨颠,我火速辦了婚禮,結(jié)果婚禮上积锅,老公的妹妹穿的比我還像新娘爽彤。我一直安慰自己,他們只是感情好缚陷,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布适篙。 她就那樣靜靜地躺著,像睡著了一般箫爷。 火紅的嫁衣襯著肌膚如雪嚷节。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天虎锚,我揣著相機(jī)與錄音硫痰,去河邊找鬼。 笑死窜护,一個(gè)胖子當(dāng)著我的面吹牛效斑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播柱徙,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼鳍悠,長吁一口氣:“原來是場噩夢啊……” “哼税娜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起藏研,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤敬矩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蠢挡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弧岳,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年业踏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了禽炬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡勤家,死狀恐怖腹尖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情伐脖,我是刑警寧澤热幔,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站讼庇,受9級特大地震影響绎巨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蠕啄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一场勤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧歼跟,春花似錦和媳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至叹卷,卻和暖如春撼港,著一層夾襖步出監(jiān)牢的瞬間坪它,已是汗流浹背骤竹。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留往毡,地道東北人蒙揣。 一個(gè)月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像开瞭,于是被迫代替她去往敵國和親懒震。 傳聞我的和親對象是個(gè)殘疾皇子罩息,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容