【一套代碼小程序&Native&Web階段總結篇】能夠這樣閱讀Vue源碼

前言

前面咱們對微信小程序進行了研究:【微信小程序項目實踐總結】30分鐘從陌生到熟悉javascript

在實際代碼過程當中咱們發現,咱們可能又要作H5站又要作小程序同時還要作個APP,這裏會形成很大的資源浪費,若是設定一個規則,讓咱們能夠先寫H5代碼,而後將小程序以及APP的業務差別代碼作掉,豈不快哉?但小程序的web框架並不開源,否則也用不着咱們在此費力了,通過研究,小程序web端框架是一套自研的MVVM框架,因而咱們立刻想到了藉助第三方框架:html

一套代碼小程序&Web&Native運行的探索01vue

通過簡單的研究,咱們發現不管React或者Vue都是能夠必定程度適應小程序開發模式的,市面上也有了對應的框架:mpvue&wepyjava

在使用以上任何一套框架體系前,咱們都須要對MVVM框架有最基礎的瞭解,否則後面隨便遇到點什麼問題可能都會變得難以繼續,負責人要對團隊負責而不是單純只是想技術嚐鮮,因此生產項目必定要使用本身能完成hold住的技術node

這段時間咱們嘗試着去閱讀Vue的源碼,但如今的Vue是一個工程化產物,咱們要學習的核心代碼可能不到其中的1/3,多出來的是各類特性以及細節處理,在不清楚代碼意圖(目標)的狀況下,事實上很難搞懂這段代碼是要幹什麼,不少代碼的出現都是一些精妙的小優化,知道的就會以爲十分驚豔,不知道的就會一頭霧水,一來就看Vue的源碼反而不利於深刻了解webpack

出於這個緣由在網上看了不少源碼介紹的文章,出來就放一個Vue官方的流程圖,而後一套組合模塊套路,基本就把我給打暈了,查詢了不少資料,仍是發現一些寫的比較清晰的(我感受適合多數人的)文章:git

讀懂源碼:一步一步實現一個 Vuegithub

https://github.com/fastCreator/MVVM(特別推薦,很是不錯)web

這兩篇文章都有一個主旨:canvas

在沒有相關框架經驗的狀況下,單單靠單步調試以及網上的源碼介紹,想要讀懂Vue源碼是不太靠譜的作法,比較好的作法是本身照着Vue的源碼寫一套簡單的,最基礎的MVVM框架,在完成這個框架後再去閱讀Vue或者React的代碼要輕易的多,我這邊是很是承認這個說法的,因此咱們照着fastCreateor的代碼(他應該是參考的Vue)也擼了一個:https://github.com/yexiaochai/wxdemo/tree/master/mvvm

這裏與其說擼了一個MVVM框架,不如說給fastCreateor的代碼加上了自我理解的註釋,經過這個過程也對MVVM框架有了第一步的認識,以前的文章或者代碼都有些散,這裏將前面學習的內容再作一次彙總,多加一些圖示,幫助本身也幫助讀者更好的瞭解,因而讓咱們開始吧!

PS:下面說的MVVM框架,基本就是Vue框架,而且是本身的理解,有問題請你們拍磚

MVVM框架的流程

咱們梳理了MVVM框架的基本流程,這裏只看首次渲染的話:

① 解析html模板造成mvvm實例對象element(實例上的$node屬性)
② 處理element屬性,這裏包括屬性處理、事件處理、指令處理
③ 使用處理過的element對象,爲每一個實例建立render方法
PS:new MVVM只會產生一個實例,每一個html標籤都會造成一個vnode,組件會造成獨立的實例,與根實例以$parent與$children維護關係
④ 使用render方法建立虛擬dom vnode,vm實例element已經具有全部建立虛擬dom的必要條件,render只是利用他們,若是代碼組織得好,不使用render也行
⑤ render執行後會生成虛擬dom vnode,藉助另外一個神器snabbdom開始對比新舊虛擬dom的結構,完成最終渲染
PS:render執行時做用域在mvvm實例(vm)下

因此整個代碼核心所有是圍繞着HTML=>element($node中間項,橋樑)=>render函數(執行返回vnode)=>引用snabbdom patch渲染

而抓住幾個點後,對應的幾個核心技術點也就出來了:

