前言

前面我们对微信小程序进行了研究:【微信小程序项目实践总结】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. BZOJ_3362_[Usaco2004 Feb]Navigation Nightmare 导航噩梦_并查集

    BZOJ_3362_[Usaco2004 Feb]Navigation Nightmare 导航噩梦_并查集 Description     农夫约翰有N(2≤N≤40000)个农场,标号1到N,M( ...

  2. BZOJ_3173_[Tjoi2013]最长上升子序列_splay

    BZOJ_3173_[Tjoi2013]最长上升子序列_splay Description 给定一个序列,初始为空.现在我们将1到N的数字插入到序列中,每次将一个数字插入到一个特定的位置.每插入一个数 ...

  3. Discuz3.4-SSRF-从触发点到构造payload

    目录 SSRF逆向分析 0x00 前言 0x01 收集情报 0x02 尝试逆向找到触发点 0x03 尝试构造payload 0x04 总结 SSRF逆向分析 0x00 前言 之前有复现过一些漏洞,但是 ...

  4. 大数据技术之_19_Spark学习_03_Spark SQL 应用解析 + Spark SQL 概述、解析 、数据源、实战 + 执行 Spark SQL 查询 + JDBC/ODBC 服务器

    第1章 Spark SQL 概述1.1 什么是 Spark SQL1.2 RDD vs DataFrames vs DataSet1.2.1 RDD1.2.2 DataFrame1.2.3 DataS ...

  5. H5 新特性之 fileReader 实现本地图片视频资源的预览

    大家好 !!  又见面了, 今天我们来搞一搞   H5的新增API    FileReader     真是一个超级超级方便的API呢!!!很多场景都可以使用.......... 我们先不赘述MDN文 ...

  6. asp.net core 系列之用户认证(authentication)

    ASP.NET Core 的 identity 是一种需要用户登录的会员系统,用户可以创建一个登录信息存储在 Identity 的的账号, 或者也可以使用第三方登录,支持的第三方登录包括:Facebo ...

  7. 闲聊js中的apply、call和arguments

    JavaScript提供了apply和call两种调用方式来确定函数中的this的指向,在现实编码中,我确实 很少接触到这两个方法.但很无奈,很多面试题都要考这两种方法,我又没怎么用到,所以我们先来 ...

  8. 视频当道的时代,这些珍藏的优质 Python 播客值得推荐

    我国互联网的发展道路与欧美不同,在内容的形式上,我们似乎实现了跨越式的发展——早早进入了移动互联网时代,直播和短视频等形式的内容成为了潮流,而文字形式的博客(blog)与声音形式的播客(podcast ...

  9. SpringBoot自动配置原理

    前言 只有光头才能变强. 文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y 回顾前面Spring的文章(以学习的顺序排好): S ...

  10. Android版数据结构与算法(一):基础简介

    版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 一.前言 项目进入收尾阶段,忙忙碌碌将近一个多月吧,还好,不算太难,就是麻烦点. 数据结构与算法这个系列早就想写了,一是梳理总结,顺便逼迫自己把一 ...