vue.js 设计与实现 - 框架
2023-04-17
前端Vue

框架范式

视图框架通常有命令式声明式,各有优劣。

命令式重过程,即一步步做:

const div = document.querySelector('#div') // 获取 div
div.innerText = 'hello world' // 赋文本
div.addEventListener('click', () => alert('ok')) // 绑事件

声明式重结果,过程由框架处理:

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

性能

声明式框架之所以简约,乃封装命令式代码于其内。故其性能不优于命令式。因而开发声明式框架,其核心关注点,在于性能优化

虚拟 DOM

声明式更新能耗 = 找差异能耗 + 修改能耗

其中,修改能耗及命令式代码。因而降低找差异能耗,声明式性能即可无限接近命令式。虚拟 DOM 应运而生。

虚拟 DOM 创建页面分两步:

  • 创建 js 对象,用以描述 DOM
  • 递归遍历虚拟 DOM 树,创建真 DOM

渲染器

渲染器用以将虚拟 DOM 转换为真实 DOM

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

/** 渲染器 */
function render(vnode, container) {
  // 创建元素
  const el = document.createElement(vnode.tag)
  // 处理属性
  for (const key in vnode.props) {
    // 以 on 打头,均为事件,注册之
    if (key.startsWith('on')) {
      el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])
    }
  }

  // 处理子元素
  // 子元素为字符串,则追加文本节点
  if (typeof vnode.children === 'string') {
    el.appendChild(document.createTextNode(vnode.children))
  }
  // 为数组,则递归
  else if (Array.isArray(vnode.children)) {
    // 递归
    vnode.children.forEach(child => render(child, el))
  }

  // 挂载
  container.appendChild(el)

}

// 调用
render(vnode, document.querySelector('#app'))

由上述代码可知,渲染器原理极为简单。

组件

组件就是一组 DOM 元素,封装之以复用。函数也是一组代码的封装。因而函数可用来表示组件,其返回值为欲渲染内容:

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

tag: 'div' 表示 <div>,同理, tag: MyComponent 表示组件。

修改渲染函数:

function renderer(vnode, container) {
  // 字符串,表明该 vnode 为普通标签
  if (typeof vnode.tag === 'string') {
    mountElement(vnode, container)
  }
  // 函数,表明是组件
  else if (typeof vnode.tag === 'function') {
    mountComponent(vnode, container)
  }
}

/** 挂载普通元素 */
function mountElement(vnode, container) {
  // 创建元素
  const el = document.createElement(vnode.tag)
  // 遍历属性
  for (const key in vnode.props) {
    if (key.startsWith('on')) {
      // 事件
      el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])
    }
  }

  // 处理子元素
  if (typeof vnode.children === 'string') {
    el.appendChild(document.createTextNode(vnode.children))
  }
  else if (Array.isArray(vnode.children)) {
    vnode.children.forEach(child => renderer(child, el))
  }

  // 挂载
  container.appendChild(el)
}

/** 挂载组件 */
function mountComponent(vnode, container) {
  // 调用组件函数,获取其内容,即虚拟 DOM
  const subtree = vnode.tag()
  // 递归调用 renderer
  renderer(subtree, container)
}

编译器

编译器用以模板转渲染函数,如:

<div @click="handle">
  点击
</div>

转化为:

function render() {
  return {
    tag: 'div',
    props: {
      onClick: handler
    },
    children: '点击'
  }
}