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)
相关推荐
「、皓子~25 分钟前
后台管理系统的诞生 - 利用AI 1天完成整个后台管理系统的微服务后端+前端
前端·人工智能·微服务·小程序·go·ai编程·ai写作
就改了27 分钟前
Ajax——在OA系统提升性能的局部刷新
前端·javascript·ajax
凌冰_29 分钟前
Ajax 入门
前端·javascript·ajax
京东零售技术44 分钟前
京东小程序JS API仓颉改造实践
前端
老A技术联盟1 小时前
从小白入门,基于Cursor开发一个前端小程序之Cursor 编程实践与案例分析
前端·小程序
风铃喵游1 小时前
构建引擎: 打造小程序编译器
前端·小程序·架构
sunbyte1 小时前
50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | ThemeClock(主题时钟)
前端·javascript·css·vue.js·前端框架·tailwindcss
小飞悟1 小时前
🎯 什么是模块化?CommonJS 和 ES6 Modules 到底有什么区别?小白也能看懂
前端·javascript·设计
浏览器API调用工程师_Taylor1 小时前
AOP魔法:一招实现登录弹窗的全局拦截与动态处理
前端·javascript·vue.js
FogLetter1 小时前
初识图片懒加载:让网页像"懒人"一样聪明加载
前端·javascript