日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

最近整理了下團隊新人文檔,對團隊內使用的框架 riot.js 這部分內容做了一些總結。本文主要在 riot.js 源碼 方面,分析一下 riot.js 的執行原理和使用優化。

Riot.js 簡介

Simple and elegant component-based UI library (Riot.js)

riot.js 是一個簡單優雅的 js UI框架。具有自定義標簽,簡單語法,API簡單,體積小, 學習成本低等特點。riot.js 使用Model-View-Presenter (MVP)設計模式來組織代碼, 這樣它能夠更模塊化、更具可測試性且易于理解。riot.js 僅僅提供了幫助UI渲染相關的基礎功能 ,并不具備其它復雜的功能,因此其體積很小,壓縮后僅有 10.39KB (react.min.js 大約 47.6KB ), 很適合組件類的業務開發。

Hello world

嘗試 Riot.js 最簡單的方法是使用 JSFiddle Hello Riot.js 例子。你可以在瀏覽器中打開它。 或者你也可以創建一個 .html 文件,然后通過如下方式引入Riot.js:

<script src="https://cdn.jsdelivr.net/npm/riot@3.7/riot+compiler.min.js"></scipt> 

自定義標簽

Riot.js 采用自定義標簽的語法,每個自定義標簽都可以看做是一個組件(Riot.js Tag 對象),自定義標簽將相關的 HTML 和 JAVAScript 粘合在一起,成為一個可重用的組件。可以認為它同時具有 React 和 Polymer 的優點,但是語法更友好,學習成本更小。

<riot-demo> 
 <span>{ title }</span> 
 <script>
 this.title = "Hello World";
 </script>
</riot-demo> 

在團隊中,我們會使用 webpack 來構建 riot 項目。每個組件都被寫成一個 *.tag 文件。

Riot.js 基本執行原理

一個riot自定義標簽在日常開發中從源碼到呈現在頁面上主要分為三步:編譯(一般利用官方自帶編譯工具)、注冊(riot.tag())和加載(riot.mount()),如下圖所示:

Riot.js 框架深入解析

 

編譯

編譯階段的主要工作就是將riot語法寫的.tag文件轉換為可執行的.js文件,這部分主要靠編譯器來完成。例如:

riot.tag2('content-demo', '<h1>{message}</h1>', '', function (opts) {
	this.message = 'hello world';
}); 

riot.tag2 函數在 riot.js 源碼中的 core.js 文件中,代碼如下:

export function tag2(name, tmpl, css, attrs, fn) {
 if (css) styleManager.add(css, name)
 // tags implementation cache 標簽接口緩存
 __TAG_IMPL[name] = {
 name,
 tmpl,
 attrs,
 fn
 }
 return name
}

參數含義如下:

  • name: riot 自定義標簽的名稱
  • tmpl: 標簽的html內容
  • css: 標簽中的內容
  • attrs: riot 自定義標簽的屬性
  • fn: 用戶自定義函數,即 標簽中的內容

riot.tag2() 函數將 riot tag 注冊到了 __TAG_IMP 對象中,方便之后的使用,css部分則被添加到了 byName 變量中,用于之后統一添加到頁面中。在源代碼中,還有一個 riot.tag()函數,這個函數用于直接直接創建一個 riot tag 實例的接口,而 riot.tag2() 是暴露給編輯器的接口,本質上功能是一樣的。

加載 riot.mount()

組件被注冊好以后,并沒有被渲染,直到我們調用 riot.mount() 函數后,相應的組件才會渲染到頁面上。源碼如下:

