翼度科技»论坛 编程开发 JavaScript 查看内容

一文详解Vue中渲染器的简单实现

9

主题

9

帖子

27

积分

新手上路

Rank: 1

积分
27
一、渲染器

渲染器用于完成渲染操作,比如在浏览器平台上渲染器可以将虚拟DOM转换为真实DOM。

二、一个简单例子理解Vue中渲染器的工作过程
  1. const { effect, ref } = VueReactivity

  2. function renderer(domString, container) {
  3.   container.innerHTML = domString
  4. }

  5. const count = ref(1)

  6. effect(() => {
  7.   renderer(`<h1>${count.value}</h1>`, document.getElementById('app'))
  8. })

  9. count.value++
复制代码
我们通过
  1. @vue/reactivity
复制代码
引用vue的响应式API,使用其中的
  1. effect
复制代码
(副作用函数)和
  1. ref
复制代码
(生成响应式数据)使得
  1. count
复制代码
  1. renderer
复制代码
建立联系,这样当
  1. count
复制代码
变化时,会调用
  1. renderer
复制代码
处理渲染,将
  1. h1
复制代码
标签挂载到id为
  1. app
复制代码
的元素上。这就是一个简易的渲染器实现。

三、渲染器涉及的几种操作: 挂载、更新、卸载

假设我们已经有了一个渲染器
  1. renderer
复制代码
  1. const renderer = createRenderer()
复制代码
有一个用于描述元素节点的数据
  1. vnode
复制代码
:类似下面的结构
  1. // type为类型,children为子节点
  2. const vnode = {
  3.     type: 'h1',
  4.     children: 'hello'
  5. }
复制代码
可以看到这是一个内部有hello文本的
  1. <h1>
复制代码
节点
那么在渲染器实际工作过程中会有如下的几种情况:
  1. renderer.render(vnode, document.querySelector('#app'))
复制代码
此时涉及到的操作是挂载,只需要将vnode变为DOM元素放在app元素内部即可
  1. renderer.render(newVNode, document.querySelector('#app'))
复制代码
由于已经有旧的节点存在,所以不能简单的直接挂载,需要对新旧节点进行对比,找到需要更新的部分再改变,此时的操作就是更新,为了处理更新,渲染器中需要有对应的逻辑。
  1. renderer.render(null, document.querySelector('#app'))
复制代码
新的节点为null则意味这渲染器需要清空
  1. app
复制代码
元素内的内容,此时涉及的操作是卸载
根据对以上三种情况的处理,可以将渲染器写为:
  1. function createRenderer() {

  2.   function patch(n1, n2, container) {

  3.   }

  4.   function render(vnode, container) {
  5.     if (vnode) {
  6.       // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数进行打补丁
  7.       patch(container._vnode, vnode, container)
  8.     } else {
  9.       if (container._vnode) {
  10.         // 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作
  11.         // 只需要将 container 内的 DOM 清空即可
  12.         container.innerHTML = ''
  13.       }
  14.     }
  15.     // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode
  16.     container._vnode = vnode
  17.   }
  18.   
  19.   return {
  20.     render
  21.   }
  22. }
复制代码
使用createRenderer重复上面三次渲染,分析过程:
  1. // 首次渲染
  2. renderer.render(vnode, document.querySelector('#app'))

  3. // 第二次渲染
  4. renderer.render(newVNode, document.querySelector('#app'))

  5. //第三次渲染
  6. renderer.render(null, document.querySelector('#app'))
复制代码

  • 首次渲染:
    1. vnode
    复制代码
    被渲染,并存在
    1. container._vnode
    复制代码
    中;
  • 第二次渲染: 新旧
    1. vnode
    复制代码
    均存在,需要在
    1. patch
    复制代码
    函数中进行更新;
  • 第三次渲染: 新的
    1. vnode
    复制代码
    1. null
    复制代码
    判断旧节点
    1. container._vnode
    复制代码
    是否存在,若旧的
    1. vnode
    复制代码
    存在,则处理为卸载;

四、如何实现一个与平台无关的渲染器:

一个通用的渲染器应该是是与平台无关的,即渲染器的渲染功能不能只在某一个平台中有效。 为了实现这个目标,我们首先来实现一个基于浏览器平台的渲染器,观察在哪些步骤中使用到了和浏览器相关的API,之后可以考虑将这些API抽离,作为配置项,这样就可以实现一个通用的渲染器。

1.实现一个依赖浏览器API的渲染器

以之前的的
  1. vnode
复制代码
为例:
  1. const vnode = {
  2.     type: 'h1',
  3.     children: 'hello'
  4. }
复制代码

    1. type
    复制代码
    为标签类型、
    1. children
    复制代码
    为子元素。
    在以上实现的基础上我们首先完善
    1. patch
    复制代码
    函数,用于处理节点的挂载和更新情况
  1. patch(container._vnode, vnode, container)
