Vue设计与实现:渲染器的设计

渲染器与响应系统的结合

给effect传入一个renderer,renderer里面读取了响应式类型的数据,就会使响应式数据跟响应式数据关联起来,当响应式类型数据改变时自动触发renderer查询渲染dom

js 复制代码
function renderer(domString, container) {
  container.innerHTML = domString
}

const count1 = ref(1)
effect(() => {
  renderer(`<h1>${count1.value}</h1>`,document.getElementById('app'))
})

count1.value++

渲染器的基本概念

挂载

createRenderer函数返回render,render函数会以container为挂载点,将vnode渲染为真实DOM并添加到该挂载点下

js 复制代码
function createRenderer() {
  function render(vnode, container) {

  }
  return {
    render
  }
}

const renderer = createRenderer()
// 挂载
renderer.render(oldVNode, document.querySelector('#app'))

更新

如果多次调用render进行更新操作,就需要新vnode跟旧vnode进行比较,提高性能

js 复制代码
const renderer = createRenderer()
// 首次渲染
renderer.render(oldVNode, document.querySelector('#app'))
// 第二次渲染
renderer.render(newVNode, document.querySelector('#app'))

解决思路:

  1. 如果vnode存在就说明有新的vnode就要传入patch进行打补丁(更新)
  2. 如果vnode不存在就说明没有新的vnode并且旧的vnode存在,说明需要把旧的vnode清空
  3. 将新的vnode保存到container的_vnode下,如果多次调用render并且container相同就可以以container._vnode为旧vnode,vnode为新vnode
diff 复制代码
function createRenderer() {
  function render(vnode, container) {
+     // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行打补丁
+    if(vnode) {
+      patch(container._vnode, vnode, container)
+    }else {
+     // 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作
+      if (container._vnode) {
+        // 只需要将 container 内的 DOM 清空即可
+        container.innerHTML = ""
+      }
+    }
+    // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode
+    container._vnode = vnode
  }
  return {
    render
  }
}

自定义渲染器

判断oldN也就是旧的vnode,不存在就意味着挂载就调用mountElement

js 复制代码
function patch(oldN, newN, container) {
  // 如果 oldN 不存在,意味着挂载,则调用 mountElement 函数完成挂载
  if (!oldN) {
    // 挂载
    mountElement(newN, container)
  } else {
    // 暂不操作
  }
}
  1. 根据vnode的type来创建对应的type
  2. 判断vnode的children是什么节点,如果是文本节点就使用textContent设置
  3. 最后使用appendChild添加到container中
js 复制代码
function mountElement(vnode, container) {
  // 创建 DOM 元素
  const el = document.createElement(vnode.type)
  // 处理子节点,如果子节点是字符串,代表元素具有文本节点
  if (typeof vnode.children === "string") {
    // 因此只需要设置元素的 textContent 属性即可
    el.textContent = vnode.children
  }
  // 将元素添加到容器中
  container.appendChild(el)
}

指定自定义的配置对象

因为mountElement 函数内调用了大量createElement、textContent、appendChild等浏览器api,需要将这些api抽离成配置项传入createRenderer,使createRenderer作用域内定义的函数都可以访问,只要传入不同的配置项,就能够完成非浏览器环境下的渲染工作。

diff 复制代码
-function createRenderer() {
+function createRenderer(options) {
  // 通过 options 得到操作 DOM 的 API
+  const { createElement, insert, setElementText } = options
  // 在这个作用域内定义的函数都可以访问那些 API
  function mountElement(vnode, container) {
    // ...
  }
  function patch(n1, n2, container) {
    // ...
  }
  function render(vnode, container) {
    // ...
  }
  return {
    render
  }
}
diff 复制代码
function mountElement(vnode, container) {
  // 调用 createElement 函数创建元素
-  const el = document.createElement(vnode.type)
+  const el = createElement(vnode.type)
  if (typeof vnode.children === 'string') {
    // 调用 setElementText 设置元素的文本节点
-    el.textContent = vnode.children
+    setElementText(el, vnode.children)
  }
-  container.appendChild(el)
  // 调用 insert 函数将元素插入到容器内
+  insert(el, container)
}

使用浏览器api执行

js 复制代码
// 在创建 renderer 时传入配置项
const renderer = createRenderer({
  // 用于创建元素
  createElement(tag) {
    console.log(`创建元素 ${tag}`)
    return document.createElement(tag)
  },  // 用于设置元素的文本节点
  setElementText(el, text) {
    console.log("设置",el,"的文本内容:",text)
    el.textContent = text
  },
  // 用于在给定的 parent 下添加指定元素
  insert(el, parent, anchor = null) {
    console.log("将",el,"添加到",parent,"下")
    parent.insertBefore(el, anchor)
  }
})

const vnode = {
  type: 'h1',
  children: 'hello'
}

const container = document.getElementById("app")
renderer.render(vnode, container)

不使用浏览器api

既可以在浏览器使用也可以在node中使用

js 复制代码
// 在创建 renderer 时传入配置项
const renderer = createRenderer({
  // 用于创建元素
  createElement(tag) {
    console.log(`创建元素 ${tag}`)
    return { tag }
  },  // 用于设置元素的文本节点
  setElementText(el, text) {
    console.log(`设置 ${JSON.stringify(el)} 的文本内容:${text}`)
    el.textContent = text
  },
  // 用于在给定的 parent 下添加指定元素
  insert(el, parent, anchor = null) {
    console.log(`将 ${JSON.stringify(el)} 添加到${JSON.stringify(parent)} 下`)
    parent.children = el
  }
})

const vnode = {
  type: 'h1',
  children: 'hello'
}

const container = { type: 'root' }
renderer.render(vnode, container)
console.log(container)
相关推荐
安冬的码畜日常1 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记2 小时前
【复习】HTML常用标签<table>
前端·html
程序员大金2 小时前
基于SpringBoot+Vue+MySQL的装修公司管理系统
vue.js·spring boot·mysql
丁总学Java2 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele2 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀3 小时前
CSS——属性值计算
前端·css
DOKE3 小时前
VSCode终端:提升命令行使用体验
前端
xgq3 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
用户3157476081353 小时前
前端之路-了解原型和原型链
前端