export function mount(selector, tagName, opts) {
 const tags = []
 let elem, allTags
 // root {HTMLElement} riot-tag 標簽節點
 function pushTagsTo(root) {
 if (root.tagName) {
 let riotTag = getAttr(root, IS_DIRECTIVE), // 要么 data-is 要么 root.tagName 本身
 tag
 // ① 設置 data-is 屬性指向
 if (tagName && riotTag !== tagName) {
 riotTag = tagName
 setAttr(root, IS_DIRECTIVE, tagName)
 }
 // ② mountTo 創建一個新的 riot tag 實例
 tag = mountTo(root, riotTag || root.tagName.toLowerCase(), opts)
 if (tag)
 tags.push(tag)
 } else if (root.length)
 each(root, pushTagsTo)
 }
 // DOM 注入 style 標簽
 styleManager.inject()
 if (isObject(tagName)) {
 opts = tagName
 tagName = 0
 }
 if (isString(selector)) {
 selector = selector === '*' ?
 allTags = selectTags() :
 selector + selectTags(selector.split(/, */))
 // ③ 利用 $$ 來判斷 這些 tag 是否已經掛載在 html 上面
 elem = selector ? $$(selector) : []
 } else
 elem = selector
 // 將所有元素掛載在根元素中
 if (tagName === '*') {
 tagName = allTags || selectTags()
 if (elem.tagName)
 // 查找elem下的 tagName
 elem = $$(tagName, elem)
 else {
 // 將查找到的所有節點都 放入 nodeList中
 var nodeList = []
 each(elem, _el => nodeList.push($$(tagName, _el)))
 elem = nodeList
 }
 tagName = 0
 }
 pushTagsTo(elem)
 return tags
} 

當調用 riot.mount 后,通過 selector 參數來查找 html 頁面上對應的節點。不在 html 上的節點是不會被渲染的。③處代碼為查找過程,其中$$為 document.querySelectAll。之后調用 pushTagsTo 函數來渲染 riot tag。

IS_DIRECTIVE = 'data-is' 渲染前,要檢查是否含有 tagName 參數,如果有的話即為 上述 riot.mount 的第三個用法。此時需要檢測 root 的 data-is 屬性值是否和 tagName 相等,如①處。不相等則將 root 設置其 data-is 為 tagName。

取消注冊 riot.unregister()

riot.unregister() 源碼十分簡單,如下:

export function unregister(name) {
 __TAG_IMPL[name] = null
}

Riot.js 組件

在 Riot.js 中,每個自定義標簽都可以看成是一個組件,每個組件其實本質上都是一個 Tag 對象, 里面包含了對象的各種屬性和方法

Tag 類

Tag 類簡化源代碼如下:

// impl 包含組件的模板,邏輯等屬性 
export default function Tag(impl = {}, conf = {}, innerHTML) {
 ...各種屬性初始化
 defineProperty(this, '__', {...})
 defineProperty(this, '_riot_id', ++__uid)
 defineProperty(this, 'refs', {})
 ...
 // 定義組件更新方法
 defineProperty(this, 'update', function tagUpdate(data){...}.bind(this))
 // 定義組件 mixin 方法
 defineProperty(this, 'update', function tagMixin(data){...}.bind(this))
 // 定義組件加載方法
 defineProperty(this, 'mount', function tagMount(data){...}.bind(this))
 // 定義組件卸載方法
 defineProperty(this, 'mount', function tagUnmount(data){...}.bind(this))
}

組件的生命周期

riot 組件狀態分為以下幾個部分:

  • before-mount:標簽被加載之前
  • mount:標簽實例被加載到頁面上以后
  • update:允許在更新之前重新計算上下文數據
  • updated:標簽模板更新后
  • before-unmount:標簽實例被卸載之前
  • unmount:標簽實例被從頁面上卸載后
Riot.js 框架深入解析

 

riot.js 采用事件驅動的方式來進行通訊,我們可以采用如下函數來監聽上面的事件,例如處理 update 事件:

<riot-demo>
 <script>
 this.on('update', function() {
 // 標簽更新后的處理
 })
 </script>
</riot-demo>

再談組件加載

當我們調用 riot.mount() 渲染指定組件的時候,riot 會從 __TAG_IMPL 中獲取相對應的已經注冊好的模板內容,并生成相應的 Tag 實例對象。并且觸發其上的 Tag.mount() 函數,最后將 Tag 對象緩存到 __TAGS_CACHE 中。代碼如下:

export function mountTo(root, tagName, opts, ctx) {
 var impl = __TAG_IMPL[tagName], // 獲取 html 模板
 implClass = __TAG_IMPL[tagName].class, // ?
 tag = ctx || (implClass ? Object.create(implClass.prototype) : {}),
 innerHTML = root._innerHTML = root._innerHTML || root.innerHTML
 var conf = extend({
 root: root,
 opts: opts
 }, {
 parent: opts ? opts.parent : null
 })
 if (impl && root) Tag.Apply(tag, [impl, conf, innerHTML]);
 if (tag && tag.mount) {
 tag.mount(true)
 // add this tag to the virtualDom variable
 if (!contains(__TAGS_CACHE, tag)) __TAGS_CACHE.push(tag)
 }
 return tag
}

組件加載階段,首先會整理標簽上所有的 attribute 的內容,區分普通屬性,和帶有表達式 expr 的屬性。

defineProperty(this, 'mount', function tagMount() {
 ...
 parseAttributes.apply(parent, [root, root.attributes, (attr, expr) => {
 // 檢測 expr 是否在 RefExpr 的原型鏈中
 if (!isAnonymous && RefExpr.isPrototypeOf(expr)) expr.tag = this;
 // 掛載在 root.attributs 上面 root 為組件所在的 dom 對象
 attr.expr = expr
 instAttrs.push(attr)
 }]) 
 // impl 對象包含組件上的各種屬性,包括模板,邏輯等內容
 implAttrs = []
 walkAttrs(impl.attrs, (k, v) => {
 implAttrs.push({
 name: k,
 value: v
 })
 })
 // 檢查的是 implAttrs
 parseAttributes.apply(this, [root, implAttrs, (attr, expr) => {
 if (expr) expressions.push(expr) //插入表達式
 else setAttr(root, attr.name, attr.value)
 }])
 ... 
}).bind(this)

初始化這些表達式內容,然后為組件添加全局注冊的mixin 內容。接下來,會執行我們為組件添加的函數內容,此時觸發 before-mount 事件。觸發完畢后,解析標簽上的表達式,比如 if each 等內容,然后執行組件的 update() 函數。

在 update() 函數中,首先會檢查用戶是否定義了組件的 shouldUpdate() 函數,如果有定義則傳入兩個參數,第一個是想要更新的內容(即調用this.update() 時傳入的參數)。第二個為接收的父組件更新的 opts 內容。若該函數返回值為 true 則更新渲染,否則放棄。 (這里需要注意,Tag.mount() 階段由于組件尚未處于記載完畢狀態,因此不會觸發 shouldUpdate() 函數)。

defineProperty(this, 'update', function tagUpdate(data) {
 ...
 // shouldUpdate 返回值檢測
 if (canTrigger && this.isMounted && isFunction(this.shouldUpdate) && !this.shouldUpdate(data, nextOpts)) {
 return this
 }
 ...
 // 擴展opts
 extend(opts, nextOpts)
 if (canTrigger) this.trigger('update', data)
 update.call(this, expressions)
 if (canTrigger) this.trigger('updated')
 return this
}).bind(this);

之后會觸發 update 事件,開始渲染新的組件。渲染完畢后觸發 updated 事件。

加載完畢后,修改組件狀態 defineProperty(this, 'isMounted', true)。如果渲染的組件不是作為子組件的話,我們就觸發自身的 mount 事件。否則的話,需要等到父組件加載完畢后,或者更新完畢后(已經加載過了),再觸發。

defineProperty(this, 'mount', function tagMount() {
 ...
 defineProperty(this, 'root', root)
 defineProperty(this, 'isMounted', true)
 if (skipAnonymous) return
 // 如果不是子組件則觸發
 if (!this.parent) {
 this.trigger('mount')
 }
 // 否則需要等待父組件的狀態渲染狀態
 else {
 const p = getImmediateCustomParentTag(this.parent)
 p.one(!p.isMounted ? 'mount' : 'updated', () => {
 this.trigger('mount')
 })
 }
 return this
}).bind(this) 

當我們調用 tag.unmount 卸載組件的時候,首先會觸發 before-unmount 事件。再接下來清除所有的屬性和事件監聽等內容后,觸發 ‘unmount’ 事件。

組件更新原理

在 riot.js 中,想要更新組件我們必須手動調用 tag.update() 方法才可以或者通過綁定 dom 事件觸發(通過模板綁定的事件,會在回調執行完畢后自動觸發 tag.update ),并不能做到實時的更新處理。例如:

<riot-demo>
 <h1>{ title }</h1>
 <button click={ handleClick }>修改內容</button>
 <script>
 this.title = "標題"
 handleClick() {
 this.title = "新標題";
 this.update(); // 調用 update 方法才能重新渲染組件
 }
 </script>
</riot-demo>

riot.js 并沒有提供 virtual dom 的功能,而是實現了一個粗粒度的 virtual dom。riot.js 為每個組件創建的 tag 對象中都保存一個 expressions 數組,更新的時候遍歷 expressions 數組,對比舊值,如果有變化就更新DOM。這種更新機制類似angular的臟檢查,但是僅有一輪檢查(單項數據流)。更新處理依照模板類型來處理:

  • 文本內容的,直接: dom.nodeValue = value
  • 值為空,而且關聯的 DOM 屬性是 checked/selected 等這種沒有屬性值的,移除對應的屬性
  • 值為函數的,則進行事件綁定
  • 屬性名為 if,則做條件判斷處理
  • 做了 show/hide 的語法糖處理
export function toggleVisibility(dom, show) {
 dom.style.display = show ? '' : 'none'
 dom['hidden'] = show ? false : true
}
  • 普通屬性的,直接設置其值

riot.js 和 react 一樣也有 props(靜態,riot 中為 opts) 和本身數據(動態),具有和 react 一樣的輸入。但是輸出的時候,由于沒有 virtual dom UI的更新并沒有集中處理,是分散的。

riot.js 采用的這種方式,代碼量上大大的減少,但是也帶來了比較嚴重的性能問題。

更新性能問題

首先我們來看一段 vue 代碼:

<div id="demo">
 <ul>
 <li v-for="item in items">
 {{ item.name }} --- {{ item.age }}
 </li>
 </ul>
 <button v-on:click="handleClick">更新列表項</button>
</div> 
var demo = new Vue({
 el: '#demo',
 data: {
 items: [
 { name: 'tgy', age: 23},
 ]
 },
 methods: {
 handleClick: function() {
 this.items = [
 { name: 'tgy', age: 23},
 { name: 'hy', age: 22},
 ]
 }
 },
 mounted: function() {
 console.log("組件掛載完畢");
 document.querySelector("li").extraType = "origin";
 },
 updated: function() {
 console.log("組件更新完畢");
 console.log(document.querySelector("li").extraType);
 }
}) 

代碼很簡單,單擊按鈕,為列表添加一條新數據。在組件掛載完畢后,為第一個 li 的 property 上面添加了 extraType 屬性。列表更新后,再去訪問這個 li 的 extraType 屬性。運行結果如下:

Riot.js 框架深入解析

 

不出意料,可以正常訪問到 li 的type屬性。這說明了,在更新過程中,第一個 li 節點僅僅是 textContent 發生了改變而不是重新創建的。這樣的結果得益于 virtual dom 算法,保證更新最小變動。同樣的我們用 riot 來重寫上面的代碼。

<content-demo>
 <ul>
 <li each={ items }>{ name } -- { age }</li>
 </ul>
 <button class="btn" click={ handleClick }>訂閱內容</button> 
 <script>
 let self = this;
 this.items = [
 {"name": "tgy", age: 23}
 ];
 handleClick() {
 this.items = [
 {"name": "tgy", age: 23},
 {"name": "hy", age: 22}
 ]
 }
 this.on('mount', function() {
 console.log("組件加載完畢");
 document.querySelector("li").extraType = "origin";
 })
 this.on('updated', function() {
 console.log("組件更新完畢");
 console.log(document.querySelector("li").extraType);
 })
 </script>
</content-demo>

查看運行結果:

Riot.js 框架深入解析

 

extraType 找不到了,所有的 li 節點都被重新構建了。這里面發生了什么,查看源碼 /tag/each.js。渲染邏輯代碼如下:

