Vue源码学习之代码实现生成原理及render函数执行准备
接上文实现模板转ast语法树之后,我们就需要将ast语法树在转换成我们的render()
函数。
第一步
在我们有了这样一棵语法树之后,我们需要将这棵树进行代码生成,生成如下格式:
// 生成树用的的DOM
<div id="app" style="background:pink;color:aqua">
<div style="color:red">
{{name}} hello {{age}}
</div>
<span> word </span>
</div>
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.stringify
let tokens = []
let match
// 用了exec并且正则中有g,他就从lastindex开始向后寻找,所以每次运行需要先重置一下lastindex
defaultTagRE.lastIndex = 0
let lastIndex = 0
while (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
接下来我们需要让
code
能运行,就需要通过new Function
根据代码生成函数,但是取值会有问题,应该要当前的vm
上去取name和age,所以就可以用with
,当拿到如下函数的时候,我们就可以通过.call(vm)
去vm
上取变量了。
// src/compiler/index
export function compileToFcuntion(template) {
// 1.将template转换为ast语法树
let ast = parseHTML(template)
// 2.生成render方法 render方法执行后的结果就是虚拟DOM
let code = codegen(ast)
code = `with(this){return ${code}}`
let render = new Function(code) // 根据代码生成render函数
return render
}
注:所有的模板引擎实现的原理都是 with + new Function
然后再回到初始化的地方
init.js
就有了render,当有了render之后就可以进行代码的初渲染。
那下一步就是用render方法进行组件的挂载,那就写一个方法
mountCompontent
挂载实例vm
,挂载到el实例,将这个方法单独放在src/lifecycle.js
// 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方法产生虚拟节点 虚拟DOM
vm._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的核心流程:
- 创造力响应式数据
- 将模板转换成ast语法树
- 将ast语法树转化成render函数
- 后续每次数据更新可以只执行render函数,无需再次执行ast转化过程
- render函数会去产生虚拟节点(使用响应式数据)
- 根据生成的虚拟节点创造真实的DOM
最后就是调用render函数产生虚拟节点变成真实DOM,这个放到下一篇写,持续学习,加油!
转载自:https://juejin.cn/post/7149095305869262878