Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

从零写一个 Vue(四)虚拟 DOM #9

Open
buppt opened this issue Jun 19, 2020 · 0 comments
Open

从零写一个 Vue(四)虚拟 DOM #9

buppt opened this issue Jun 19, 2020 · 0 comments

Comments

@buppt
Copy link
Owner

buppt commented Jun 19, 2020

写在前面

本篇是从零实现 vue2 系列第四篇,为 YourVue 添加虚拟 dom。

之前在第一篇实现 vue 流程的时候,将模版解析成 ast,直接生成了真实 dom。这并不是 vue 的实现方式,真正的实现方式是将 parse(template) 生成的 ast 通过 gencode 生成 render 函数,然后执行 render 函数生成 VNode,构建虚拟 dom 树,然后通过虚拟 dom 树创建真实 dom。

文章会最先更新在公众号:BUPPT。代码仓库:https://github.com/buppt/YourVue

正文

VNode

首先我们先定义虚拟 dom 的节点 VNode,存储的内容无非就是 tag、属性、子节点、文本内容等。

export class VNode{
  constructor(tag, data={}, children=[], text='', elm, context){
    this.tag=tag;
    this.props=data ;
    this.children=children;
    this.text=text
    this.key = data && data.key
    var count = 0;
    children.forEach(child => {
      if(child instanceof VNode){
        count += child.count;
      }
      count++;
    });
    this.count = count;
  }
}

render

再定义四个基本的 render 函数,使用和 vue 相同的 _c、_s 命名方式,每一个都很简单,就是分别创建不同的 VNode。

  • _c 创建正常的带有 tag 的 VNode
  • _v 创建文本节点对应的 VNode
  • _s 就是将变量转成字符串的 toString 函数
  • _e 用来创建一个空的 VNode 节点
export default class YourVue{
  _init(options){
      initRender(this)
  }
}
export function initRender(vm){
  vm._c = createElement
  vm._v = createTextVNode
  vm._s = toString
  vm._e = createEmptyVNode
}
function createElement (tag, data={}, children=[]){
  children = simpleNormalizeChildren(children)
  return new VNode(tag, data, children, undefined, undefined)
}

export function createTextVNode (val) {
  return new VNode(undefined, undefined, undefined, String(val))
}

export function toString (val) {
  return val == null
    ? ''
    : Array.isArray(val)
      ? JSON.stringify(val, null, 2)
      : String(val)
}
export function createEmptyVNode (text) {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

export function simpleNormalizeChildren (children) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}

这其中 createElement 时需要把 children 展开一层,如果 children 中某个子元素是数组,就把子元素数组中的元素 concat 到 children 上。

因为 children 中都应该是 VNode,像后面会实现的 v-for 和 slot 都向 children 中添加了数组元素,其实这些数组元素中的 VNode 和 children 中的 VNode 是并列的 dom 元素,所以将子元素只展开了一层。

gencode

gencode 的作用就是生成将 parse 生成的 ast,通过上面 render 函数生成 VNode 的字符串代码。

通过 gencode 生成的代码需要结合前面的 render 函数的参数来阅读,比如 _c 第一个参数是 tag,第二个参数是元素属性,第三个是子节点。

_c('h4',{attrs:{"style":"color: red"}},[
  _v(_s(message))]),
  _v(" "),
  _c('button',{on:{click:decCount}},[_v("decCount")
])

生成的代码也是类似 dom 树的嵌套结构,最外层是一个 node.type === 1 元素节点,使用 _c 函数,将元素属性通过第二个参数传入 VNode,其余元素按结构保存到第三个 children 参数中。

如果 node.type === 3 说明是纯文本节点,直接使用JSON.stringify(node.text)

如果 node.type === 2 说明是带有变量的文本节点,使用 parse 生成的 node.expression

export function templateToCode(template){
  const ast = parse(template, {})
  return generate(ast)
}
export function generate(ast){
  const code = ast ? genElement(ast) : '_c("div")'
  return `with(this){return ${code}}`
}

function genElement(el){
  let code
  let data = genData(el)
  const children = el.inlineTemplate ? null : genChildren(el, true)
  code = `_c('${el.tag}'${
    data ? `,${data}` : '' // data
  }${
    children ? `,${children}` : '' // children
  })`
  return code
}

export function genChildren (el){
  const children = el.children
  if (children.length) {
    const el = children[0]
    return `[${children.map(c => genNode(c)).join(',')}]`
  }
}
function genNode (node) {
  if (node.type === 1) {
    return genElement(node)
  } else if (node.type === 3 && node.isComment) {
    return `_e(${JSON.stringify(node.text)})`
  } else {
    return `_v(${node.type === 2
      ? node.expression
      :JSON.stringify(node.text)
    })`
  }
}

function genData(el){
  let data = '{'
  if (el.attrs) {
    data += `attrs:${genProps(el.attrs)},`
  }
  if (el.props) {
    data += `domProps:${genProps(el.props)},`
  }
  if (el.events) {
    data += `on:${genHandlers(el.events)},`
  }
  data = data.replace(/,$/, '') + '}'
  return data
}

function genHandlers(events){
  let res = '{'
  for(let key in events){
    res += key + ':' + events[key].value
  }
  res += '}'
  return res
}

转成可执行函数

可以看到最后生成的字符串代码是 with(this){return ${code}},那么怎样将它变成可执行的函数呢?就是通过 new Function 来实现啦。

export default class YourVue{
    $mount(){
        const options = this.$options
        if (!options.render) {
            let template = options.template
            if (template) {
                const code = templateToCode(template)
                const render = new Function(code).bind(this)
                options.render = render
            }
        }
        const vm = this
        new Watcher(vm, vm.update.bind(vm), noop)
    }
}

这样我们就把 template 转换成可以生成 VNode 的 render 函数挂到 YourVue 实例的 options 属性上了,这篇内容信息量已经非常多了,如何将 render 函数转换成真实 dom 呢?请看下篇文章。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant