前言

前面我们对微信小程序进行了研究:【微信小程序项目实践总结】30分钟从陌生到熟悉

在实际代码过程中我们发现,我们可能又要做H5站又要做小程序同时还要做个APP,这里会造成很大的资源浪费,如果设定一个规则,让我们可以先写H5代码,然后将小程序以及APP的业务差异代码做掉,岂不快哉?但小程序的web框架并不开源,不然也用不着我们在此费力了,经过研究,小程序web端框架是一套自研的MVVM框架,于是我们马上想到了借助第三方框架:

一套代码小程序&Web&Native运行的探索01

经过简单的研究,我们发现无论React或者Vue都是可以一定程度适应小程序开发模式的,市面上也有了对应的框架:mpvue&wepy

在使用以上任何一套框架体系前,我们都需要对MVVM框架有最基础的了解,不然后面随便遇到点什么问题可能都会变得难以继续,负责人要对团队负责而不是单纯只是想技术尝鲜,所以生产项目一定要使用自己能完成hold住的技术

这段时间我们尝试着去阅读Vue的源码,但现在的Vue是一个工程化产物,我们要学习的核心代码可能不到其中的1/3,多出来的是各种特性以及细节处理,在不清楚代码意图(目标)的情况下,事实上很难搞懂这段代码是要干什么,很多代码的出现都是一些精妙的小优化,知道的就会觉得十分惊艳,不知道的就会一头雾水,一来就看Vue的源码反而不利于深入了解

出于这个原因在网上看了很多源码介绍的文章,出来就放一个Vue官方的流程图,然后一套组合模块套路,基本就把我给打晕了,查询了很多资料,还是发现一些写的比较清晰的(我感觉适合多数人的)文章:

读懂源码:一步一步实现一个 Vue

https://github.com/fastCreator/MVVM(特别推荐,非常不错)

这两篇文章都有一个主旨:

在没有相关框架经验的情况下,单单靠单步调试以及网上的源码介绍,想要读懂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,非常简单清晰,为了帮助更好的理解,我们这里也写了一段比较独立的代码,包括了核心流程:

 <div id="app">
<input type="text" v-model="name">
{{name}}
</div> <script type="text/javascript" > function getElById(id) {
return document.getElementById(id);
} //主体对象,存储所有的订阅者
function Dep () {
this.subs = [];
} //通知所有订阅者数据变化
Dep.prototype.notify = function () {
for(let i = 0, l = this.subs.length; i < l; i++) {
this.subs[i].update();
}
} //添加订阅者
Dep.prototype.addSub = function (sub) {
this.subs.push(sub);
} let globalDataDep = new Dep(); //观察者,框架会接触data的每一个与node相关的属性,
//如果data没有与任何节点产生关联,则不予理睬
//实际的订阅者对象
//注意,只要一个数据对象对应了一个node对象就会生成一个订阅者,所以真实通知的时候应该需要做到通知到对应数据的dom,这里不予关注
function Watcher(vm, node, name) {
this.name = name;
this.node = node;
this.vm = vm;
if(node.nodeType === 1) {
this.node.value = this.vm.data[name];
} else if(node.nodeType === 3) {
this.node.nodeValue = this.vm.data[name] || '';
}
globalDataDep.addSub(this); } Watcher.prototype.update = function () {
if(this.node.nodeType === 1) {
this.node.value = this.vm.data[this.name ];
} else if(this.node.nodeType === 3) {
this.node.nodeValue = this.vm.data[this.name ] || '';
}
} //这块代码仅做功能说明,不用当真
function compile(node, vm) {
let reg = /\{\{(.*)\}\}/; //节点类型
if(node.nodeType === 1) {
let attrs = node.attributes;
//解析属性
for(let i = 0, l = attrs.length; i < l; i++) {
if(attrs[i].nodeName === 'v-model') {
let name = attrs[i].nodeValue;
if(node.value === vm.data[name]) break; // node.value = vm.data[name] || '';
new Watcher(vm, node, name) //此处不做太多判断,直接绑定事件
node.addEventListener('input', function (e) {
//赋值操作
let newObj = {};
newObj[name] = e.target.value;
vm.setData(newObj, true);
}); break;
}
}
} else if(node.nodeType === 3) { if(reg.test(node.nodeValue)) {
let name = RegExp.$1; // 获取匹配到的name
name = name.trim();
// node.nodeValue = vm.data[name] || '';
new Watcher(vm, node, name)
}
}
} //获取节点
function nodeToFragment(node, vm) {
let flag = document.createDocumentFragment();
let child; while (child = node.firstChild) {
compile(child, vm);
flag.appendChild(child);
} return flag;
} function MVVM(options) {
this.data = options.data;
let el = getElById(options.el);
this.$dom = nodeToFragment(el, this)
this.$el = el.appendChild(this.$dom); // this.$bindEvent();
} MVVM.prototype.setData = function (data, noNotify) {
for(let k in data) {
this.data[k] = data[k];
}
//执行更新逻辑
// if(noNotify) return;
globalDataDep.notify();
} let mvvm = new MVVM({
el: 'app',
data: {
name: '叶小钗'
}
}) setTimeout(function() {
mvvm.setData({name: '刀狂剑痴叶小钗'})
}, 3000) </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模板,这里举个例子:

 <!doctype html>