复制代码
通过之前处理的代码,可以看到,
  1. patch
复制代码
函数会接受三个参数,分别是 旧的节点、新的节点、以及容器, 我们在其中对旧节点参数进行判断,从而处理不同的操作:目前只分析挂载的情况:
  1.   function patch(n1, n2, container) {
  2.     if (!n1) {
  3.       mountElement(n2, container)
  4.     } else {
  5.       //
  6.     }
  7.   }
复制代码
如上代码所示: 当
  1. n1
复制代码
即旧节点不存在时证明是挂载操作,则直接调用
  1. mountElement
复制代码
对新的节点进行挂载处理
  1.   function mountElement(vnode, container) {
  2.     const el = createElement(vnode.type)
  3.     if (typeof vnode.children === 'string') {
  4.       el.textContent = vnode.children
  5.     }
  6.     container.appendChild(el)
  7.   }
复制代码
  1. mountElement
复制代码
中我们对挂载的逻辑进行了简单的处理:
1. 使用
  1. createElement
复制代码
根据vnode中的
  1. type
复制代码
的值去创建对应的DOM类型:
  1. vnode.type='h1'
复制代码
则el为
  1. <h1></h1>
复制代码
2. 对vnode.children进行判断: 如果
  1. vnode.children
复制代码
为字符串类型则证明子节点是一个文本节点,直接使用元素的
  1. textContent
复制代码
属性设置元素。
3. 调用appendChild将处理好的元素添加到目标容器中。由此挂载完成。

2.实现一个通用的渲染器

以上实现的渲染器对于浏览器的API是有依赖的,其中的
  1. appendChild
复制代码
  1. createElement
复制代码
  1. textContent
复制代码
都是需要浏览器环境才能执行的API,如果要让渲染器能够通用,就需要去除对这些API的依赖。
解决办法:将这些API作为配置项传入渲染器创建函数,这样我们可以自己定义
  1. createRenderer
复制代码
使用哪些API去执行渲染操作,从而使得渲染器的工作不再依赖于某一平台。
  1. const renderer2 = createRenderer({
  2.   //创建元素
  3.   createElement(tag) {
  4.     return { tag }
  5.   },
  6.   //设置元素文本内容
  7.   setElementText(el, text) {
  8.     console.log(`设置 ${JSON.stringify(el)} 的文本内容:${text}`)
  9.     el.text = text
  10.   },
  11.   //将元素插入目标节点
  12.   insert(el, parent, anchor = null) {
  13.     parent.children = el
  14.   }
  15. })
复制代码
我们将创建元素的逻辑和API放入
  1. createElement
复制代码
中,将设置文本内容的API放入
  1. setElementText
复制代码
中, 将元素挂载到容器的API过程放入
  1. insert
复制代码
中。 再使用传入的配置去调用
  1. mountElement
复制代码
  1.   function mountElement(vnode, container) {
  2.     const el = createElement(vnode.type)
  3.     if (typeof vnode.children === 'string') {
  4.       setElementText(el, vnode.children)
  5.     }
  6.     insert(el, container)
  7.   }
复制代码
这样我们可以通过传入的
  1. createRenderer
复制代码
函数的
  1. options去
复制代码
配置,不同平台中使用什么样的方法去执行元素的创建、元素内容的处理以及元素挂载等操作。
由此整个渲染函数如下:
  1. function createRenderer(options) {  const {    createElement,    insert,    setElementText  } = options  function mountElement(vnode, container) {
  2.     const el = createElement(vnode.type)
  3.     if (typeof vnode.children === 'string') {
  4.       setElementText(el, vnode.children)
  5.     }
  6.     insert(el, container)
  7.   }  function patch(n1, n2, container) {
  8.     if (!n1) {
  9.       mountElement(n2, container)
  10.     } else {
  11.       //
  12.     }
  13.   }  function render(vnode, container) {                if (vnode) {      // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数进行打补丁      patch(container._vnode, vnode, container)    } else {      if (container._vnode) {        // 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作        // 只需要将 container 内的 DOM 清空即可        container.innerHTML = ''      }    }    // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode    container._vnode = vnode  }    return {    render  }}
复制代码
由此一个简单的渲染器已经实现,但在实际情况下元素的挂载和更新还有更多的细节现需要处理,如元素上的属性、class、事件如何被正确的挂载,如何提升渲染器更新的效率(diff算法)等,这些将在后续文章中进行讨论。
以上就是一文详解Vue中渲染器的简单实现的详细内容,更多关于Vue实现渲染器的资料请关注脚本之家其它相关文章!

来源:https://www.jb51.net/javascript/321008lur.htm
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

举报 回复 使用道具