Vue设计与实现:初识渲染器

52 阅读3分钟

总结

  1. 渲染器的作用:把虚拟dom转换成真实dom
  2. 渲染器的原理:递归虚拟dom并用appendChildcreateTextNode来创建真实dom
  3. template模版会被编译器编译成渲染函数(render函数

渲染器

什么叫虚拟dom?

使用js对象来描述dom就是虚拟dom,render函数返回值就是虚拟dom

编写渲染器函数

html代码

 <div @click="() => alert('hello')">click me</div>

将html转成虚拟dom

虚拟dom描述信息

  1. 标签是div
  2. 参数绑定点击事件
  3. 子节点是 click me 文本
const vnode = {
  tag: "div",
  props: {
    onClick: () => alert("hello")
  },
  children: "click me"
}

渲染器函数

  1. renderer传参为虚拟dom、真实dom(挂载点)
  2. 首先根据虚拟dom的标签通过document.createElement创建真实dom
  3. 遍历虚拟dom的属性,判断是否为on开头,如果是就是事件,使用key.substr(2)把key去掉on再使用toLowerCase()把事件转小写,最后使用真实dom.addEventListener绑定vnode.props[key]事件
  4. 判断子节点是类型,文本?组件?
    4.1 要是文本得话使用document.createTextNode(vnode.children)创建文本节点再使用真实dom.appendChild添加
    4.2 要是多个子节点那么子节点就是一个数组,需要递归子节点
  5. 最终container.appendChild(el)绑定真实dom
function renderer(vnode, container) {
  // 使用 vnode.tag 作为标签名称创建 DOM 元素
  const el = document.createElement(vnode.tag)
  // 遍历 vnode.props 将属性、事件添加到 DOM 元素
  for (const key in vnode.props) {
    if (/^on/.test(key)) {
      // 如果 key 以 on 开头,那么说明它是事件
      el.addEventListener(
        key.substr(2).toLowerCase(), // 事件名称 onClick ---> click
        vnode.props[key] // 事件处理函数
      )
    }
  }

  // 处理 children
  if (typeof vnode.children === 'string') {
    // 如果 children 是字符串,说明是元素的文本子节点
    el.appendChild(document.createTextNode(vnode.children))
  } else if (Array.isArray(vnode.children)) {
    // 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
    vnode.children.forEach(child => renderer(child, el))
  }

  // 将元素添加到挂载点下
  container.appendChild(el)
}

将vnode,body节点传入renderer

renderer(vnode, document.body) // body 作为挂载点

页面显示click me image.png 点击触发点击事件 image.png

组件本质

组件就是一组dom元素的封装,可以定义一个函数来代表组件,而函数的返回值就代表组件要渲染的内容

const MyComponent = function () {
  return {
    tag: 'div',
    props: {
      onClick: () => alert('hello')
    },
    children: 'click me'
  }
}

虚拟dom怎么描述组件

可以用虚拟dom的tag属性

const vnode = {
  tag: MyComponent
}

渲染器函数添加判断组件逻辑

现在虚拟dom的tag除了字符串(节点)还有函数(组件),把renderer函数拆分mountElement函数mountComponent函数来处理对应tag

mountElement函数

一开始renderer函数就是处理普通节点,直接复制

function mountElement(vnode, container) {
  // 使用 vnode.tag 作为标签名称创建 DOM 元素
  const el = document.createElement(vnode.tag)
  // 遍历 vnode.props 将属性、事件添加到 DOM 元素
  for (const key in vnode.props) {
    if (/^on/.test(key)) {
      // 如果 key 以 on 开头,那么说明它是事件
      el.addEventListener(
        key.substr(2).toLowerCase(), // 事件名称 onClick ---> click
        vnode.props[key] // 事件处理函数
      )
    }
  }

  // 处理 children
  if (typeof vnode.children === 'string') {
    // 如果 children 是字符串,说明是元素的文本子节点
    el.appendChild(document.createTextNode(vnode.children))
  } else if (Array.isArray(vnode.children)) {
    // 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
    vnode.children.forEach(child => renderer(child, el))
  }

  // 将元素添加到挂载点下
  container.appendChild(el)
}

mountComponent函数

因为组件是函数,需要执行vnode.tag()得到组件的虚拟dom,再传入renderer

function mountComponent(vnode, container) {
  // 调用组件函数,获取组件要渲染的内容(虚拟 DOM)
  const subtree = vnode.tag()
  // 递归调用 renderer 渲染 subtree
  renderer(subtree, container)
}

renderer函数

function renderer(vnode, container) {
  if (typeof vnode.tag === 'string') {
    // 说明 vnode 描述的是标签元素
    mountElement(vnode, container)
  } else if (typeof vnode.tag === 'function') {
    // 说明 vnode 描述的是组件
    mountComponent(vnode, container)
  }
}