<html>
<head>
<title>起步</title>
</head>
<body> <div id="app"> </div>
<script > // Regular Expressions for parsing tags and attributes
let startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:@][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/,
endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/,
attr = /([a-zA-Z_:@][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g // Empty Elements - HTML 5
let empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr") // Block Elements - HTML 5
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") // Inline Elements - HTML 5
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") // Elements that you can, intentionally, leave open
// (and which close themselves)
let closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr") // Attributes that have their values filled in disabled="disabled"
let fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected") // Special Elements (can contain anything)
let special = makeMap("script,style") function makeMap(str) {
var obj = {}, items = str.split(",");
for (var i = 0; i < items.length; i++)
obj[items[i]] = true;
return obj;
} function HTMLParser(html, handler) {
var index, chars, match, stack = [], last = html;
stack.last = function () {
return this[this.length - 1];
}; while (html) {
chars = true; // Make sure we're not in a script or style element
if (!stack.last() || !special[stack.last()]) { // Comment
if (html.indexOf("<!--") == 0) {
index = html.indexOf("-->"); if (index >= 0) {
if (handler.comment)
handler.comment(html.substring(4, index));
html = html.substring(index + 3);
chars = false;
} // end tag
} else if (html.indexOf("</") == 0) {
match = html.match(endTag); if (match) {
html = html.substring(match[0].length);
match[0].replace(endTag, parseEndTag);
chars = false;
} // start tag
} else if (html.indexOf("<") == 0) {
match = html.match(startTag); if (match) {
html = html.substring(match[0].length);
match[0].replace(startTag, parseStartTag);
chars = false;
}
} if (chars) {
index = html.indexOf("<"); var text = index < 0 ? html : html.substring(0, index);
html = index < 0 ? "" : html.substring(index); if (handler.chars)
handler.chars(text);
} } else {
html = html.replace(new RegExp("([\\s\\S]*?)<\/" + stack.last() + "[^>]*>"), function (all, text) {
text = text.replace(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/g, "$1$2");
if (handler.chars)
handler.chars(text); return "";
}); parseEndTag("", stack.last());
} if (html == last)
throw "Parse Error: " + html;
last = html;
} // Clean up any remaining tags
parseEndTag(); function parseStartTag(tag, tagName, rest, unary) {
tagName = tagName.toLowerCase(); if (block[tagName]) {
while (stack.last() && inline[stack.last()]) {
parseEndTag("", stack.last());
}
} if (closeSelf[tagName] && stack.last() == tagName) {
parseEndTag("", tagName);
} unary = empty[tagName] || !!unary; if (!unary)
stack.push(tagName); if (handler.start) {
var attrs = []; rest.replace(attr, function (match, name) {
var value = arguments[2] ? arguments[2] :
arguments[3] ? arguments[3] :
arguments[4] ? arguments[4] :
fillAttrs[name] ? name : ""; attrs.push({
name: name,
value: value,
escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //"
});
}); if (handler.start)
handler.start(tagName, attrs, unary);
}
} function parseEndTag(tag, tagName) {
// If no tag name is provided, clean shop
if (!tagName)
var pos = 0; // Find the closest opened tag of the same type
else
for (var pos = stack.length - 1; pos >= 0; pos--)
if (stack[pos] == tagName)
break; if (pos >= 0) {
// Close all the open elements, up the stack
for (var i = stack.length - 1; i >= pos; i--)
if (handler.end)
handler.end(stack[i]); // Remove the open elements from the stack
stack.length = pos;
}
}
}; html = `
<div id="s_wrap" class="s-isindex-wrap">
<div id="s_main" class="main clearfix">
<div id="s_mancard_main" class="s-mancacrd-main">
<div class="s-menu-container">
<div id="s_menu_gurd" class="s-menu-gurd">
<div id="s_ctner_menus" class="s-ctner-menus s-opacity-blank8">
<span id="s_menu_mine" class="s-menu-item s-menu-mine s-opacity-white-background current" data-id="100">
<div class="mine-icon"></div>
<div class="mine-text">我的关注</div>
</span>
<div class="s-menus-outer">
<div id="s_menus_wrapper" class="menus-wrapper"></div>
<div class="s-bg-space s-opacity-white-background"></div>
<span class="s-menu-music" data-id="3"></span>
</div>
<span id="s_menu_set" class="s-menu-setting s-opacity-white-background" data-id="99" title="设置">
<div class="menu-icon"></div>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
` HTMLParser(html,{
start: function(tag, attrs, unary) {
console.log('标签头', tag, attrs)
},
end: function (tag) {
console.log('标签尾', tag)
},
//处理真实的节点
chars: function(text) {
console.log('标签字段', text.trim().length > 0 ? text : '空字符' )
}
}) </script> </body>
</html>

HTMLParser的简单例子

 html = `
<div id="s_wrap" class="s-isindex-wrap">
<div id="s_main" class="main clearfix">
<div id="s_mancard_main" class="s-mancacrd-main">
<div class="s-menu-container">
<div id="s_menu_gurd" class="s-menu-gurd">
<div id="s_ctner_menus" class="s-ctner-menus s-opacity-blank8">
<span id="s_menu_mine" class="s-menu-item s-menu-mine s-opacity-white-background current" data-id="100">
<div class="mine-icon"></div>
<div class="mine-text">我的关注</div>
</span>
<div class="s-menus-outer">
<div id="s_menus_wrapper" class="menus-wrapper"></div>
<div class="s-bg-space s-opacity-white-background"></div>
<span class="s-menu-music" data-id="3"></span>
</div>
<span id="s_menu_set" class="s-menu-setting s-opacity-white-background" data-id="99" title="设置">
<div class="menu-icon"></div>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
` HTMLParser(html,{
start: function(tag, attrs, unary) {
console.log('标签头', tag, attrs)
},
end: function (tag) {
console.log('标签尾', tag)
},
//处理真实的节点
chars: function(text) {
console.log('标签字段', text.trim().length > 0 ? text : '空字符' )
}
})

这里使用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>
 _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.data = Ctor.data || {};
var el =createElement('sel')
vnode.elm.append(el)
Ctor.el = el;
componentVm = new Factory(Ctor);
vnode.key = componentVm.uid;
componentVm._isComponent = true
componentVm.$parent = this;
(this.$children || (this.$children = [])).push(componentVm);
//写在调用父组件值
for (let key in data.attrs) {
if (Ctor.data[key]) {
warn(`data:${key},已存在`);
continue;
}
}
}
Ctor._vnode = new VNode(sel,null,data, [], undefined, createElement(sel));
return Ctor._vnode
}

但是我们为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

【一套代码小程序&Native&Web阶段总结篇】可以这样阅读Vue源码的更多相关文章

  1. 一套代码小程序&Web&Native运行的探索07——mpvue简单调研

    前言 接上文:[一套代码小程序&Native&Web阶段总结篇]可以这样阅读Vue源码 最近工作比较忙,加之上个月生了小孩,小情人是各种折腾他爸妈,我们可以使用的独立时间片不多,虽然这 ...

  2. 一套代码小程序&Web&Native运行的探索06——组件系统

    接上文:一套代码小程序&Web&Native运行的探索05——snabbdom 对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tre ...

  3. 一套代码小程序&Web&Native运行的探索05——snabbdom

    接上文:一套代码小程序&Web&Native运行的探索04——数据更新 对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tree/ma ...

  4. 一套代码小程序&Web&Native运行的探索03——处理模板及属性

    接上文:一套代码小程序&Web&Native运行的探索02 对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tree/master/m ...

  5. 一套代码小程序&Web&Native运行的探索04——数据更新

    接上文:一套代码小程序&Web&Native运行的探索03 对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tree/master/m ...

  6. 一套代码小程序&Web&Native运行的探索02

    接上文:一套代码小程序&Web&Native运行的探索01,本文都是一些探索性为目的的研究学习,在最终版输出前,内中的内容可能会有点乱 参考: https://github.com/f ...

  7. 一套代码小程序&Web&Native运行的探索01

    前言 前面我们对微信小程序进行了研究:[微信小程序项目实践总结]30分钟从陌生到熟悉 并且用小程序翻写了之前一个demo:[组件化开发]前端进阶篇之如何编写可维护可升级的代码 之前一直在跟业务方打交道 ...

  8. 微信小程序中如何使用WebSocket实现长连接(含完整源码)

    本文由腾讯云技术团队原创,感谢作者的分享. 1.前言   微信小程序提供了一套在微信上运行小程序的解决方案,有比较完整的框架.组件以及 API,在这个平台上面的想象空间很大.腾讯云研究了一番之后,发现 ...

  9. 说说 PWA 和微信小程序--Progressive Web App

    作者:云图图链接:https://zhuanlan.zhihu.com/p/22578965来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 利益相关:微信小用户,谷歌小 ...

随机推荐

  1. 【源码解析】Sharding-Jdbc的执行过程(一)

    一.ShardingContext 在Sharding-Jdbc中,我们其实需要抓住一个核心类,也就是ShardingContext,分片上下文,里面定义了下面几个内容: @RequiredArgsC ...

  2. BZOJ_1485_[HNOI2009]有趣的数列_卡特兰数

    BZOJ_1485_[HNOI2009]有趣的数列_卡特兰数 Description 我们称一个长度为2n的数列是有趣的,当且仅当该数列满足以下三个条件: (1)它是从1到2n共2n个整数的一个排列{ ...

  3. BZOJ1854: [Scoi2010]游戏 二分图

    很早之前写的题了,发现没有更博,想了想,更一发出来. Orz ljss 这是冬令营上的例题...之后,我推出来了一种时间复杂度没有问题,空间复杂度没有问题的方法,额(⊙o⊙)…和给出的正解不同,但是能 ...

  4. 关于String类型中==和equals的区别。

    "=="操作符的作用 1.用于基本数据类型的比较,比较的是值. 2.用于比较对象,判断对象的引用是否指向堆内存的同一块地址. equals的作用 用于比较两个对象的内容是否相同 代 ...

  5. java自动化-数据驱动juint演示,上篇

    本文旨在帮助读者介绍,一般的全自动化代码接口,并简单介绍如何使用数据驱动来实现简单的自动化 在经过上述几个博客介绍后,相信读者对自动启动执行一个java编译过的class有了一定了解,也完全有能力去执 ...

  6. eShopOnContainers 知多少[9]:Ocelot gateways

    引言 客户端与微服务的通信问题永远是一个绕不开的问题,对于小型微服务应用,客户端与微服务可以使用直连的方式进行通信,但对于对于大型的微服务应用我们将不得不面对以下问题: 如何降低客户端到后台的请求数量 ...

  7. 怎么构建vue-cli项目

    1.安装node.js(已安装可直接跳过,建议查看node版本,node -v): 2.npm包管理器,是集成在node中的,可跳过(npm -v): 3.由于npm的有些资源被墙,为了更快更稳定,所 ...

  8. 零基础如何学Python爬虫技术?

    在作者学习的众多编程技能中,爬虫技能无疑是最让作者着迷的.与自己闭关造轮子不同,爬虫的感觉是与别人博弈,一个在不停的构建 反爬虫 规则,一个在不停的破译规则. 如何入门爬虫?零基础如何学爬虫技术?那前 ...

  9. 前端 SPA 单页应用数据统计解决方案 (ReactJS / VueJS)

    前端 SPA 单页应用数据统计解决方案 (ReactJS / VueJS) 一.百度统计的代码: UV PV 统计方式可能存在问题 在 SPA 的前端项目中 数据统计,往往就是一个比较麻烦的事情,Re ...

  10. 命令行中的 vi 模式

    命令行中修改已经输入的命令比较麻烦,如果你不知道一些快捷键的话,只能使用方向键一个一个字符地移动到目标位置进行修改,对于比较复杂且过长的命令来说,效率不高. 以下信息来自 bash 的 man 页面: ...