export default function _each(dom, parent, expr) {
 ...
 expr.update = function updateEach() {
 ...
 each(items, function (item, i) {
 // 僅僅記錄 items 是對象的
 var
 doReorder = mustReorder && typeof item === T_OBJECT && !hasKeys,
 // 舊數據
 oldPos = oldItems.indexOf(item),
 // 是新的
 isNew = oldPos === -1,
 pos = !isNew && doReorder ? oldPos : i,
 tag = tags[pos],
 // 必須追加
 mustAppend = i >= oldItems.length,
 // 必須創建 isNew
 mustCreate = doReorder && isNew || !doReorder && !tag
 // 有key值得時候需要 mkitem
 item = !hasKeys && expr.key ? mkitem(expr, item, i) : item
 // 必須創建一個新 tag 
 if (mustCreate) {
 tag = new Tag(impl, {
 parent,
 isLoop,
 isAnonymous,
 tagName,
 root: dom.cloneNode(isAnonymous),
 item,
 index: i,
 }, dom.innerHTML)
 // mount the tag
 tag.mount()
 if (mustAppend)
 append.apply(tag, [frag || root, isVirtual])
 else
 insert.apply(tag, [root, tags[i], isVirtual])
 if (!mustAppend) oldItems.splice(i, 0, item)
 tags.splice(i, 0, tag)
 if (child) arrayishAdd(parent.tags, tagName, tag, true)
 } else if (pos !== i && doReorder) {
 // move
 // 移動
 if (contains(items, oldItems[pos])) {
 move.apply(tag, [root, tags[i], isVirtual])
 // move the old tag instance
 tags.splice(i, 0, tags.splice(pos, 1)[0])
 // move the old item
 oldItems.splice(i, 0, oldItems.splice(pos, 1)[0])
 }
 if (expr.pos) tag[expr.pos] = i
 if (!child && tag.tags) moveNestedTags.call(tag, i)
 }
 // 緩存原始數據到節點上
 tag.__.item = item
 tag.__.index = i
 tag.__.parent = parent;
 // 如果不是創建的,我們需要更新節點內容。
 if (!mustCreate) tag.update(item)
 })
 // remove the redundant tags
 // 刪除多余的標簽
 unmountRedundant(items, tags)
 // 記錄舊的數據
 // clone the items array
 oldItems = items.slice()
 // dom 插入節點
 root.insertBefore(frag, placeholder)
 }
} 

這段為列表渲染邏輯,遍歷新的數據items中的每一下 item。在原始數據 oldItems 中去查找(oldItems.indexOf(itemId)),是否存在 item 項。如果不存在,則標記 isNews 為 true。之后走到 if 的 mustCreaete 為 true 的分支,去創建一個新的 tag(將 li 節點看成是一個tag)。以此類推,當全部創建完畢后,刪除舊的節點(unmountRedundant(items, tags))。在斷點下,可以清楚看到節點的變化情況:

Riot.js 框架深入解析

 

優化更新

綜上所述,riot.js 的更新邏輯僅僅是判斷新舊數據項是否為同一對象。為此,為了減少 DOM 的變動,降低渲染邏輯。我們修改handleClick函數:

handleClick() {
 this.items.push({"name": "hy", age: 22})
} 

這樣輸出結果就會和 vue 的保持一致,并沒有創建新的 tag,而是利用了已經存在的內容。源碼中,這種情況下 isNews 為 false,從而避開了 創建標簽。而僅僅是通過 tags.splice(i, 0, tags.splice(pos, 1)[0]); 來移動位置,if (!mustCreate) { tag.update(item); } 更新節點內容。

保證數據項對象地址不變,僅僅是修改上面的不可變對象的值,將大大的提高 riot.js 的渲染效率。

// 更新第一個li內容 
// 不推薦寫法,對象發生變化;
this.items[0] = {"name": "hy", age: 23}; 
// 推薦寫法,僅僅是修改對象中的值
this.items[0].name = "hy";
this.items[0].age = 22;

保證源數據對象的不變,僅僅改變其上面的值,這樣就能減少 riot.js 渲染過程中,創建新的 tag 對象的開銷。

希望本文能幫助到您!

點贊+轉發,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓-_-)

關注 {我},享受文章首發體驗!

每周重點攻克一個前端技術難點。更多精彩前端內容私信 我 回復“教程”

原文鏈接:http://eux.baidu.com/blog/fe/riot-js-%E6%A1%86%E6%9E%B6%E6%B7%B1%E5%85%A5%E8%A7%A3%E6%9E%90

作者:田光宇

分享到:
標簽:Riot js
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定