在實現 VUE 中 MVVM 的系列文章的最後一篇文章中說道:我以爲可響應的數據結構做用很大,在整理了一段時間後,這是咱們的最終產出:RD - Reactive Datacss
ok 回到整理,這篇文章咱們不研究 Vue
了,而是根據咱們如今的研究成果來手擼一個 MVVM
。html
先看看下咱們的研究成果:一個例子vue
let demo = new RD({ data(){ return { text: 'Hello', firstName: 'aco', lastName: 'yang' } }, watch:{ 'text'(newValue, oldValue){ console.log(newValue) console.log(oldValue) } }, computed:{ fullName(){ return this.firstName + ' ' + this.lastName } }, method:{ testMethod(){ console.log('test') } } }) demo.text = 'Hello World' // console: Hello World // console: Hello demo.fullName // console: aco yang demo.testMethod() // console: test
寫法上與 Vue
的同樣,先說說擁有那些屬性吧:node
關於數據react
關於生命週期git
關於實例間關係程序員
實例下的方法:github
關於事件web
其餘方法npm
類下方法:
以上即是全部的內容,由於 RD
僅僅關注於數據的變化,因此生命週期就就只有建立和銷燬。
對比與 Vue
多了一個 $initProp
,一樣的因爲僅僅關注於數據變化,因此當父實例相關的 prop
發生變化時,須要手動通知子組件修改相關數據。
其餘的屬性以及方法的使用與 Vue
一致。
ok 大概說了下,具體的內容能夠點擊查看
有了 RD
咱們來手擼一個 MVVM
框架。
咱們先肯定咱們大體須要什麼?
dom
結構)ok 模板引擎,JSX
語法不錯,來一份。
接着虛擬節點,github
上搜一搜,ok 找到了,點擊查看
全部條件都具有了,咱們的實現思路以下:
RD + JSX + VNode = MVVM
具體的實現咱們一邊寫 TodoList
一邊實現
首先咱們得要有一個 render
函數,ok 配上,先來個標題組件 Title
和一個使用標題的 App
的組件吧。
能夠對照完整的 demo
查看一下內容,demo。
var App = RD.extend({ render(h) { return ( <div className='todo-wrap'> <Title/> </div> ) } }) var Title = RD.extend({ render(h) { return ( <p className='title'>{this.title}</p> ) }, data(){ return { title:'這是個標題' } } })
這裏就不說明 JSX
語法了,能夠在 babel
上看下轉碼的結果,點擊查看。
至於 render
的參數爲何是 h
?這是大部分人都承認這麼作,因此咱們這麼作就好。
根據 JSX
的語法,咱們須要實現一個建立虛擬節點的方法,也就是 render
須要傳入的參數 h
。
ok 實現一下,咱們編寫一個插件使用 RD.use
來實現對於實例的擴展
// demo/jsxPlugin/index.js export default { install(RD) { RD.prototype.$createElement = function (tag, properties, ...children) { return createElement(this, tag, properties, ...children) } RD.prototype.render = function () { return this.$option.render.call(this, this.$createElement.bind(this)) } } }
咱們把具體的處理邏輯放在 createElement
這個方法中,而實例下的 $createElement
僅僅是爲了把當前對象 this
傳入這個函數中。
接着咱們把傳入的 render
方法包裝一下,掛載到實例的 render
方法下,咱們先假設這個 createElement
能生成一個樹結構,這樣調用 實例下的 render()
,就能得到一個節點樹。
注:這裏得到的並非虛擬節點樹,節點樹須要涉及子組件,咱們要確保這個節點樹僅僅和當前實例相關,否則會比較麻煩,暫且叫它是節點模板。
ok 咱們能夠想象一下這節點模板會長什麼樣?
參考虛擬節點的庫後,獲得這樣一個結構:
{ tagName: 'div', properties: {className: 'todo-wrap'}, children:[ tagName:'component-1',// 後面的 1 是擴展出來的類的 cid ,每一個類都有一個單獨的 cid parent: App, isComponent: true, componentClass: Title properties: {}, children: [] ] }
原有標籤的處理虛擬節點的庫已經幫咱們作了,咱們來實現一下組件的節點:
// demo/jsxPulgin/createElemet.js import {h, VNode} from 'virtual-dom' export default function createElement(ctx, tag, properties, ...children) { if (typeof tag === 'function' || typeof tag === 'object') { let node = new VNode() // 構建一個空的虛擬節點,帶上組件的相關信息 node.tagName = `component-${tag.cid}` node.properties = properties // prop node.children = children // 組件的子節點,也就是 slot 這裏並無實現 node.parent = ctx // 父節點信息 node.isComponent = true // 用於判斷是不是組件 node.componentClass = tag // 組件的類 return node } return h(tag, properties, children) // 通常標籤直接調用庫提供的方法生成 }
如今咱們能夠經過實例的 render
方法獲取到了一個節點模板,但須要注意的是:這個僅僅只能算是經過 JSX
語法獲取的一個模板,並無轉換爲真正的虛擬節點,這是一個節點模板,當把其中的組件節點給替換掉就能獲得真正的虛擬節點樹。
捋一捋咱們如今有的:
render
函數render
函數生成的一個節點模板接着來實現一個方法,用於將節點模板轉化爲虛擬節點樹,具體過程看代碼中的註釋
// demo/jsxPlugin/getTree.js function extend(source, extend) { for (let key in extend) { source[key] = extend[key] } return source } function createTree(template) { // 因爲虛擬節點只接受經過 VNode 建立的對象 // 而且爲了保持模板不被污染,因此新建立一個節點 let tree = extend(new VNode(), template) if (template && template.children) { // 遍歷全部子節點 tree.children = template.children.map(node => { let treeNode = node // 若是是組件,則用保存的類實例化一個 RD 對象 if (node.isComponent) { // 肯定 parent 實例以及 初始化 prop node.component = new node.componentClass({parent: node.parent, propData: node.properties}) // 將模板對應的節點模板指向實例的節點模板,實例下的 $vnode 用於存放節點模板 // 這樣就將父組件中的組件節點替換爲組件的節點模板,而後遞歸子組件,直到全部的組件節點都轉換爲了虛擬節點 // 這裏使用了 $createComponentVNode 來獲取節點模板,下一步咱們就會實現它 treeNode = node.component.$vnode = node.component.$createComponentVNode(node.properties) // 若是是組件節點,則保存一個字段在虛擬節點下,用於區分普通節點 treeNode.component = node.component } if (treeNode.children) { // 遞歸生成虛擬節點樹 treeNode = createTree(treeNode) } if (node.isComponent) { // 將生成的虛擬節點樹保存在實例的 _vnode 字段下 node.component._vnode = treeNode } return treeNode }) } return tree }
如今的流程是 render => createElement => createTree
生成了虛擬節點,$createComponentVNode
其實就是調用組件的 render
函數,如今咱們寫一個 $patch
方法,包裝這個行爲,而且經過 $mount
實現掛載到 DOM
節點的過程。
// demo/jsxPlugin/index.js import {create, diff, patch} from 'virtual-dom' import createElement from './createElement' export default { install(RD) { RD.$mount = function (el, rd) { // 獲取節點模板 let template = rd.render.call(rd) // 初始化 prop rd.$initProp(rd.propData) // 生成虛擬節點樹 rd.$patch(template) // 掛載到傳入的 DOM 上 el.appendChild(rd.$el) } RD.prototype.$createElement = function (tag, properties, ...children) { return createElement(this, tag, properties, ...children) } RD.prototype.render = function () { return this.$option.render.call(this, this.$createElement.bind(this)) } // 對 render 的封裝,用於獲取節點模板 RD.prototype.$createComponentVNode = function (prop) { this.$initProp(prop) return this.render.call(this) } RD.prototype.$patch = function (newTemplate) { // 獲取到虛擬節點樹 let newTree = createTree(newTemplate) // 將生成 DOM 元素保存在 $el 下,create 爲虛擬節點庫提供,用於生成 DOM 元素 this.$el = create(newTree) // 保存節點模板 this.$vnode = newTemplate // 保存虛擬節點樹 this._vnode = newTree } } }
ok 接着咱們來調用一下
// demo/index.js import RD from '../src/index' import jsxPlugin from './jsxPlugin/index' import App from './component/App' import './index.scss' RD.use(jsxPlugin, RD) RD.$mount(document.getElementById('app'), App)
到目前爲止,咱們僅僅是經過了頁面的組成顯示出了一個頁面,並無實現數據的綁定,可是有了 RD
的支持,咱們能夠很簡單的實現這種由數據的變化致使視圖變化的效果,加幾段代碼便可
// demo/jsxPlugin/index.js import {create, diff, patch} from 'virtual-dom' import createElement from './createElement' import getTree from './getTree' export default { install(RD) { RD.$mount = function (el, rd) { let template = null rd.$initProp(rd.propData) // 監聽 render 所須要用的數據,當用到的數據發生變化的時候觸發回調,也就是第二個參數 // 回調的的參數新的節點模板(也就是 $watch 第一個函數參數的返回值) // 回調觸發 $patch rd.$renderWatch = rd.$watch(() => { template = rd.render.call(rd) return template }, (newTemplate) => { rd.$patch(newTemplate) }) rd.$patch(template) el.appendChild(rd.$el) } RD.prototype.$createElement = function (tag, properties, ...children) { return createElement(this, tag, properties, ...children) } RD.prototype.render = function () { return this.$option.render.call(this, this.$createElement.bind(this)) } RD.prototype.$createComponentVNode = function (prop) { let template = null this.$initProp(prop) // 監聽 render 所須要用的數據,當用到的數據發生變化的時候觸發 $patch this.$renderWatch = this.$watch(() => { template = this.render.call(this) return template }, (newTemplate) => { this.$patch(newTemplate) }) return template } RD.prototype.$patch = function (newTemplate) { // 因爲是新建立和更新都在同一個函數中處理了 // 這裏的 createTree 是須要條件判斷調用的 // 因此這裏的 getTree 就先認爲是獲取虛擬節點,以後再說 // $vnode 保存着節點模板,對於更新來講,這個就是舊模板 let newTree = getTree(newTemplate, this.$vnode) // _vnode 是原來的虛擬節點,若是沒有的話就說明是第一次建立,就不須要走 diff & patch if (!this._vnode) { this.$el = create(newTree) } else { this.$el = patch(this.$el, diff(this._vnode, newTree)) } // 更新保存的變量 this.$vnode = newTemplate this._vnode = newTree this.$initDOMBind(this.$el, newTemplate) } // 因爲組件的更新須要一個 $el ,因此 $initDOMBind 在每次 $patch 以後都須要調用,肯定子組件綁定的元素 // 這裏須要明確的是,因爲模板必須使用一個元素包裹,因此父組件的狀態改變時,父組件的 $el 是不會變的 // 須要變的僅僅是子組件的 $el 綁定,因此這個方法是向下進行的,不回去關注父組件以上的組件 RD.prototype.$initDOMBind = function (rootDom, vNodeTemplate) { if (!vNodeTemplate.children || vNodeTemplate.children.length === 0) return for (let i = 0, len = vNodeTemplate.children.length; i < len; i++) { if (vNodeTemplate.children[i].isComponent) { vNodeTemplate.children[i].component.$el = rootDom.childNodes[i] this.$initDOMBind(rootDom.childNodes[i], vNodeTemplate.children[i].component.$vnode) } else { this.$initDOMBind(rootDom.childNodes[i], vNodeTemplate.children[i]) } } } } }
ok 如今咱們大概實現了一個 MVVM
框架,缺的僅僅是 getTree
這個獲取虛擬節點樹的方法,咱們來實現一下。
首先,getTree
須要傳入兩個參數,分別是新老節點模板,因此當老模板不存在時,走原來的邏輯便可
// demo/jsxPlugin/getTree.js function deepClone(node) { if (node.type === 'VirtualNode') { let children = [] if (node.children && node.children.length !== 0) { children = node.children.map(node => deepClone(node)) } let cloneNode = new VNode(node.tagName, node.properties, children) if (node.component) cloneNode.component = node.component return cloneNode } else if (node.type === 'VirtualText') { return new VText(node.text) } } export default function getTree(newTemplate, oldTemplate) { let tree = null if (!oldTemplate) { // 走原來的邏輯 tree = createTree(newTemplate) } else { // 走更新邏輯 tree = changeTree(newTemplate, oldTemplate) } // 確保給出一份徹底新的虛擬節點樹,咱們克隆一份返回 return deepClone(tree) } // 具體的更新邏輯 function changeTree(newTemplate, oldTemplate) { let tree = extend(new VNode(), newTemplate) if (newTemplate && newTemplate.children) { // 遍歷新模板的子節點 tree.children = newTemplate.children.map((node, index) => { let treeNode = node let isNewComponent = false if (treeNode.isComponent) { // 出於性能考慮,老節點模板中相同的 RD 類,就使用它 node.component = getOldComponent(oldTemplate.children, treeNode.componentClass.cid) if (!node.component) { // 在老模板中沒有找到,就生成一個,與 createTree 中一致 node.component = new node.componentClass({parent: node.parent, propData: node.properties}) node.component.$vnode = node.component.$createComponentVNode(node.properties) treeNode = node.component.$vnode treeNode.component = node.component isNewComponent = true } else { // 更新複用組件的 prop node.component.$initProp(node.properties) // 直接引用組件的虛擬節點樹 treeNode = node.component._vnode // 保存組件的實例 treeNode.component = node.component } } if (treeNode.children && treeNode.children.length !== 0) { if (isNewComponent) { // 若是是新的節點,直接調用 createTree treeNode = createTree(treeNode) } else { // 當遞歸的時候,有時可能出現老模板沒有的狀況,好比遞歸新節點的時候 // 因此須要判斷 oldTemplate 的狀況 if (oldTemplate && oldTemplate.children) { treeNode = changeTree(treeNode, oldTemplate.children[index]) } else { treeNode = createTree(treeNode) } } } if (isNewComponent) { node.component._vnode = treeNode } return treeNode }) // 註銷在老模板中沒有被複用的組件,釋放內存 if (oldTemplate && oldTemplate.children.length !== 0) for (let i = 0, len = oldTemplate.children.length; i < len; i++) { if (oldTemplate.children[i].isComponent && !oldTemplate.children[i].used) { oldTemplate.children[i].component.$destroy() } } } return tree } // 獲取在老模板中可服用的實例 function getOldComponent(list = [], cid) { for (let i = 0, len = list.length; i < len; i++) { if (!list[i].used && list[i].isComponent && list[i].componentClass.cid === cid) { list[i].used = true return list[i].component } } }
ok 整個 MVVM
框架實現,具體的效果能夠把整個項目啦下來,執行 npm run start:demo
便可。上訴全部的代碼都在 demo
中。
咱們來統計下咱們一共寫了幾行代碼來實現這個 MVVM
的框架:
因此咱們僅僅使用了 22 + 111 + 65 = 198
行代碼實現了一個 MVVM
的框架,能夠說是不多了。
可能有的同窗會說這還不算使用 RD
和虛擬節點庫呢?是的咱們並無算上,由於這兩個庫的功能足夠的獨立,即便庫變更了,實現相應的 api
用上面的代碼咱們一樣可以實現,因此黑盒裏的代碼咱們不算。
一樣的咱們也能夠這麼說,咱們使用 198
行的代碼鏈接了 JSX/VNode/RD
實現了一個 MVVM
框架。
在研究 Vue
源碼的過程當中,在代碼裏看到了很多 SSR
和 WEEX
的判斷,我的以爲這個不必。這會致使 Vue
不論在哪段使用都會有較多的代碼冗餘。我認爲一個理想的框架應該是足夠的可配置的,至少對於開發人員來講應該如此。
因此我以爲應該想 react
那樣,在開發哪端的項目就引入相應的庫便可,而不是將代碼所有都聚合到同一個庫中。
如下我認爲是能夠作的,好比在開發 web
應用時,這樣寫
import vue from 'vue' import vue-dom from 'vue-dom' vue.use(vue-dom)
在開發 WEEX
應用時:
import vue from 'vue' import vue-dom from 'vue-weex' vue.use(vue-weex)
在開發 SSR
時:
import vue from 'vue' import vue-dom from 'vue-ssr' vue.use(vue-ssr)
固然若是說非要一套代碼統一 3
端
import vue from 'vue' import vue-dom from 'vue-dynamic-import' vue.use(vue-dynamic-import)
vue-dynamic-import
這個組件用於環境判斷,動態導入相應環境的插件。
這種想法也是我想把 RD
給獨立出來的緣由,一個模塊足夠的獨立,讓環境的判斷交給程序員來決定,由於大部分項目是僅僅須要其中的一個功能,而不須要所有的功能的。
以上,更多關於 Vue
的內容,已經關於 RD
的編寫過程,能夠到個人博客查看