回顾上一篇
上一篇文章手写了响应式原理,目前达成效果当读取数据会走get里的方法,数据修改会走set方法,数组调用改变数组的方法会触发我们新写的方法 当前目录结构以及效果
好废话不多说直接开始
1、生成render函数
1、$mount
众所周知 vue是需要传进去一个根节点的即el,我们正是要把template模板的内容挂到el,这个el是必传的,但是方法不唯一也可以 <math xmlns="http://www.w3.org/1998/Math/MathML"> m o u n t ( e l ) , 传到 o p t i o n s 里的 e l 也是走 mount(el),传到options里的el也是走 </math>mount(el),传到options里的el也是走mount方法。 以下是实现过程 init.js里
js
export function initMixin(Vue) {
Vue.prototype._init = function(options) {
const vm = this;
vm.$options = options
initState(vm);
if(options.el) {
vm.$mount(options.el)
}
}
Vue.prototype.$mount = function(el) {
const vm = this;
el = document.querySelector(el);
let ops = vm.$options
if (!ops.render) {
let template;
if(!ops.template && el) { // 没写模板但是写了el
template = el.outerHTML
} else {
if(el) {
template = ops.template
}
}
if(template) {
const render = compileToFunction(template)
ops.render = render;
}
}
// mountComponent(vm, el) // 组件挂载
}
}
这块template也是options选项里的内容,如果传了就用传进来的没有会拿el节点的xml内容,最终我们总会拿到template,然后我们需要根据拿到的模板生成render函数,即compileToFunction
2、生成render函数第一步:生成ast语法树
src下新建compiler文件夹,新建index.js
js
export function compileToFunction(template) {
// 将template 转换成ast语法书
// 生成render方法 render方法执行后返回结果是 虚拟dom
let ast = parseHTML(template)
// 核心就是生成render函数
// let render = ''
// return render
}
对模板的处理用的是正则匹配,为维护方便,新建一个parse.js。因为只是学习原理不会写完所有正则匹配,只写几个常用的,这块真的太磨人了。 以下是用到的正则
js
// // 识别合法的xml标签
const ncname = '[a-zA-Z_][\\-\\.0-9_a-zA-Z]*';
// // 复用拼接
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// // 匹配注释
// var comment =/^<!--/;
// // 匹配<!DOCTYPE> 声明标签
// var doctype = /^<!DOCTYPE [^>]+>/i;
// // 匹配条件注释
// // var conditionalComment =/^<![/;
// // 匹配开始标签
const startTagOpen = new RegExp(`^<${qnameCapture}`);
// // 匹配结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
// // 匹配单标签
const startTagClose = /^\s*(\/?)>/;
// // 匹配属性,例如 id、class
// 第一分组是key value是 3/4/5 分组
const attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// {{ ... }}
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
下边的跟上边是同一个文件,为了加一些说明,所以拆开
js
export function parseHTML(html) {
// 标签type=1 文本=3 只是一个标识
const ELEMENT_TYPE = 1
const TEXT_TYPE = 3
...
while (html) {
// 首次传进来 <div></div>
let textEnd = html.indexOf('<');
// 如果<是第一个 再去用正则判断是不是起始标签的开始例:<div> 如果是则对标签内容进行处理
if (textEnd === 0) {
// 对开始标签处理
const startTagMatch = parseStartTag()
if(startTagMatch) {
// 生成ast语法对象
start(startTagMatch.tagName, startTagMatch.attrs)
continue
}
// 判断是不结束标签的开始例如</div>
let endTagMacth = html.match(endTag)
if(endTagMacth) {
end(endTagMacth[1])
advance(endTagMacth[0].length)
continue
}
}
// <尖角号不是开始而是在后边判断为文本内容处理 如 123</div> 对123进行处理
if(textEnd > 0) {
let text = html.substring(0,textEnd)
if(text) {
chars(text)
advance(text.length)
}
}
}
}
以上是判断出模板内容的几种情况,然后根据不同情况进行不同的处理
下面开始具体处理方法
1、对开始标签的处理
js
// 处理过的内容进行删除
function advance(n) {
html = html.substring(n)
}
function parseStartTag() {
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: []
}
// 对内容处理完后就删掉
advance(start[0].length)
let attr, end;
// 如果不是开始标签的结束(这里匹配到结束符号即>会给end赋值)
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length)
match.attrs.push({name: attr[1], value: attr[3] || attr[4] || attr[5] || true})
}
// 如果end有值了 则进行删除操作
if (end) {
advance(end[0].length)
}
// 结果举例:{tagName:'div', attrs:[{name:'id', value:'1'}] }
return match
}
// 如果不是开始标签的结束 循环match属性
return false
}
生成ast语法
js
// 创建一个栈,读取到开始标签就把当前标签生成的ast对象放进去
// 标签可能是多层级的例如 <div> <div></div> <p><p> </div>
// 当我们读取到开始标签就入栈读取到结束标签就把该ast语法对象出栈,永远保证不出错
const stack =[];
// 父标签
let currentParent;
// 根节点
let root;
function start(tag, attrs) {
let node = createASTElement(tag, attrs)
if(!root) {
root = node
}
// 有父元素 就把正在处理的parent指向父元素, 父元素的children push进当前处理的元素
if(currentParent) {
node.parent = currentParent
currentParent.children.push(node)
}
stack.push(node)
// 把当前元素置为父元素
currentParent = node
}
function createASTElement(tag, attrs) {
return {
tag,
type: ELEMENT_TYPE,
children: [],
attrs,
parent: null
}
}
2、结束标签的开始 的处理
js
// 结束就是 stack的当前处理标签的出栈 以及当前父元素重新获取当前栈的最后一个
function end(tag) {
stack.pop()
currentParent = stack[stack.length -1]
}
3、文本内容的处理
js
function chars(text) {
text = text.replace(/\s/g, '')
text && currentParent.children.push({
type: TEXT_TYPE,
text,
parent: currentParent
})
}
4、完整版
js
// // 识别合法的xml标签
const ncname = '[a-zA-Z_][\\-\\.0-9_a-zA-Z]*';
// // 复用拼接,这在我们项目中完成可以学起来
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// // 匹配注释
// var comment =/^<!--/;
// // 匹配<!DOCTYPE> 声明标签
// var doctype = /^<!DOCTYPE [^>]+>/i;
// // 匹配条件注释
// // var conditionalComment =/^<![/;
// // 匹配开始标签
const startTagOpen = new RegExp(`^<${qnameCapture}`);
// // 匹配结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
// // 匹配单标签
const startTagClose = /^\s*(\/?)>/;
// // 匹配属性,例如 id、class
// 第一分组是key value是 3/4/5 分组
const attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// {{ ... }}
const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
// // 匹配动态属性,例如 v-if、v-else
// var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)[[^=]+][^\s"'<>/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
export function parseHTML(html) { //html 以<开始
const ELEMENT_TYPE = 1
const TEXT_TYPE = 3
const stack =[]
let currentParent ;
let root;
function createASTElement(tag, attrs) {
return {
tag,
type: ELEMENT_TYPE,
children: [],
attrs,
parent: null
}
}
function start(tag, attrs) {
let node = createASTElement(tag, attrs)
if(!root) {
root = node
}
if(currentParent) {
node.parent = currentParent
currentParent.children.push(node)
}
stack.push(node)
currentParent = node
}
function chars(text) {
text = text.replace(/\s/g, '')
text && currentParent.children.push({
type: TEXT_TYPE,
text,
parent: currentParent
})
}
function end(tag) {
stack.pop()
currentParent = stack[stack.length -1]
}
function advance(n) {
html = html.substring(n)
}
function parseStartTag() {
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: []
}
advance(start[0].length)
let attr, end;
// 如果不是开始标签的结束
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length)
match.attrs.push({name: attr[1], value: attr[3] || attr[4] || attr[5] || true})
}
if (end) {
advance(end[0].length)
}
return match
}
// 如果不是开始标签的结束 循环match属性
return false
}
while (html) {
// 首次传进来 <div></div>
let textEnd = html.indexOf('<');
// 如果<是第一个 再去用正则判断是不是起始标签的开始例:<div> 如果是则对标签内容进行处理
if (textEnd === 0) {
const startTagMatch = parseStartTag()
if(startTagMatch) {
start(startTagMatch.tagName, startTagMatch.attrs)
continue
}
// 判断是不结束标签的开始例如</div>
let endTagMacth = html.match(endTag)
if(endTagMacth) {
end(endTagMacth[1])
advance(endTagMacth[0].length)
continue
}
}
// <尖角号不是开始而是在后边判断为文本内容处理 如 123</div> 对123进行处理
if(textEnd > 0) {
let text = html.substring(0,textEnd)
if(text) {
chars(text)
advance(text.length)
}
}
}
return root
}
6、检验成果
以上模板转ast语法就完成了 文章太长了,下一篇 从ast语法树到render函数,再到真实dom,dom diff算法再依赖收集和通知更新写完后再写