对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tree/master/mvvm
我们在研究如果小程序在多端运行的时候,基本在前端框架这块陷入了困境,因为市面上没有框架可以直接拿来用,而Vue的相识度比较高,而且口碑很好,我们便接着这个机会同步学习Vue也解决我们的问题,我们看看这个系列结束后,会不会离目标进一点,后续如果实现后会重新整理系列文章......
参考:
https://github.com/fastCreator/MVVM(极度参考,十分感谢该作者,直接看Vue会比较吃力的,但是看完这个作者的代码便会轻易很多,可惜这个作者没有对应博客说明,不然就爽了)
https://www.tangshuang.net/3756.html
https://www.cnblogs.com/kidney/p/8018226.html
https://github.com/livoras/blog/issues/13
上文中我们借助HTMLParser这种高级神器,终于将文本中的表达式替换了出来,这里单纯说文本这里也有以下问题:这段是不支持js代码的,+-、三元代码都不支持,所以以上都只是帮助我们理解,还是之前那句话,越是单纯的代码,越是考虑少的代码,可能越是能理解实现,但是后续仍然需要补足,我们这里还是要跟Vue对齐,这样做有个好处,当你不知道怎么做的时候,可以看看Vue的实现,当你思考这么做合不合适的时候,也可以参考Vue,那可是经过烈火淬炼的,值得深度学习,我们今天的任务比较简单便是完整的处理完style、属性以及表达式处理,这里我们直接在fastCreator这个作者下的源码开始学习,还有种学习源码的方法就是抄三次......
我们学习的过程,先将代码写到一起方便理解,后续再慢慢拆分,首先是MVVM类,我们新建libs文件夹,先新建两个js文件,一个html-parser一个index(框架入口文件)
libs
--index.js
--html-parser.js
index.html
import HTMLParser from './html-parser.js' function arrToObj(arr) {
let map = {};
for(let i = 0, l = arr.length; i < l; i++) {
map[arr[i].name] = arr[i].value
}
return map;
} function htmlParser(html) { //存储所有节点
let nodes = []; //记录当前节点位置,方便定位parent节点
let stack = []; HTMLParser(html, {
/*
unary: 是不是自闭和标签比如 <br/> input
attrs为属性的数组
*/
start: function( tag, attrs, unary ) { //标签开始
/*
stack记录的父节点,如果节点长度大于1,一定具有父节点
*/
let parent = stack.length ? stack[stack.length - 1] : null; //最终形成的node对象
let node = {
//1标签, 2需要解析的表达式, 3 纯文本
type: 1,
tag: tag,
attrs: arrToObj(attrs),
parent: parent,
//关键属性
children: []
}; //如果存在父节点,也标志下这个属于其子节点
if(parent) {
parent.children.push(node);
}
//还需要处理<br/> <input>这种非闭合标签
//... //进入节点堆栈,当遇到弹出标签时候弹出
stack.push(node)
nodes.push(node); // debugger;
},
end: function( tag ) { //标签结束
//弹出当前子节点,根节点一定是最后弹出去的,兄弟节点之间会按顺序弹出,其父节点在最后一个子节点弹出后会被弹出
stack.pop(); // debugger;
},
chars: function( text ) { //文本
//如果是空格之类的不予处理
if(text.trim() === '') return;
text = text.trim(); //匹配 {{}} 拿出表达式
let reg = /\{\{(.*)\}\}/;
let node = nodes[nodes.length - 1];
//如果这里是表达式{{}}需要特殊处理
if(!node) return; if(reg.test(text)) {
node.children.push({
type: 2,
expression: RegExp.$1,
text: text
});
} else {
node.children.push({
type: 3,
text: text
});
}
// debugger;
}
}); return nodes; } export default class MVVM {
/*
暂时要求必须传入data以及el,其他事件什么的不管 */
constructor(opts) { //要求必须存在,这里不做参数校验了
this.$el = typeof opts.el === 'string' ? document.getElementById(opts.el) : opts.el; //data必须存在,其他不做要求
this.$data = opts.data; //模板必须存在
this.$template = opts.template; //存放解析结束的虚拟dom
this.$nodes = []; //将模板解析后,转换为一个函数
this.$initRender(); //渲染之
this.$render();
debugger;
} $initRender() {
let template = this.$template;
let nodes = htmlParser(template);
this.$nodes = nodes;
} //解析模板生成的函数,将最总html结构渲染出来
$render() { let data = this.$data;
let root = this.$nodes[0];
let parent = this._createEl(root);
//简单遍历即可 this._render(parent, root.children); this.$el.appendChild(parent);
} _createEl(node) {
let data = this.$data; let el = document.createElement(node.tag || 'span'); for (let key in node.attrs) {
el.setAttribute(key, node.attrs[key])
} if(node.type === 2) {
el.innerText = data[node.expression];
} else if(node.type === 3) {
el.innerText = node.text;
} return el;
}
_render(parent, children) {
let child = null;
for(let i = 0, len = children.length; i < len; i++) {
child = this._createEl(children[i]);
parent.append(child);
if(children[i].children) this._render(child, children[i].children);
}
} }
index
/*
* Modified at https://github.com/blowsie/Pure-JavaScript-HTML5-Parser
*/ // 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;
} export default 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-parser
这个时候我们的index代码量便下来了:
<!doctype html>
<html>
<head>
<title>起步</title>
</head>
<body> <div id="app"> </div> <script type="module"> import MVVM from './libs/index.js' let html = `
<div class="c-row search-line" data-flag="start" ontap="clickHandler">
<div class="c-span9 js-start search-line-txt">
{{name}}</div>
<input type="text">
<br>
</div>
` let vm = new MVVM({
el: 'app',
template: html,
data: {
name: '叶小钗'
}
}) </script>
</body>
</html>
我们现在来更改index.js入口文件的代码,这里特别说一下其中的$mount方法,他试试是要做一个这样的事情:
//模板字符串
<div id = "app">
{{message}}
</div>
//render函数
function anonymous() {
with(this){return _h('div',{attrs:{"id":"app"}},["\n "+_s(message)+"\n"])}
}
将模板转换为一个函数render放到参数上,这里我们先简单实现,后续深入后我们重新翻下这个函数,修改后我们的index.js变成了这个样子:
import HTMLParser from './html-parser.js' //工具函数 begin function isFunction(obj) {
return typeof obj === 'function'
} function makeAttrsMap(attrs, delimiters) {
const map = {}
for (let i = 0, l = attrs.length; i < l; i++) {
map[attrs[i].name] = attrs[i].value;
}
return map;
} //dom操作
function query(el) {
if (typeof el === 'string') {
const selector = el
el = document.querySelector(el)
if (!el) {
return document.createElement('div')
}
}
return el
} function cached(fn) {
const cache = Object.create(null)
return function cachedFn(str) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}
} let idToTemplate = cached(function (id) {
var el = query(id)
return el && el.innerHTML;
}) //工具函数 end //模板解析函数 begin const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
const regexEscapeRE = /[-.*+?^${}()|[\]/\\]/g const buildRegex = cached(delimiters => {
const open = delimiters[0].replace(regexEscapeRE, '\\$&')
const close = delimiters[1].replace(regexEscapeRE, '\\$&')
return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
}) function TextParser(text, delimiters) {
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
if (!tagRE.test(text)) {
return
}
const tokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index
while ((match = tagRE.exec(text))) {
index = match.index
// push text token
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}
// tag token
const exp = match[1].trim()
tokens.push(`_s(${exp})`)
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
return tokens.join('+')
} //******核心中的核心
function compileToFunctions(template, vm) {
let root;
let currentParent;
let options = vm.$options;
let stack = []; //这段代码昨天做过解释,这里属性参数比昨天多一些
HTMLParser(template, {
start: function(tag, attrs, unary) { let element = {
vm: vm,
//1 标签 2 文本表达式 3 文本
type: 1,
tag,
//数组
attrsList: attrs,
attrsMap: makeAttrsMap(attrs), //将属性数组转换为对象
parent: currentParent,
children: []
}; if(!root) {
vm.$vnode = root = element;
} if(currentParent && !element.forbidden) {
currentParent.children.push(element);
element.parent = currentParent;
} if(!unary) {
currentParent = element;
stack.push(element);
} },
end: function (tag) {
//获取当前元素
let element = stack[stack.length - 1];
let lastNode = element.children[element.children.length - 1];
//删除最后一个空白节点,暂时感觉没撒用呢
if(lastNode && lastNode.type === 3 && lastNode.text.trim === '') {
element.children.pop();
} //据说比调用pop节约性能相当于stack.pop()
stack.length -= 1;
currentParent = stack[stack.length - 1]; },
//处理真实的节点
chars: function(text) {
if (!text.trim()) {
//text = ' '
return;
}
//解析文本节点 exp: a{{b}}c => 'a'+_s(a)+'b'
let expression = TextParser(text, options.delimiters)
if (expression) {
currentParent.children.push({
type: 2,
expression,
text
})
} else {
currentParent && currentParent.children.push({
type: 3,
text
})
}
} }); return root; } //模板解析函数 end //因为我们后面采用setData的方式通知更新,不做响应式更新,这里也先不考虑update,不考虑监控,先关注首次渲染
//要做到更新数据,DOM跟着更新,事实上就是所有的data数据被监控(劫持)起来了,一旦更新都会调用对应的回调,我们这里做到更新再说
function initData(vm, data) {
if (isFunction(data)) {
data = data()
}
vm.$data = data;
} //全局数据保证每个MVVM实例拥有唯一id
let uid = 0; export default class MVVM {
constructor(options) {
this.$options = options; //我们可以在传入参数的地方设置标签替换方式,比如可以设置为['<%=', '%>'],注意这里是数组
this.$options.delimiters = this.$options.delimiters || ["{{", "}}"]; //唯一标志
this._uid = uid++; if(options.data) {
//
initData(this, options.data);
} this.$mount(options.el); } //解析模板compileToFunctions,将之形成一个函数
//很多网上的解释是将实例挂载到dom上,这里有些没明白,我们后面点再看看
$mount(el) {
let options = this.$options; el = el && query(el);
this.$el = el; //如果用户自定义了render函数则不需要解析template
//这里所谓的用户自定义,应该是用户生成了框架生成那坨代码,事实上还是将template转换为vnode
if(!options.render) {
let template = options.template;
if(template) {
if(typeof template === 'string') {
//获取script的template模板
if (template[0] === '#') {
template = idToTemplate(template)
}
} else if (template.nodeType) {
//如果template是个dom结构,只能有一个根节点
template = template.innerHTML;
}
} //上面的代码什么都没做,只是确保正确的拿到了template数据,考虑了各种情况
//下面这段是关键,也是我们昨天干的事情
if(template) {
//***核心函数***/
let render = compileToFunctions(template, this);
options.render = render;
} } } } //过去的代码
function arrToObj(arr) {
let map = {};
for(let i = 0, l = arr.length; i < l; i++) {
map[arr[i].name] = arr[i].value
}
return map;
} function htmlParser(html) { //存储所有节点
let nodes = []; //记录当前节点位置,方便定位parent节点
let stack = []; HTMLParser(html, {
/*
unary: 是不是自闭和标签比如 <br/> input
attrs为属性的数组
*/
start: function( tag, attrs, unary ) { //标签开始
/*
stack记录的父节点,如果节点长度大于1,一定具有父节点
*/
let parent = stack.length ? stack[stack.length - 1] : null; //最终形成的node对象
let node = {
//1标签, 2需要解析的表达式, 3 纯文本
type: 1,
tag: tag,
attrs: arrToObj(attrs),
parent: parent,
//关键属性
children: []
}; //如果存在父节点,也标志下这个属于其子节点
if(parent) {
parent.children.push(node);
}
//还需要处理<br/> <input>这种非闭合标签
//... //进入节点堆栈,当遇到弹出标签时候弹出
stack.push(node)
nodes.push(node); // debugger;
},
end: function( tag ) { //标签结束
//弹出当前子节点,根节点一定是最后弹出去的,兄弟节点之间会按顺序弹出,其父节点在最后一个子节点弹出后会被弹出
stack.pop(); // debugger;
},
chars: function( text ) { //文本
//如果是空格之类的不予处理
if(text.trim() === '') return;
text = text.trim(); //匹配 {{}} 拿出表达式
let reg = /\{\{(.*)\}\}/;
let node = nodes[nodes.length - 1];
//如果这里是表达式{{}}需要特殊处理
if(!node) return; if(reg.test(text)) {
node.children.push({
type: 2,
expression: RegExp.$1,
text: text
});
} else {
node.children.push({
type: 3,
text: text
});
}
// debugger;
}
}); return nodes; } class MVVM1 {
/*
暂时要求必须传入data以及el,其他事件什么的不管 */
constructor(opts) { //要求必须存在,这里不做参数校验了
this.$el = typeof opts.el === 'string' ? document.getElementById(opts.el) : opts.el; //data必须存在,其他不做要求
this.$data = opts.data; //模板必须存在
this.$template = opts.template; //存放解析结束的虚拟dom
this.$nodes = []; //将模板解析后,转换为一个函数
this.$initRender(); //渲染之
this.$render();
debugger;
} $initRender() {
let template = this.$template;
let nodes = htmlParser(template);
this.$nodes = nodes;
} //解析模板生成的函数,将最总html结构渲染出来
$render() { let data = this.$data;
let root = this.$nodes[0];
let parent = this._createEl(root);
//简单遍历即可 this._render(parent, root.children); this.$el.appendChild(parent);
} _createEl(node) {
let data = this.$data; let el = document.createElement(node.tag || 'span'); for (let key in node.attrs) {
el.setAttribute(key, node.attrs[key])
} if(node.type === 2) {
el.innerText = data[node.expression];
} else if(node.type === 3) {
el.innerText = node.text;
} return el;
}
_render(parent, children) {
let child = null;
for(let i = 0, len = children.length; i < len; i++) {
child = this._createEl(children[i]);
parent.append(child);
if(children[i].children) this._render(child, children[i].children);
}
} }
index.js
这里仅仅是到输出vnode这步,接下来是将vnode转换为函数render,在写这段代码之前我们来说一说Vue中的render参数,事实上,我们new Vue的时候可以直接传递render参数:
new Vue({
render: function () {
return this._h('div', {
attrs:{
a: 'aaa'
}
}, [
this._h('div')
])
}
})
他对应的这段代码:
new Vue({
template: '<div class="aa">Hello World! </div>'
})
真实代码过程中的过程,以及我们上面代码的过程是,template 字符串 => 虚拟DOM对象 ast => 根据ast生成render函数......,这里又涉及到了另一个需要引用的工具库snabbdom
snabbdom-render
https://github.com/snabbdom/snabbdom,Vue2.0底层借鉴了snabdom,我们这里先重点介绍他的h函数,h(help帮助创建vnode)函数可以让我们轻松创建vnode,这里再对Virtual DOM做一个说明,这段话是我看到觉得很好的解释的话(https://github.com/livoras/blog/issues/13):
我们一段js对象可以很容易的翻译为一段HTML代码:
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"]},
]
}
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>
同样的,我们一段HTML代码其实属性、参数是很有限的,也十分轻易的能转换成一个js对象,我们如果使用dom操作改变了我们的html结构,事实上会形成一个新的js对象,这个时候我们将渲染后形成的js对象和渲染前形成的js对象进行对比,便可以清晰知道这次变化的差异部分,然后拿着差异部分的js对象(每个js对象都会映射到一个真实的dom对象)做更新即可,关于Virtual DOM文章作者对此做了一个总结:
① 用js对象表示DOM树结构,然后用这个js对象树结构生成一个真正的DOM树(document.create***操作),插入文档中(这个时候会形成render tree,看得到了)
② 当状态变化时(数据变化时),重新构造一颗新的对象树,和之前的作对比,记录差异部分
③ 将差异部分的数据更新到视图上,更新结束
他这里描述的比较简单,事实上我们根据昨天的学习,可以知道框架事实上是劫持了没个数据对象,所以每个数据对象做了改变,会影响到哪些DOM结构是有记录的,这块我们后面章节再说,我们其实今天主要的目的还是处理文本和属性生成,却不想提前接触虚拟DOM了......
其实我们之前的js对象element就已经可以代表一个虚拟dom了,之所以引入snabbddom应该是后面要处理diff部分,所以我们乖乖的学吧,首先我们定义一个节点的类:
class Element {
constructor(tagName, props, children) {
this.tagName = tagName;
this.props = props;
this.children = children;
}
}
上面的dom结构便可以变成这样了:
new Element('ul', {id: 'list'}, [
new Element('li', {class: 'item'}, ['Item 1']),
new Element('li', {class: 'item'}, ['Item 2']),
new Element('li', {class: 'item'}, ['Item 3'])
])
似乎代码有点不好看,于是封装下实例化操作:
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'])
])
然后就是根据这个js对象生成真正的DOM结构,也就是上面的html字符串:
<!doctype html>
<html>
<head>
<title>起步</title>
</head>
<body> <script type="text/javascript">
//***虚拟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); </script> </body>
</html>
饶了这么大一圈子,我们再回头看这段代码:
new Vue({
render: function () {
return this._h('div', {
attrs:{
a: 'aaa'
}
}, [
this._h('div')
])
}
})
这个时候,我们对这个_h干了什么,可能便有比较清晰的认识了,于是我们回到我们之前的代码,暂时跳出snabbdom
解析模板
在render中,我们有这么一段代码:
//没有指令时运行,或者指令解析完毕
function nodir(el) {
let code
//设置属性 等值
const data = genData(el);
//转换子节点
const children = genChildren(el, true);
code = `_h('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
return code
}
事实上这个跟上面那坨代码完成的工作差不多(同样的遍历加递归),只不过他这里还有更多的目的,比如这段代码最终会生成这样的:
_h('div',{},[_h('div',{},["\n "+_s(name)]),_h('input',{}),_h('br',{})])
这段代码会被包装成一个模板类,等待被实例化,显然到这里还没进入我们的模板解析过程,因为里面出现了_s(name),我们如果加一个span的话会变成这样:
<div class="c-row search-line" data-flag="start" ontap="clickHandler">
<div class="c-span9 js-start search-line-txt">
{{name}}</div>
<span>{{age+1}}</span>
<input type="text">
<br>
</div>
_h('div',{},[_h('div',{},["\n "+_s(name)]),_h('span',{},[_s(age+1)]),_h('input',{}),_h('br',{})])
真实运行的时候这段代码是这个样子的:
这段代码很纯粹,不包含属性和class,我们只需要处理文本内容替换即可,今天的任务比较简单,所以接下来的流程后便可以得出第一阶段代码:
<!doctype html>
<html>
<head>
<title>起步</title>
</head>
<body> <div id="app"> </div> <script type="module"> import MVVM from './libs/index.js' let html = `
<div class="c-row search-line" data-flag="start" ontap="clickHandler">
<div class="c-span9 js-start search-line-txt">
{{name}}</div>
<span>{{age+1}}</span>
<input type="text">
<br>
</div>
` let vm = new MVVM({
el: '#app',
template: html,
data: {
name: '叶小钗',
age: 30
}
}) </script>
</body>
</html>
import HTMLParser from './html-parser.js' //工具函数 begin function isFunction(obj) {
return typeof obj === 'function'
} function makeAttrsMap(attrs, delimiters) {
const map = {}
for (let i = 0, l = attrs.length; i < l; i++) {
map[attrs[i].name] = attrs[i].value;
}
return map;
} //dom操作
function query(el) {
if (typeof el === 'string') {
const selector = el
el = document.querySelector(el)
if (!el) {
return document.createElement('div')
}
}
return el
} function cached(fn) {
const cache = Object.create(null)
return function cachedFn(str) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}
} let idToTemplate = cached(function (id) {
var el = query(id)
return el && el.innerHTML;
}) //工具函数 end //模板解析函数 begin const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
const regexEscapeRE = /[-.*+?^${}()|[\]/\\]/g const buildRegex = cached(delimiters => {
const open = delimiters[0].replace(regexEscapeRE, '\\$&')
const close = delimiters[1].replace(regexEscapeRE, '\\$&')
return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
}) function TextParser(text, delimiters) {
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
if (!tagRE.test(text)) {
return
}
const tokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index
while ((match = tagRE.exec(text))) {
index = match.index
// push text token
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}
// tag token
const exp = match[1].trim()
tokens.push(`_s(${exp})`)
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
return tokens.join('+')
} function makeFunction(code) {
try {
return new Function(code)
} catch (e) {
return function (){};
}
} //***虚拟dom部分代码,后续会换成snabdom
class Element {
constructor(tagName, props, children) {
this.tagName = tagName;
this.props = props;
this.children = children || [];
}
render() {
//拿着根节点往下面撸
let el = document.createElement(this.tagName);
let props = this.props; for(let name in props) {
el.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);
}
el.append(childEl);
}
return el;
}
} function el(tagName, props, children) {
return new Element(tagName, props, children)
} //***核心中的核心,将vnode转换为函数 const simplePathRE = /^\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?']|\[".*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*\s*$/
const modifierCode = {
stop: '$event.stopPropagation();',
prevent: '$event.preventDefault();',
self: 'if($event.target !== $event.currentTarget)return;',
ctrl: 'if(!$event.ctrlKey)return;',
shift: 'if(!$event.shiftKey)return;',
alt: 'if(!$event.altKey)return;',
meta: 'if(!$event.metaKey)return;'
} const keyCodes = {
esc: 27,
tab: 9,
enter: 13,
space: 32,
up: 38,
left: 37,
right: 39,
down: 40,
'delete': [8, 46]
} function codeGen(ast) {
//解析成h render字符串形式
const code = ast ? genElement(ast) : '_h("div")'
//把render函数,包起来,使其在当前作用域内
return makeFunction(`with(this){ debugger; return ${code}}`)
} function genElement(el) {
//无指令
return nodir(el)
} //没有指令时运行,或者指令解析完毕
function nodir(el) {
let code
//设置属性 等值
const data = genData(el);
//转换子节点
const children = genChildren(el, true);
code = `_h('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
return code
} function genChildren(el, checkSkip) {
const children = el.children
if (children.length) {
const el = children[0]
// 如果是v-for
//if (children.length === 1 && el.for) {
// return genElement(el)
//}
const normalizationType = 0
return `[${children.map(genNode).join(',')}]${
checkSkip
? normalizationType ? `,${normalizationType}` : ''
: ''
}`
}
} function genNode(node) {
if (node.type === 1) {
return genElement(node)
} else {
return genText(node)
}
} function genText(text) {
return text.type === 2 ? text.expression : JSON.stringify(text.text)
} function genData(el) {
let data = '{'
// attributes
if (el.style) {
data += 'style:' + genProps(el.style) + ','
}
if (Object.keys(el.attrs).length) {
data += 'attrs:' + genProps(el.attrs) + ','
}
if (Object.keys(el.props).length) {
data += 'props:' + genProps(el.props) + ','
}
if (Object.keys(el.events).length) {
data += 'on:' + genProps(el.events) + ','
}
if (Object.keys(el.hook).length) {
data += 'hook:' + genProps(el.hook) + ','
}
data = data.replace(/,$/, '') + '}'
return data
} function genProps(props) {
let res = '{';
for (let key in props) {
res += `"${key}":${props[key]},`
}
return res.slice(0, -1) + '}'
} //******核心中的核心
function compileToFunctions(template, vm) {
let root;
let currentParent;
let options = vm.$options;
let stack = []; //这段代码昨天做过解释,这里属性参数比昨天多一些
HTMLParser(template, {
start: function(tag, attrs, unary) { let element = {
vm: vm,
//1 标签 2 文本表达式 3 文本
type: 1,
tag,
//数组
attrsList: attrs,
attrsMap: makeAttrsMap(attrs), //将属性数组转换为对象
parent: currentParent,
children: [], //下面这些属性先不予关注,因为底层函数没有做校验,不传要报错
events: {},
style: null,
hook: {},
props: {},//DOM属性
attrs: {}//值为true,false则移除该属性 }; if(!root) {
vm.$vnode = root = element;
} if(currentParent && !element.forbidden) {
currentParent.children.push(element);
element.parent = currentParent;
} if(!unary) {
currentParent = element;
stack.push(element);
} },
end: function (tag) {
//获取当前元素
let element = stack[stack.length - 1];
let lastNode = element.children[element.children.length - 1];
//删除最后一个空白节点,暂时感觉没撒用呢
if(lastNode && lastNode.type === 3 && lastNode.text.trim === '') {
element.children.pop();
} //据说比调用pop节约性能相当于stack.pop()
stack.length -= 1;
currentParent = stack[stack.length - 1]; },
//处理真实的节点
chars: function(text) {
if (!text.trim()) {
//text = ' '
return;
}
//解析文本节点 exp: a{{b}}c => 'a'+_s(a)+'b'
let expression = TextParser(text, options.delimiters)
if (expression) {
currentParent.children.push({
type: 2,
expression,
text
})
} else {
currentParent && currentParent.children.push({
type: 3,
text
})
}
} }); //***关键代码***
//将vnode转换为render函数,事实上可以直接传入这种render函数,便不会执行这块逻辑,编译时候会把这块工作做掉
return codeGen(root); } //模板解析函数 end //因为我们后面采用setData的方式通知更新,不做响应式更新,这里也先不考虑update,不考虑监控,先关注首次渲染
//要做到更新数据,DOM跟着更新,事实上就是所有的data数据被监控(劫持)起来了,一旦更新都会调用对应的回调,我们这里做到更新再说
function initData(vm, data) {
if (isFunction(data)) {
data = data()
} //这里将data上的数据移植到this上,后面要监控
for(let key in data) { //这里有可能会把自身方法覆盖,所以自身的属性方法需要+$
vm[key] = data[key];
} vm.$data = data;
} //全局数据保证每个MVVM实例拥有唯一id
let uid = 0; export default class MVVM {
constructor(options) {
this.$options = options; //我们可以在传入参数的地方设置标签替换方式,比如可以设置为['<%=', '%>'],注意这里是数组
this.$options.delimiters = this.$options.delimiters || ["{{", "}}"]; //唯一标志
this._uid = uid++; if(options.data) {
//
initData(this, options.data);
} this.$mount(options.el); let _node = this._render().render();
this.$el.appendChild( _node) } //解析模板compileToFunctions,将之形成一个函数
//很多网上的解释是将实例挂载到dom上,这里有些没明白,我们后面点再看看
$mount(el) {
let options = this.$options; el = el && query(el);
this.$el = el; //如果用户自定义了render函数则不需要解析template
//这里所谓的用户自定义,应该是用户生成了框架生成那坨代码,事实上还是将template转换为vnode
if(!options.render) {
let template = options.template;
if(template) {
if(typeof template === 'string') {
//获取script的template模板
if (template[0] === '#') {
template = idToTemplate(template)
}
} else if (template.nodeType) {
//如果template是个dom结构,只能有一个根节点
template = template.innerHTML;
}
} //上面的代码什么都没做,只是确保正确的拿到了template数据,考虑了各种情况
//下面这段是关键,也是我们昨天干的事情
if(template) {
//***核心函数***/
let render = compileToFunctions(template, this);
options.render = render;
}
} return this;
} _render() {
let render = this.$options.render
let vnode
try {
//自动解析的template不需要h,用户自定义的函数需要h
vnode = render.call(this, this._h);
} catch (e) {
warn(`render Error : ${e}`)
}
return vnode
} _h(tag, data, children) {
return el(tag, data, children)
} _s(val) {
return val == null
? ''
: typeof val === 'object'
? JSON.stringify(val, null, 2)
: String(val)
} }
libs/index.js
之前我们图简单,一直没有解决属性问题,现在我们在模板里面加入一些属性:
1 <div class="c-row search-line" data-name="{{name}}" data-flag="start" ontap="clickHandler">
2 <div class="c-span9 js-start search-line-txt">
3 {{name}}</div>
4 <span>{{age+1}}</span>
5 <input type="text" value="{{age}}">
6 <br>
7 </div>
情况就变得有所不同了,这里多加一句:
1 setElAttrs(el, delimiters)
2 //==>
3 function setElAttrs(el, delimiters) {
4 var s = delimiters[0], e = delimiters[1];
5 var reg = new RegExp(`^${s}(\.+\)${e}$`);
6 var attrs = el.attrsMap;
7 for (let key in attrs) {
8 let value = attrs[key];
9 var match = value.match(reg)
10 if (match) {
11 value = match[1];
12 if (isAttr(key)) {
13 el.props[key] = '_s('+value+')';
14 } else {
15 el.attrs[key] = value;
16 }
17 } else {
18 if (isAttr(key)) {
19 el.props[key] = "'" + value + "'";
20 } else {
21 el.attrs[key] = "'" + value + "'";
22 }
23 }
24
25 }
26 }
这段代码会处理所有的属性,如果是属性中包含“{{}}”关键词,便会替换,不是我们的属性便放到attrs中,是的就放到props中,这里暂时不太能区分为什么要分为attrs何props,后续我们这边给出代码,于是我们的index.js变成了这个样子:
_h('div',{attrs:{"data-name":name,"data-flag":'start',"ontap":'clickHandler'},props:{"class":'c-row search-line'}},
[_h('div',{props:{"class":'c-span9 js-start search-line-txt'}},
["\n "+_s(name)]),_h('span',{},
[_s(age+1)]),_h('input',{props:{"type":'text',"value":_s(age)}}),_h('br',{})])
1 <div id="app">
2 <div class="c-row search-line" data-name="叶小钗" data-flag="start" ontap="clickHandler">
3 <div class="c-span9 js-start search-line-txt">
4 叶小钗</div>
5 <span>31</span>
6 <input type="text" value="30">
7 <br>
8 </div>
9 </div>
然后我们来处理class以及style,他们是需要特殊处理的:
<div class="c-row search-line {{name}} {{age}}" style="font-size: 14px; margin-left: {{age}}px " data-name="{{name}}"
data-flag="start" ontap="clickHandler">
<div class="c-span9 js-start search-line-txt">
{{name}}</div>
<span>{{age+1}}</span>
<input type="text" value="{{age}}">
<br>
</div>
生成了如下代码:
1 <div class="c-row search-line 叶小钗 30" data-name="叶小钗" data-flag="start" ontap="clickHandler" style="font-size: 14px; margin-left: 30px ;">
2 <div class="c-span9 js-start search-line-txt">
3 叶小钗</div>
4 <span>31</span>
5 <input type="text" value="30">
6 <br>
7 </div>
虽然这段代码能运行,无论如何我们的属性和class也展示出来了,但是问题却不少:
① 这段代码仅仅就是为了运行,或者说帮助我们理解
② libs/index.js代码已经超过了500行,维护起来有点困难了,连我自己都有时候找不到东西,所以我们该分拆文件了
于是,我们暂且忍受这段说明性(演示性)代码,将之进行文件分拆
文件分拆
文件拆分后代码顺便传到了github上:https://github.com/yexiaochai/wxdemo/tree/master/mvvm
这里简单的解释下各个文件是干撒的:
1 ./libs
2 ..../codegen.js 代码生成器,传入一个ast(js树对象),转换为render函数
3 ..../helps.js 处理vnode的相关工具函数,比如处理属性节点,里面的生成函数感觉该放到utils中
4 ..../html-parser.js 第三方库,HTML解析神器,帮助生成js dom树对象
5 ..../instance.js 初始化mvvm实例工具类
6 ..../mvvm.js 入口函数
7 ..../parser.js 模板解析生成render函数,核心
8 ..../text-parser.js 工具类,将{{}}做替换生成字符串
9 ..../utils.js 工具库
10 ..../vnode.js 虚拟树库,暂时自己写的,后续要换成snabbdom
11 ./index.html 入口文件
今天的学习到此位置,明天我们来处理数据更新相关