vue2源码学习--02-2模板编译(从ast语法树到虚拟dom再到真实dom)

如标题,模板编译第二步,获取到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
    }
}

以上 下面我们验证结果

模板编译写完了,下一篇实现依赖收集和通知更新

相关推荐
Xy9102 分钟前
开发者视角:App Trace 一键拉起(Deep Linking)技术详解
java·前端·后端
lalalalalalalala5 分钟前
开箱即用的 Vue3 无限平滑滚动组件
前端·vue.js
前端Hardy5 分钟前
8个你必须掌握的「Vue」实用技巧
前端·javascript·vue.js
snakeshe10107 分钟前
深入理解 React 中 useEffect 的 cleanUp 机制
前端
星月日9 分钟前
深拷贝还在用lodash吗?来试试原装的structuredClone()吧!
前端·javascript
爱学习的茄子10 分钟前
JavaScript闭包实战:解析节流函数的精妙实现 🚀
前端·javascript·面试
浅忆无痕11 分钟前
Flutter抓包
前端·flutter
mdpmw11 分钟前
从区块链基础到DApp开发
前端·web3
东倒西歪小田螺12 分钟前
一个树状结构的参数需求
前端
凌晨两点的菜鸡12 分钟前
前端部署-docker
前端·docker