① 模板解析這裏對應着 HTMLParser,幫忙解決了不少問題
② 造成vnode須要的render函數,而且調用後維護彼此關係,這個是框架作的最多的工做
③ 生成真正的vnode,而後執行對比差別渲染patch操做,這塊重要的工做由snabbdom接手了

咱們再把這裏的目標映射成過程,就獲得了這張圖了(來自https://github.com/fastCreator/MVVM):

上面一行就是首次渲染執行的流程,下面幾個圖就是實現數據變化時候更新試圖的操做,分解到程序層面,核心就是:

① 實例化

② Parser => HTMLParser

③ codegen

在此基礎上再包裝出數據響應模型以及組件系統、指令系統,每一個模塊都很獨立,但又互相關聯,抓住這個主幹看各個分支這樣就會相對比較清晰。因此網上不少幾百行代碼實現MVVM框架核心的就是隻作最核心這一塊,好比這個學習材料:https://github.com/DMQ/mvvm,很是簡單清晰,爲了幫助更好的理解,咱們這裏也寫了一段比較獨立的代碼,包括了核心流程:

  1 <div id="app">
  2   <input type="text" v-model="name">
  3   {{name}}
  4 </div>
  5 
  6 <script type="text/javascript" >
  7 
  8   function getElById(id) {
  9     return document.getElementById(id);
 10   }
 11 
 12   //主體對象,存儲全部的訂閱者
 13   function Dep () {
 14     this.subs = [];
 15   }
 16 
 17   //通知全部訂閱者數據變化
 18   Dep.prototype.notify = function () {
 19     for(let i = 0, l = this.subs.length; i < l; i++) {
 20       this.subs[i].update();
 21     }
 22   }
 23 
 24   //添加訂閱者
 25   Dep.prototype.addSub = function (sub) {
 26     this.subs.push(sub);
 27   }
 28 
 29   let globalDataDep = new Dep();
 30 
 31   //觀察者,框架會接觸data的每個與node相關的屬性,
 32   //若是data沒有與任何節點產生關聯,則不予理睬
 33   //實際的訂閱者對象
 34   //注意,只要一個數據對象對應了一個node對象就會生成一個訂閱者,因此真實通知的時候應該須要作到通知到對應數據的dom,這裏不予關注
 35   function Watcher(vm, node, name) {
 36     this.name = name;
 37     this.node = node;
 38     this.vm = vm;
 39     if(node.nodeType === 1) {
 40       this.node.value = this.vm.data[name];
 41     } else if(node.nodeType === 3) {
 42       this.node.nodeValue = this.vm.data[name] || '';
 43     }
 44     globalDataDep.addSub(this);
 45 
 46   }
 47 
 48   Watcher.prototype.update = function () {
 49     if(this.node.nodeType === 1) {
 50       this.node.value = this.vm.data[this.name ];
 51     } else if(this.node.nodeType === 3) {
 52       this.node.nodeValue = this.vm.data[this.name ] || '';
 53     }
 54   }
 55 
 56   //這塊代碼僅作功能說明,不用當真
 57   function compile(node, vm) {
 58     let reg = /\{\{(.*)\}\}/;
 59 
 60     //節點類型
 61     if(node.nodeType === 1) {
 62       let attrs = node.attributes;
 63       //解析屬性
 64       for(let i = 0, l = attrs.length; i < l; i++) {
 65         if(attrs[i].nodeName === 'v-model') {
 66           let name = attrs[i].nodeValue;
 67           if(node.value === vm.data[name]) break;
 68 
 69 //          node.value = vm.data[name] || '';
 70           new Watcher(vm, node, name)
 71 
 72           //此處不作太多判斷,直接綁定事件
 73           node.addEventListener('input', function (e) {
 74             //賦值操做
 75             let newObj = {};
 76             newObj[name] = e.target.value;
 77             vm.setData(newObj, true);
 78           });
 79 
 80           break;
 81         }
 82       }
 83     } else if(node.nodeType === 3) {
 84 
 85       if(reg.test(node.nodeValue)) {
 86         let name = RegExp.$1; // 獲取匹配到的name
 87         name = name.trim();
 88 //          node.nodeValue = vm.data[name] || '';
 89         new Watcher(vm, node, name)
 90       }
 91     }
 92   }
 93 
 94   //獲取節點
 95   function nodeToFragment(node, vm) {
 96     let flag = document.createDocumentFragment();
 97     let child;
 98 
 99     while (child = node.firstChild) {
100       compile(child, vm);
101       flag.appendChild(child);
102     }
103 
104     return flag;
105   }
106 
107   function MVVM(options) {
108     this.data = options.data;
109     let el = getElById(options.el);
110     this.$dom = nodeToFragment(el, this)
111     this.$el = el.appendChild(this.$dom);
112 
113 //    this.$bindEvent();
114   }
115 
116   MVVM.prototype.setData = function (data, noNotify) {
117     for(let k in data) {
118       this.data[k] = data[k];
119     }
120     //執行更新邏輯
121 //    if(noNotify) return;
122     globalDataDep.notify();
123   }
124 
125   let mvvm = new MVVM({
126     el: 'app',
127     data: {
128       name: '葉小釵'
129     }
130   })
131 
132   setTimeout(function() {
133     mvvm.setData({name: '刀狂劍癡葉小釵'})
134   }, 3000)
135 
136 </script>
最簡單的mvvm例子

你們對照着這個例子本身擼一下,其中有幾個在業務中不太經常使用的知識點,第一個就是訪問器屬性,這裏大概寫個例子介紹下:

var obj = { };
// 爲obj定義一個名爲 name 的訪問器屬性
Object.defineProperty(obj, "name", {

  get: function () {
    console.log('get', arguments);
  },
  set: function (val) {
    console.log('set', arguments);
  }
})
obj.name = '葉小釵'
console.log(obj, obj.name)
/*
 set Arguments ["葉小釵", callee: ƒ, Symbol(Symbol.iterator): ƒ]
 get Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]
*/

接下來咱們對MVVM框架會用到的兩大神器依次作下介紹

神器HTMLPaser

HTMLParser這個庫的代碼在這裏能夠拿到:https://github.com/yexiaochai/wxdemo/blob/master/mvvm/libs/html-parser.js

這個庫完成的功能比較簡單,就是解析你傳入的html模板,這裏舉個例子:

  1 <!doctype html>
  2 <html>
  3 <head>
  4   <title>起步</title>
  5 </head>
  6 <body>
  7 
  8 <div id="app">
  9 
 10 </div>
 11 <script  >
 12 
 13   // Regular Expressions for parsing tags and attributes
 14   let startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:@][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/,
 15           endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/,
 16           attr = /([a-zA-Z_:@][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g
 17 
 18   // Empty Elements - HTML 5
 19   let empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr")
 20 
 21   // Block Elements - HTML 5
 22   let block = makeMap("a,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video")
 23 
 24   // Inline Elements - HTML 5
 25   let inline = makeMap("abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var")
 26 
 27   // Elements that you can, intentionally, leave open
 28   // (and which close themselves)
 29   let closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr")
 30 
 31   // Attributes that have their values filled in disabled="disabled"
 32   let fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected")
 33 
 34   // Special Elements (can contain anything)
 35   let special = makeMap("script,style")
 36 
 37   function makeMap(str) {
 38     var obj = {}, items = str.split(",");
 39     for (var i = 0; i < items.length; i++)
 40       obj[items[i]] = true;
 41     return obj;
 42   }
 43 
 44   function HTMLParser(html, handler) {
 45     var index, chars, match, stack = [], last = html;
 46     stack.last = function () {
 47       return this[this.length - 1];
 48     };
 49 
 50     while (html) {
 51       chars = true;
 52 
 53       // Make sure we're not in a script or style element
 54       if (!stack.last() || !special[stack.last()]) {
 55 
 56         // Comment
 57         if (html.indexOf("<!--") == 0) {
 58           index = html.indexOf("-->");
 59 
 60           if (index >= 0) {
 61             if (handler.comment)
 62               handler.comment(html.substring(4, index));
 63             html = html.substring(index + 3);
 64             chars = false;
 65           }
 66 
 67           // end tag
 68         } else if (html.indexOf("</") == 0) {
 69           match = html.match(endTag);
 70 
 71           if (match) {
 72             html = html.substring(match[0].length);
 73             match[0].replace(endTag, parseEndTag);
 74             chars = false;
 75           }
 76 
 77           // start tag
 78         } else if (html.indexOf("<") == 0) {
 79           match = html.match(startTag);
 80 
 81           if (match) {
 82             html = html.substring(match[0].length);
 83             match[0].replace(startTag, parseStartTag);
 84             chars = false;
 85           }
 86         }
 87 
 88         if (chars) {
 89           index = html.indexOf("<");
 90 
 91           var text = index < 0 ? html : html.substring(0, index);
 92           html = index < 0 ? "" : html.substring(index);
 93 
 94           if (handler.chars)
 95             handler.chars(text);
 96         }
 97 
 98       } else {
 99         html = html.replace(new RegExp("([\\s\\S]*?)<\/" + stack.last() + "[^>]*>"), function (all, text) {
100           text = text.replace(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/g, "$1$2");
101           if (handler.chars)
102             handler.chars(text);
103 
104           return "";
105         });
106 
107         parseEndTag("", stack.last());
108       }
109 
110       if (html == last)
111         throw "Parse Error: " + html;
112       last = html;
113     }
114 
115     // Clean up any remaining tags
116     parseEndTag();
117 
118     function parseStartTag(tag, tagName, rest, unary) {
119       tagName = tagName.toLowerCase();
120 
121       if (block[tagName]) {
122         while (stack.last() && inline[stack.last()]) {
123           parseEndTag("", stack.last());
124         }
125       }
126 
127       if (closeSelf[tagName] && stack.last() == tagName) {
128         parseEndTag("", tagName);
129       }
130 
131       unary = empty[tagName] || !!unary;
132 
133       if (!unary)
134         stack.push(tagName);
135 
136       if (handler.start) {
137         var attrs = [];
138 
139         rest.replace(attr, function (match, name) {
140           var value = arguments[2] ? arguments[2] :
141                   arguments[3] ? arguments[3] :
142                           arguments[4] ? arguments[4] :
143                                   fillAttrs[name] ? name : "";
144 
145           attrs.push({
146             name: name,
147             value: value,
148             escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //"
149           });
150         });
151 
152         if (handler.start)
153           handler.start(tagName, attrs, unary);
154       }
155     }
156 
157     function parseEndTag(tag, tagName) {
158       // If no tag name is provided, clean shop
159       if (!tagName)
160         var pos = 0;
161 
162       // Find the closest opened tag of the same type
163       else
164         for (var pos = stack.length - 1; pos >= 0; pos--)
165           if (stack[pos] == tagName)
166             break;
167 
168       if (pos >= 0) {
169         // Close all the open elements, up the stack
170         for (var i = stack.length - 1; i >= pos; i--)
171           if (handler.end)
172             handler.end(stack[i]);
173 
174         // Remove the open elements from the stack
175         stack.length = pos;
176       }
177     }
178   };
179 
180     html = `
181 <div id="s_wrap" class="s-isindex-wrap">
182   <div id="s_main" class="main clearfix">
183     <div id="s_mancard_main" class="s-mancacrd-main">
184       <div class="s-menu-container">
185         <div id="s_menu_gurd" class="s-menu-gurd">
186           <div id="s_ctner_menus" class="s-ctner-menus s-opacity-blank8">
187             <span id="s_menu_mine" class="s-menu-item s-menu-mine s-opacity-white-background current" data-id="100">
188               <div class="mine-icon"></div>
189               <div class="mine-text">個人關注</div>
190             </span>
191             <div class="s-menus-outer">
192               <div id="s_menus_wrapper" class="menus-wrapper"></div>
193               <div class="s-bg-space s-opacity-white-background"></div>
194               <span class="s-menu-music" data-id="3"></span>
195             </div>
196             <span id="s_menu_set" class="s-menu-setting s-opacity-white-background" data-id="99" title="設置">
197               <div class="menu-icon"></div>
198             </span>
199           </div>
200         </div>
201       </div>
202     </div>
203   </div>
204 </div>
205 `
206 
207 HTMLParser(html,{
208   start: function(tag, attrs, unary) {
209     console.log('標籤頭', tag, attrs)
210   },
211   end: function (tag) {
212     console.log('標籤尾', tag)
213   },
214   //處理真實的節點
215   chars: function(text) {
216     console.log('標籤字段', text.trim().length > 0 ? text : '空字符' )
217   }
218 })
219 
220 </script>
221 
222 
223 </body>
224 </html>
HTMLParser的簡單例子
 1 html = `
 2 <div id="s_wrap" class="s-isindex-wrap">
 3   <div id="s_main" class="main clearfix">
 4     <div id="s_mancard_main" class="s-mancacrd-main">
 5       <div class="s-menu-container">
 6         <div id="s_menu_gurd" class="s-menu-gurd">
 7           <div id="s_ctner_menus" class="s-ctner-menus s-opacity-blank8">
 8             <span id="s_menu_mine" class="s-menu-item s-menu-mine s-opacity-white-background current" data-id="100">
 9               <div class="mine-icon"></div>
10               <div class="mine-text">個人關注</div>
11             </span>
12             <div class="s-menus-outer">
13               <div id="s_menus_wrapper" class="menus-wrapper"></div>
14               <div class="s-bg-space s-opacity-white-background"></div>
15               <span class="s-menu-music" data-id="3"></span>
16             </div>
17             <span id="s_menu_set" class="s-menu-setting s-opacity-white-background" data-id="99" title="設置">
18               <div class="menu-icon"></div>
19             </span>
20           </div>
21         </div>
22       </div>
23     </div>
24   </div>
25 </div>
26 `
27 
28 HTMLParser(html,{
29   start: function(tag, attrs, unary) {
30     console.log('標籤頭', tag, attrs)
31   },
32   end: function (tag) {
33     console.log('標籤尾', tag)
34   },
35   //處理真實的節點
36   chars: function(text) {
37     console.log('標籤字段', text.trim().length > 0 ? text : '空字符' )
38   }
39 })

這裏使用HTMLParser,很容易就能夠把html模板解析爲element樹

神器Snabbdom

咱們很容易就能夠將一根dom結構用js對象來抽象,好比咱們這裏的班次列表排序:

這裏出發的因子就有出發時間、耗時、價格,這裏表示下就是:

let trainData = {
  sortKet: 'time', //耗時,價格,發車時間等等方式排序
  sortType: 1, //1升序,2倒敘
  oData: [], //服務器給過來的原生數據
  data: [], //當前篩選條件下的數據
}

這個對象有個缺陷就是不能與頁面映射起來,咱們須要在代碼中維護數據與試圖的映射關係(data與dom的關係),一旦數據發生變化便從新渲染。比較複雜的問題是半年後這個頁面的維護者三易其手,而篩選條件增長、業務邏輯變化,這個頁面的代碼可能會變得至關難維護,其中最難的點可能就是頁面中的dom關係和事件維護

而咱們想要的就是數據改變了,DOM本身就發生變化,而且以高效的方式發生變化,這個就是咱們snabbdom作的工做了,咱們用一段代碼說明這個問題:

var element = {
  tagName: 'ul', // 節點標籤名
  props: { // DOM的屬性,用一個對象存儲鍵值對
    id: 'list'
  },
  children: [ // 該節點的子節點
    {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
    {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
  ]
}

這個映射成dom結構就是:

1 <ul id='list'>
2   <li class='item'>Item 1</li>
3   <li class='item'>Item 2</li>
4   <li class='item'>Item 3</li>
5 </ul>

真實的VNode會翻譯爲這樣:

class Element {
  constructor(tagName, props, children) {
    this.tagName = tagName;
    this.props = props;
    this.children = children;
  }
}

function el(tagName, props, children)  {
  return new Element(tagName, props, children)
}

el('ul', {id: 'list'}, [
  el('li', {class: 'item'}, ['Item 1']),
  el('li', {class: 'item'}, ['Item 2']),
  el('li', {class: 'item'}, ['Item 3'])
])

這裏很快就能封裝一個可運行的代碼出來:

//***虛擬dom部分代碼,後續會換成snabdom
class Element {
  constructor(tagName, props, children) {
    this.tagName = tagName;
    this.props = props;
    this.children = children;
  }
  render() {
    //拿着根節點往下面擼
    let root = document.createElement(this.tagName);
    let props = this.props;

    for(let name in props) {
      root.setAttribute(name, props[name]);
    }

    let children = this.children;

    for(let i = 0, l = children.length; i < l; i++) {
      let child = children[i];
      let childEl;
      if(child instanceof Element) {
        //遞歸調用
        childEl = child.render();
      } else {
        childEl = document.createTextNode(child);
      }
      root.append(childEl);
    }

    this.rootNode = root;
    return root;
  }
}

function el(tagName, props, children)  {
  return new Element(tagName, props, children)
}

let vnode = el('ul', {id: 'list'}, [
  el('li', {class: 'item'}, ['Item 1']),
  el('li', {class: 'item'}, ['Item 2']),
  el('li', {class: 'item'}, ['Item 3'])
])

let root = vnode.render();

document.body.appendChild(root);

snabbdom作的事情,即是把這段代碼寫的更加完善一點,而且處理裏面最爲複雜的比較兩顆虛擬樹的差別了,而這塊也是snabbdom的核心,固然也比較有難度啦,咱們這裏能用就行便不深刻了,這裏來一段代碼說明下snabbdom的使用:

var snabbdom = require("snabbdom");
var patch = snabbdom.init([ // 初始化補丁功能與選定的模塊
  require("snabbdom/modules/class").default, // 使切換class變得容易
  require("snabbdom/modules/props").default, // 用於設置DOM元素的屬性(注意區分props,attrs具體看snabbdom文檔)
  require("snabbdom/modules/style").default, // 處理元素的style,支持動畫
  require("snabbdom/modules/eventlisteners").default, // 事件監聽器
]);
//h是一個生成vnode的包裝函數,factory模式?對生成vnode更精細的包裝就是使用jsx
//在工程裏,咱們一般使用webpack或者browserify對jsx編譯
var h = require("snabbdom/h").default; // 用於建立vnode,VUE中render(createElement)的原形

var container = document.getElementById("container");

var vnode = h("div#container.two.classes", {on: {click: someFn}}, [
  h("span", {style: {fontWeight: "bold"}}, "This is bold"),
  " and this is just normal text",
  h("a", {props: {href: "/foo"}}, "I\"ll take you places!")
]);
// 第一次打補丁,用於渲染到頁面,內部會創建關聯關係,減小了建立oldvnode過程
patch(container, vnode);
//建立新節點
var newVnode = h("div#container.two.classes", {on: {click: anotherEventHandler}}, [
  h("span", {style: {fontWeight: "normal", fontStyle: "italic"}}, "This is now italic type"),
  " and this is still just normal text",
  h("a", {props: {href: "/bar"}}, "I\"ll take you places!")
]);
//第二次比較,上一次vnode比較,打補丁到頁面
//VUE的patch在nextTick中,開啓異步隊列,刪除了沒必要要的patch
//nextTick異步隊列解析,下面文章中會詳解
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state

繼續來一段例子作說明:

<div id="container">
</div>

<script type="module">
  "use strict";
  import { patch, h, VNode } from './libs/vnode.js'
  var container = document.getElementById("container");
  function someFn(){ console.log(1)}
  function anotherEventHandler(){ console.log(2)}

  var oldVnode = h("div", {on: {click: someFn}}, [
    h("span", {style: {fontWeight: "bold"}}, "This is bold"),
    " and this is just normal text",
    h("a", {props: {href: "/foo"}}, "I\"ll take you places!")
  ]);

  // 第一次打補丁,用於渲染到頁面,內部會創建關聯關係,減小了建立oldvnode過程
  let diff = patch(container, oldVnode);
  //建立新節點
  var newVnode = h("div", {on: {click: anotherEventHandler}}, [
    h("span", {style: {fontWeight: "normal", fontStyle: "italic"}}, "This is now italic type"),
    " and this is still just normal text",
    h("a", {props: {href: "/bar"}}, "I\"ll take you places!")
  ]);
  //第二次比較,上一次vnode比較,打補丁到頁面
  //VUE的patch在nextTick中,開啓異步隊列,刪除了沒必要要的patch
  //nextTick異步隊列解析,下面文章中會詳解
  patch(oldVnode, newVnode); // Snabbdom efficiently updates the old view to the new state
  function test() {
    return {
      oldVnode,newVnode,container,diff
    }
  }
</script>

snabbdom在組件系統中的應用

MVVM系統還有個比較關鍵的是組件系統,通常認爲MVVM的兩大特色實際上是響應式數據更新(VNode相關),而後就是組件體系,這二者須要完成的工做都是讓咱們更高效的開發代碼,一個爲了解決紛亂的dom操做,一個爲了解決負責的業務邏輯結構,而組件體系便會用到snabbdom中的hook:

//建立組件
//子組件option,屬性,子元素,tag
_createComponent(Ctor, data, children, sel) {
 Ctor.data = mergeOptions(Ctor.data);
 let componentVm;
 let Factory = this.constructor
 let parentData = this.$data
 data.hook.insert = (vnode) => {
    //...
 }
 Ctor._vnode = new VNode(sel,null,data, [], undefined, createElement(sel));
 return Ctor._vnode
}

使用通常流程,咱們不會解析這個組件而是插入沒有意義的標籤:

<my-component></my-component>
<div m-for="(val, key, index) in arr">索引 1 :葉小釵</div>
<div m-for="(val, key, index) in arr">索引 2 :素還真</div>
<div m-for="(val, key, index) in arr">索引 3 :一頁書</div>
 1 _createComponent(Ctor, data, children, sel) {
 2  Ctor.data = mergeOptions(Ctor.data);
 3  let componentVm;
 4  let Factory = this.constructor
 5  let parentData = this.$data
 6  data.hook.insert = (vnode) => {  7    Ctor.data = Ctor.data || {};  8    var el =createElement('sel')  9  vnode.elm.append(el) 10    Ctor.el = el; 11    componentVm = new Factory(Ctor); 12    vnode.key = componentVm.uid; 13    componentVm._isComponent = true
14    componentVm.$parent = this; 15    (this.$children || (this.$children = [])).push(componentVm); 16    //寫在調用父組件值
17    for (let key in data.attrs) { 18      if (Ctor.data[key]) { 19  warn(`data:${key},已存在`); 20        continue; 21  } 22  } 23  } 24  Ctor._vnode = new VNode(sel,null,data, [], undefined, createElement(sel));
25  return Ctor._vnode
26 }

可是咱們爲snabbdom設置了一個hook(鉤子),當標籤被插入的時候會執行這段邏輯(加粗部分代碼),這裏先建立了一個空標籤(sel)直接插入my-component中,而後執行與以前同樣的實例化流程:

componentVm = new Factory(Ctor);

這個會在patch後將實際的dom節點更新上去:

this.$el = patch(this.$el, vnode); //$el如今爲sel標籤(dom標籤)

這個就是snabbdom hook所幹的工做,同時能夠看到組件系統這裏有這些特色:

① 組件是一個獨立的mvvm實例,經過parent能夠找到其父親mvvm實例,可能跟實例,也多是另外一個組件

② 根實例能夠根據$children參數找到其下面全部的組件

③ 組件與跟實例經過data作交流,原則不容許在組件內部改變屬性值,須要使用事件進行通訊,事件通訊就是在組件中的點擊事件不作具體的工做,而是釋放$emit(),這種東西讓跟實例調用,最終仍是以setData的方式改變基本數據,從而引起組件同步更新

能夠看到,只要利用好了HTMLParser以及snabbdom兩大神器,咱們的框架代碼變化簡單許多,而瞭解了這兩大神器的使用後,再去讀Vue的源碼可能也會簡單流暢一些

結語

前段時間,咱們由於想要統一小程序&web&Native端的代碼作了一些研究,而且模仿着實現了一個簡單缺漏的mvvm框架,這樣的過程當中,咱們抓住了mvvm框架的基本脈絡,接下來咱們看看mpvue是怎麼作的,而後再繼續咱們後續的研究

對應Git代碼地址請見:https://github.com/yexiaochai/wxdemo/tree/master/mvvm

參考:

https://github.com/fastCreator/MVVM(極度參考,十分感謝該做者,直接看Vue會比較吃力的,可是看完這個做者的代碼便會輕易不少,惋惜這個做者沒有對應博客說明,否則就爽了)

https://www.tangshuang.net/3756.html

https://www.cnblogs.com/kidney/p/8018226.html

http://www.cnblogs.com/kidney/p/6052935.html

https://github.com/livoras/blog/issues/13

相關文章
相關標籤/搜索