如标题,模板编译第二步,获取到ast语法树之后就要考虑生成虚拟dom了,而生成虚拟dom的关键就是就是render函数,最后一步就是虚拟dom转换成真实dom
1、根据ast语法树生成render函数
相信很多同学都写过函数式组件比如jsx,vue的render函数也是一种函数式编程,大概写法是这样
js
render() {
return h('div', { id: 'app' }, [
h('img', {
alt: 'vue.logo',
src: img,
}),
h(HelloWorld, {
msg: 'Welcome to Your Vue.js + TypeScript App',
name: '李四',
age: 15,
sex: '男',
}),
])
},
好我们现在就是要生成这样的render函数
第一步就是照葫芦画瓢,无非就是这个h函数 第一个参数是标签,第二个参数是属性,第三个参数为子组件。 回到compiler/index.js
js
function genChildren(children) {
return children.map(child => gen(child))
}
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 compileToFunction(template) {
// 将template 转换成ast语法书
// 生成render方法 render方法执行后返回结果是 虚拟dom
let ast = parseHTML(template)
// codegen就是生成render函数
let code = codegen(ast)
}
复杂的就在 gen函数和genProps函数,前者对子组件进行处理,后者对标签上的属性进行处理,以下是实现代码
js
// 匹配{{}} 插值表达式的正则
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
function gen(node) {
// 标签类型 递归执行上一步即可
if (node.type === 1) {
return codegen(node)
} else {
// 文本
let text = node.text
// 没匹配到{{}}
if(!defaultTagRE.test(text)) {
// _v是创建出文本的虚拟dom
// _s是对文本内容的处理主要是对文本如果是object类型进行兼容处理
return `_v(${JSON.stringify(text)})`
} else {
//_v(_s(name)+'hello'+_s(name))
let tokens = [];
let match;
// 正则匹配到lastIndex就会往后移,上边我们已经匹配过一次了所以我们想匹配完整需要重置该属性
defaultTagRE.lastIndex = 0
let lastIndex = 0
while (match = defaultTagRE.exec(text)) {
let index = match.index
// 这种情况就是{{name}}123{{age}} 两个插值表达式之间有文本需要把文本放进去
if(index > lastIndex) {
// 将变量中间的文本push进去
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}
tokens.push(`_s(${match[1].trim()})`)
// 匹配到结束需要把lastIndex变为匹配完成的位置
lastIndex = index + match[0].length
}
// 将最后的文本push进去
if(lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
return `_v(${tokens.join('+')})`
}
}
}
genProps 对属性进行处理,这个的目的是把所有的属性以对象的形式陈列出来,注意对style的处理要变成style:{color:'red'}这种形式
js
function genProps(attrs) {
let str = '' //{name, value}
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i]
if (attr.name === 'style') {
// color: red;height:100px => {color: red}
let obj = attr.value.split(';').reduce((pre, cur) => {
let [key, value] = cur.split(':')
key = key.trim()
pre[key] = value
return pre
}, {})
attr.value = obj
}
str += `${attr.name}:${JSON.stringify(attr.value)},`
}
return `{${str.slice(0, -1)}}`
}
以上我们完成了生成render函数的基本操作,但是目前我们生成的是一串字符串,核心其实就是with语法,with语法里边的变量会从传进来的this上获取,这也就是为什么我们写模板的时候不用写this的原因,之后我们只要解决调用时this指向就行
完善compileToFunction函数
js
export function compileToFunction(template) {
// 将template 转换成ast语法书
// 生成render方法 render方法执行后返回结果是 虚拟dom
let ast = parseHTML(template)
let code = codegen(ast)
code = `with(this){return ${code}}`
let render = new Function(code)
return render;
}
先在init.js里把生成的render函数放到options上方便后续放到vue原型上
js
if(template) {
const render = compileToFunction(template)
ops.render = render;
}
还没完_v,_s以及虚拟dom with的this指向也还要搞定
在src下新建lifecycle.js
js
export function initLifeCycle(Vue) {
Vue.prototype._c = function() {
return createElementVNode(this, ...arguments)
}
Vue.prototype._v = function() {
return createTextVNode(this, ...arguments)
}
// 纯文本
Vue.prototype._s = function(value) {
if(typeof value !== 'object') return value
return JSON.stringify(value)
}
// 将option上的render函数放到原型上,并改变this指向,调用的时候调用_render
Vue.prototype._render = function() {
return this.$options.render.call(this)
}
}
我们还剩createElementVNode,createTextVNode两个函数没有实现,为了后续虚拟dom留位置 我们在src下新建vdom文件夹下新建index.js
js
// _c
export function createElementVNode(vm, tag, data, ...children) {
if(data == null) {
data = {}
}
let key = data.key
if(key) {
delete data.key
}
return vnode(vm, tag, key ,data, children)
}
// _v
export function createTextVNode(vm,text) {
return vnode(vm, undefined, undefined, undefined, undefined, text)
}
// ast做的语法层面的转化 描述的是语法本身
// 虚拟dom是描述dom元素,可以增加一些自定义属性
function vnode(vm, tag, key, data, children, text) {
return {
vm,
tag,
key,
data,
children,
text
}
}
我们调用下_render看下效果
成功生成虚拟dom,并触发了name属性的get,此时我们只要将虚拟dom转换成真实dom即可
直接在调试打印虚拟dom那调用函数mountComponent
js
Vue.prototype.$mount = function(el) {
...
mountComponent(vm, el)
}
在lifecycle.js创建并导出mountComponent
js
export function initLifeCycle(Vue) {
Vue.prototype._update =function(vnode) {
const vm = this
const el = vm.$el
// patch 既有初始化的功能 又有更新的功能
vm.$el = patch(el, vnode); // 下次更新取旧el 就是本次的el
}
Vue.prototype._c = function() {
return createElementVNode(this, ...arguments)
}
Vue.prototype._v = function() {
return createTextVNode(this, ...arguments)
}
// 纯文本
Vue.prototype._s = function(value) {
if(typeof value !== 'object') return value
return JSON.stringify(value)
}
Vue.prototype._render = function() {
return this.$options.render.call(this)
}
}
export function mountComponent(vm, el) {
vm.$el = el
// 1.调用render产生虚拟节点 虚拟dom
//vm.$options.render() 虚拟节点
const updateComponent = () => {
vm._update(vm._render())
}
updateComponent()
}
实际是将虚拟dom传给_update,update去调用patch方法生成真实dom,patch有初次渲染生成dom的功能也有更新的功能,以下完善patch方法
vdom/patch.js
js
// 创建真实dom
export function createElm(vnode) {
let {tag, data, children,text} = vnode
if(typeof tag === 'string') {
vnode.el = document.createElement(tag) // 真实节点和虚拟节点对应起来
patchProps(vnode.el, {}, data) // 增加属性
children.forEach(child => {
vnode.el.appendChild(createElm(child))
});
} else {
vnode.el = document.createTextNode(text)
}
return vnode.el
}
// 给真实dom加属性
export function patchProps(el, oldProps = {} ,props = {}) {
for (let key in props) {
if(key === 'style') {
for (const styleName in props.style) {
el.style[styleName] = props.style[styleName]
}
} else {
el.setAttribute(key, props[key])
}
}
}
export function patch(oldVnode, vnode) {
// 初渲染流程
const isRealElement = oldVnode.nodeType // nodeType 原生方法
if(isRealElement) {
const elm = oldVnode //真实元素
const parentElm = elm.parentNode //父元素
let newEle = createElm(vnode)
parentElm.insertBefore(newEle, elm.nextSibling)
parentElm.removeChild(elm)
return newEle
} else {
// dom diff
}
}
以上 下面我们验证结果
模板编译写完了,下一篇实现依赖收集和通知更新