数据劫持的概念
当我去操作数据的时候,知道他改了这个数据。
获取数据模板
if(vm.$options.el){
// 如果有数据的话,那么就把用户的数据挂载到模板上。
vm.$mount(vm.$options.el)
}
// 这个方法存在是为了如果用户没有$mount那么我们就允许用户手动指定el上去
Vue.prototype.$mount=function(el){
el=document.querySelector(el)
}
// 上述这个方法处理过之后,我们就能拿到这个el了。
那么我们就需要考虑一个问题。如果说数据改变之后,我们html上的数据变化了,我们怎么通知vue进行改变
解决方法是我们更新html上挂载到vue上的节点数据的时候,我们可以通知vue进行改变,我们也可以使用虚拟dom。将真实dom传入,通过渲染函数将其变成虚拟dom。然后diff算法来模拟数据改变
将所有的节点数据都放在虚拟节点上,最后我们再产生真实节点进行更新,这样我们的性能会更高一些。
但是这里要注意一个点,那就是用户可能传入options的时候,里面带了一个render函数,这个函数代表用户想要返回他自己定义的虚拟节点。那么我们就渲染这个自己定义的函数,这个就是render的内容了。我们后面再讲。
Vue.prototype.$mount=function(el){
let options=vm.$options
if(vm.$options.render){
let template=options.template
// 如果说用户也没有传入template,那么我们就找el上面的outHTML进行打印
if(!template&&el){
template=el.outerHTML
let render=complieToFunction(template)
options.render=render
}
}
}
本质上我们要做的事情就是将template变成render函数
上面我们引入了一个方法叫做,compilerToFunction。这里就引出了Vue2渲染的一个关键类库,叫做compile库。通过这个库我们可以将真实节点转化为虚拟节点。方便我们的Vue去做处理。
我们今天从零开始手写一个Vue2的模板渲染器。可能和Vue2官方的不同,不过这个更加体现了创意。可能符合了活动规范,也算是我眼中的Vue2把。毕竟吃饭的东西。还是要把每个细节都把握好的。
词法分析器
下方的变量存放了用来解析模板的正则,后面我们将慢慢将这些模板都给他用上去
const ncname =`[a-zA-Z_ ][\\-\1.0-9 a-zA-Z]*`; // 标签名
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+I'([^']*)'+|([^\s"'=<> ]+)))?/;
const startTagClose = /^\s*(\/?)>/;
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
const qnameCapture =`((?:${ncname}\\:)?${ncname})`; //<aa:xxx></aa:xxx>
const startTagOpen = new RegExp(`^<${ qnameCapture}`);
const endTag = new RegExp(`<\\/$ {qnameCapture}[^>]*>`);
Vue并不是逐词解析的,他是用正则表达式来解析的,用来匹配各种各样的标签属性,这个模板的标签名,闭合标签。如果正常来写的话应该用状态机来逐词解析。然后引入状态机的概念来一个一个的解析。
- ncname: 标签名
- qnameCapture: 匹配特殊的标签 aa:xxx
- startTagOpen: 匹配开始标签
- endTag: 匹配闭合标签
- attribute:匹配标签上传入的属性
- startTagClose:匹配标签关闭
- defaultTagRE:匹配马斯塔奇语法 {{aaa}}
我们拿到了template之后,我们需要解析我们的HTML,然后HTML解析成ast语法树
export function compileToFunction(template){
// 解析开始标签
function start(){
}
// 解析结束标签是谁
function end(){
}
// 解析文本内容
function chars(){
}
}
这里我们方法封装一下
专门解析ast的方法
// 解析开始标签
function start(){
}
// 解析结束标签是谁
function end(){
}
// 解析文本内容
function chars(){
}
function parseHTML(html){ //html: <div>123123123</div>
while(html){ // 看要解析的内容是否存在,存在就不停的解析。
let textEnd=html.indexOf('<') // 看看是不是尖角号
if(textEnd==0){
const startTagMatch=parseStartTag() // 解析开始标签,但是可能解析不出来
if(startTagMatch){
}
const endTagMatch=parseEndTag() // 解析结束标签,如果说一个<符号所在的标签不是开始符号就是结束符号
if(startTagMatch){
}
}
}
}
export function compileToFunction(template){
parseHTML(template)
}
上面将一个标签传进去之后,我们用来解析开始标签和结束标签。下面介绍具体匹配开始和结束标签的方法
function parseStartTag(html){
const start=html.match(startTagOpen)
if(start){
const match={
tagName: start[1],
attrs: []
}
}
return false // 不是开始标签就直接跳过
}
那么我们现在里面判断了两种情况,如果说我们拿到了标签,那么我们就判断它是不是一个开始标签。如果不是函数标签的话呢,我们就直接返回false,走匹配文本的逻辑。如果是的话呢,我们就想办法拿到开始标签里面的属性。并且我们每次匹配将开始标签都给他删除掉,那么我们就需要写一个步进器:
function advance(len){
html=html.substring(len) // 此处的html是parseHtml中的html
}
function parseStartTag(){
const start=html.match(startTagOpen)
if(start){
const match={
tagName: start[1],
attrs: []
}
advance(html.length)
}
return false // 不是开始标签就直接跳过
}
我们标签删除完了,我们就需要开始匹配结束标识。将一个标签里面存在的所有属性给他提取出来。
function advance(len){
html=html.substring(len) // 此处的html是parseHtml中的html
}
function parseStartTag(){
const start=html.match(startTagOpen)
if(start){
// 用来记录处理标签属性和名称的空间
const match={
tagName: start[1],
attrs: []
}
advance(html.length)
let end=html.match(startTagClose)
let attr=html.match(attribute)
while(!end&&!attr){
// 下方的3,4,5标识匹配标签和哪一个reg分组匹配
match.attrs.push({name:attr[1],value:attr[3]||attr[4]||attr[5]})
advance(attr[0].length) // 匹配到属性之后我们要将匹配过后的属性给他删除
}
if(end){
advance(end.length) // 如果说end还有值的话,我们将结束标签也删除完
}
return match
}
return false // 不是开始标签就直接跳过
}
这里我们就可以把标签的名称和属性给他解析出来了。
标签解析完了,我们回到最开始的地方,现在我们已经知道它是一个开始标签了,我们就往后面走回到我们最开始遍历每个字符串的地方
while(html){
let textEnd=html.indexOf('<');
if(textEnd==0){
const startTagMatch=parseStartTag()
if(startTagMatch){ // 第一次解析完了直接退出循环
start(startTagMatch.tagName,startTagMatch.attrs) // 解析开始标签读取到的属性
break
}
}
// 遇到文本了
let text // 123123</div>
if(textEnd>0){
text=html.substring(0,textEnd) //123123
}
if(text){
// 处理文本
chars(text)
advance(text.length) // 处理完文本之后删除文本
break; // 告诉状态机文本也处理好了,处理结束标签去
}
}
匹配闭合标签
while(html){
let textEnd=html.indexOf('<');
if(textEnd==0){
const startTagMatch=parseStartTag()
if(startTagMatch){ // 第一次解析完了直接退出循环
start(startTagMatch.tagName,startTagMatch.attrs) // 解析开始标签读取到的属性
break
}
const endTagMatch=html.match(endTag)
if(endTagMatch){
end(endTagMatch[1])
advance(endTagMatch[0].length)
}
}
// 遇到文本了
let text // 123123</div>
if(textEnd>0){
text=html.substring(0,textEnd) //123123
}
if(text){
// 处理文本
chars(text)
advance(text.length) // 处理完文本之后删除文本
// 告诉状态机文本也处理好了,处理结束标签去
}
}
现在我们的开始,结束,内容都已经能够正常拿出来了,下篇文章我们研究vue拿到这些标签和属性之后它会干些什么,它是怎么通过这些东西生成虚拟节点的
我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!