在我们有了这样一棵语法树之后,我们需要将这棵树进行代码生成,生成如下格式:
// 生成树用的的DOM {{name}} hello {{age}} word
render() {return _c('div', { id: 'app' }, _c('div', { style: { color: 'red' } }, _v(_s(name) + 'hello'),_c('span', null, _v(_s(age) + 'hello'))))
}
这样我们就调用codegen()
方法来生成对应的代码,核心思想就是字符串拼接,直接上代码:
// src/compiler/index
import { parseHTML } from "./parse";
// 生成孩子
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // 匹配到的内容就是表达式的变量
function gen(node) {// 判断是文本还是元素if (node.type === 1) {// 为1为元素return codegen(node)} else {// 文本 有可能是{{name}}hello 或者只有{{name}}等各种格式let text = node.text.trim()if (!defaultTagRE.test(text)) {return `_v(${JSON.stringify(text)})`} else {// _v( _s(name)+'helool'+_s(name)) s是JSON.stringifylet tokens = []let match// 用了exec并且正则中有g,他就从lastindex开始向后寻找,所以每次运行需要先重置一下lastindexdefaultTagRE.lastIndex = 0let lastIndex = 0while (match = defaultTagRE.exec(text)) {let index = match.index //匹配的位置 {{name}} hello {{name}}tokens.push(`_s(${match[1].trim()})`)if (index > lastIndex) {tokens.push(JSON.stringify(text.slice(lastIndex, index)))}lastIndex = index = match[0].length}if (lastIndex < text.length) {tokens.push(JSON.stringify(text.slice(lastIndex)))}return `_v(${tokens.join('+')})`}}
}
// 处理孩子
function genChildren(children) {if (children) {return children.map(child => gen(child)).join(',')}
}
// 处理属性
function genProps(attrs) {let str = '' //{name,value}for (let i = 0; i < attrs.length; i++) {let attr = attrs[i]if (attr.name === 'style') {// background:pink => {background:pink}let obj = {}attr.value.split(';').forEach(item => { // qs库或者正则表达式也可以处理let [key, value] = item.split(':')obj[key] = value});attr.value = obj}str += `${attr.name}:${JSON.stringify(attr.value)},` // a:b,c:d,}return `{${str.slice(0, -1)}}`
}
// 代码生成
function codegen(ast) {let children = genChildren(ast.children)let code = `_c('${ast.tag}',${ast.attrs.length > 0 ? genProps(ast.attrs) : 'null'}${ast.children.length ? `,${children}` : ''})`return code
}
export function compileToFcuntion(template) {......
}
通过第一步的操作我们就成功将ast语法树拼接成了字符串代码code
// src/compiler/index
export function compileToFcuntion(template) {// 1.将template转换为ast语法树let ast = parseHTML(template)// 2.生成render方法 render方法执行后的结果就是虚拟DOMlet code = codegen(ast)code = `with(this){return ${code}}`let render = new Function(code) // 根据代码生成render函数return render
}
注:所有的模板引擎实现的原理都是 with + new Function
// init.js
Vue.prototype.$mount = function (el) { ...... mountCompontent(vm,el) // 组建的挂载
}
// src/index.js
import { initLifeCycle } from "./lifecycle";
......
initLifeCycle(Vue)
......
// src/lifecycle.js
xport function initLifeCycle(Vue) {Vue.prototype._update = function () {console.log('update');}Vue.prototype._render = function () {console.log('render');}
}
export function mountCompontent(vm, el) {// 1.调用render方法产生虚拟节点 虚拟DOMvm._update(vm._render())// vm._render() // vm.$options.render()虚拟节点// vm._update(vm._render()) 把虚拟节点变成真实节点// 2. 根据虚拟DOM产生真实DOM// 3. 插入到el元素中
}
在mountCompontent()
方法中第一步就是调用render
方法产生虚拟节点,第二步根据虚拟DOM产生真实DOM,第三步插入到el元素中。如何去做源码中用了两个方法vm._render()
执行返回虚拟节点和vm._update()
执行后将虚拟节点变为真实DOM,那接下来就扩展这两个方法,需要在src/index.js
中进行扩展。
总结一下vue的核心流程:
最后就是调用render函数产生虚拟节点变成真实DOM,这个放到下一篇写,持续学习,加油!
整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。
有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享
部分文档展示:
文章篇幅有限,后面的内容就不一一展示了
有需要的小伙伴,可以点下方卡片免费领取