Vue源码学习之代码实现生成原理及render函数执行准备

1,008 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情

接上文实现模板转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

image.png 接下来我们需要让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

image.png 然后再回到初始化的地方init.js就有了render,当有了render之后就可以进行代码的初渲染。

image.png 那下一步就是用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中进行扩展。

image.png

最后

总结一下vue的核心流程:

  • 创造力响应式数据
  • 将模板转换成ast语法树
  • 将ast语法树转化成render函数
  • 后续每次数据更新可以只执行render函数,无需再次执行ast转化过程
  • render函数会去产生虚拟节点(使用响应式数据)
  • 根据生成的虚拟节点创造真实的DOM

最后就是调用render函数产生虚拟节点变成真实DOM,这个放到下一篇写,持续学习,加油!