VueJS:渲染器的设计

163 阅读5分钟

第七章:渲染器的设计

createRenderer ---创建---> 渲染器(renderer) ---调用render---> 挂载 ---调用render---> 更新 ---调用render---> 卸载

createRenderer - 创建渲染器

  • render - newVNode container
function render(vNode, container) {
  if (vNode) {
      patch(container._vNode, vNode, container)
  } else {
      if (container._vNode) {
          // 调用unmount移除
          unmount(container._vNode)
      }
  }
  container._vNode = vNode
}
  • patch - 比对新旧虚拟DOM,更新真实DOM
function patch (oldVNode, newVNode, container) {
  // 如果vNode1不存在,则意味着挂载,调用mountElement完成挂载
  if (!oldVNode) {
    mountElement(newVNode, container)
  } else {
    // vNode1存在,意味着打补丁
  }
}
  • patchProps - 解析虚拟DOM中的props
patchProps(el, key, preValue, nextValue) {
  if (key === 'class') {
    el.className = nextValue || ''
  } else if (shouldSetAsProps(el. key, nextValue)) {
    const type = typeof el[key]
    if (type === 'boolean' && nextValue === '') {
      el[key] = true
    } else {
      el[key] = nextValue
    }
  } else {
    el.setAttribute(key, nextValue)
  }
}
  • mountElement - 将虚拟DOM转为真是DOM并挂载到 container 上
function mountElement(vNode, container) {
  const el = vNode.el = createElement(vNode.type)
  if (typeof vNode.children === 'string') {
      setElementText(el, vNode.children)
  } else if (Array.isArray(vNode.children)) {
      vNode.children.forEach(child => {
          patch(null, child, el)
      })
  }

  if (vNode.props) {
      for (const key in vNode.props) {
          patchProps(el, key, null, vNode.props[key])
      }
  }
  insert(el, container)
}
  • unmount - 移除元素/组件
function unmount(vNode) {
  // 获取虚拟节点对应真实DOM的父节点
  const parent = vNode.el.parentNode
  if (parent) {
      // 移除虚拟节点对应的真实DOM
      parent.removeChild(vNode.el)
  }
}

7.1:渲染器与响应系统的结合

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

副作用函数内调用renderer函数执行渲染。副作用函数执行完毕后,会与响应式数据建立响应联系。当修改 count.value的值时,副作用函数会重新执行,完成重新渲染。

7。2:渲染器基本概念

  • 渲染器的作用时把虚拟DOM渲染为特定平台上的真实元素。在浏览器上,渲染器会把虚拟DOM渲染为真实DOM元素。
  • 渲染器将虚拟DOM节点渲染为真实DOM节点的过程叫做挂载,这就意味着在mounted钩子中可以访问真实DOM元素。
  • 渲染器通常需要接收一个挂载点作为参数,用来指定具体的挂载位置。渲染器会把该DOM元素作为容器,并把内容渲染到其中。
  • 渲染器的内容非常广泛,用来把vNode渲染为真实DOM的render函数只是其中一部分。

渲染器会使用newVNode与上一次渲染的oldVNode进行比较,视图找到并更新变更点。这个过程叫做打补丁。 但实际上,挂载动作本身也可以看作一种特殊的打补丁,它的特殊之处在于oldVNode不存在。(不用太纠结挂载/打补丁两个概念)

function createRenderer () {
  function render (vNode, container) {
    if (vNode) {
        // 新vNode存在,将其与vNode一起传递给 patch 函数,进行打补丁
        patch(container._vNode, vNode, container)
    } else {
        if (container._vNode) {
            // 旧vNode存在,且新vNode不存在,说明时 卸载 操作
            // 只需要将 container 内的DOM清空即可
            container.innerHTML = ''
        }
    }
    // 把 vNode 存储到 container._vNode下,即后续渲染中的旧 vNode
    container._vNode = vNode
  }
  return {
    render
  }
}

结合上面和下面的代码来分析执行流程,从而理解render函数的实现思路。

const renderer = createRenderer()
const app = document.querySelector('#app')
// 第一次渲染
renderer.render(vNode1, app)
// 第二次渲染
renderer.render(vNode2, app)
// 第三次渲染
renderer.render(null, app)

首次渲染时。渲染器会将 vNode1 渲染为真实DOM。渲染完成后,vNode1 会存储到容器元素container._vNode属性中, 它会在后续渲染中作为 oldVNode 使用。
第二次渲染时,oldVNode存在,此时渲染器会把 vNode2 作为新 vNode,并将新旧 vNode 同时传递给patch函数进行打补丁。
第三次渲染时,newVNode 的值为null,即什么都不渲染。但此时容器中渲染的时 vNode2 所描述的内容,所以 渲染器需要清空容器。

7.3:自定义渲染器

渲染器不仅能够把虚拟DOM渲染为浏览器平台上的真实DOM。通过将渲染器设计为可配置的通用渲染器,即可实现 渲染到任意目标平台上。

补充patch

function createRenderer () {

  function patch (vNode1, vNode2, container) {
    // 如果vNode1不存在,则意味着挂载,调用mountElement完成挂载
    if (!vNode1) {
      mountElement(vNode2, container)
    } else {
      // vNode1存在,意味着打补丁
    }
  }

  function mountElement (vNode, container) {
    // 创建DOM元素
    const el = document.createElement(vNode.type)
    // 处理子节点,如果子节点是字符串,代表元素具有文本节点
    if (typeof vNode.children === 'string') {
      // 因此只需要设置元素的 textContent 属性即可
      el.textContent = vNode.children
    }
    // 将元素添加到容器中
    container.appendChild(el)
  }

  function render (vNode, container) {
    if (vNode) {
        // 新vNode存在,将其与vNode一起传递给 patch 函数,进行打补丁
        patch(container._vNode, vNode, container)
    } else {
        if (container._vNode) {
            // 旧vNode存在,且新vNode不存在,说明时 卸载 操作
            // 只需要将 container 内的DOM清空即可
            container.innerHTML = ''
        }
    }
    // 把 vNode 存储到 container._vNode下,即后续渲染中的旧 vNode
    container._vNode = vNode
  }

  return {
    render
  }
}

目标是设计不依赖于浏览器平台的通用渲染器,但mountElement函数内调用了大量依赖于浏览器的API。 例如 document.createElement、el.textContent、appendChild等。想要设计通用渲染器, 第一步要做的就是将这些浏览器特有的API抽离。

    const renderer = createRenderer({
        // 用于创建元素
        createElement (tag) {
            return document.createElement(tag)
        },
        // 用于设置元素的文本节点
        setElementText (el, text) {
            el.textContent = text
        },
        // 用于在给定的parent下添加指定元素
        insert (el, parent, anchor = null) {
            parent.insertBefore(el, anchor)
        }
    })
    function createRenderer (options) {
      // 通过options得到操作DOM的API
      const {
        createElement,
        insert,
        setElementText
      } = options

      // 在这个作用域内定义的函数都可以访问那些API
      function mountElement(vNode, container) {
        const el = createElement(vNode.type)
        if (typeof vNode.children === 'string') {
            setElementText(vNode.children)
        }
        insert(el, container)
      }

      function patch (vNode1, vNode2, container) {
        // ...
      }
    }

自定义渲染只是通过抽象的手段,让核心代码不再依赖平台特有的API,再通过支持个性化配置的能力来实现跨平台。

为了让渲染器不直接依赖浏览器平台特有的API,我们将这些用来创建、修改和删除元素的操作抽象成可配置的对象。 用户可以再调用createRenderer函数创建渲染器的时候指定自定义的配置对象,从而实现自定义的行为。