- 烘托器是Vue.js中非常重要的一部分,许多功能依靠烘托器来完结,例如Transition组件、Teleport组件、Suspense组件以及template ref 和自界说指令等
烘托器与呼应体系的结合
烘托器是用来履行烘托使命的,在浏览器渠道上,用它来烘托其间的实在DOM元素。烘托器还是框架跨渠道能力的关键,在规划烘托器的时分需求考虑好自界说的能力。本节将烘托器限定在DOM渠道,下面的函数便是一个合格的烘托器:
function renderer(domString, container) {
container.innerHTML = domString
}
renderer(`<h1>hello</h1>`, document.getElementById('app'))
不只能够烘托静态的字符串,还能够烘托动态拼接的HTML内容 假如是一个呼应式数据,能够联想到副作用函数 运用呼应体系 能够让整个烘托进程自动化
const count = ref(1)
effect(() => {
renderer(`<h1>${count}</h1>`, document.getElementById('app'))
})
count++
烘托器的基本概念
-
一般用renderer来表明‘烘托器’,render表明‘烘托’。烘托器的作用是把虚拟DOM烘托为特定渠道上的实在元素。在浏览器渠道上,烘托器会把虚拟DOM烘托为实在DOM元素
虚拟DOM一般用 virtual DOM来表达,简写成vdom 虚拟DOM和实在DOM的结构相同,都是由一个个节点组成的树型结构 所以经常能听到’虚拟节点‘这样的词,即 virtual node,简写成vnode 虚拟DOM是树型结构,这棵树中的任何一个vnode节点都能够是一颗子树 因此vdom和vnode有时可替换运用,以下一致运用vnode
-
烘托器把虚拟DOM节点烘托为实在DOM节点的进程叫做 挂载(mount),例如Vue.js组件中的mounted钩子就会在挂载完结时触发。这意味着,在mounted钩子中能够访问实在DOM元素。
-
烘托器将实在DOM挂载在哪里呢?烘托器一般需求接收一个挂载点作为参数,用来指定详细的挂载位置(‘挂载点’其实便是一个DOM元素,烘托器会把该DOM元素作为容器元素,并把内容烘托到其间)
function createRenderer() { function render(vnode, container) { } function hydrate(vnode, container) { } return { render, hydrate } }
如上代码,createRenderer函数用来创立一个烘托器,为什么需求这个函数,直接界说render不就能够了吗?
烘托器与烘托是不同的,烘托器是愈加宽泛的概念,包含烘托, 把vnode烘托为实在DOM的render函数只是其间一部分。烘托器不只能够用来烘托,还能够用来激活已有的DOM元素,这个进程一般发生在同构烘托的情况下。在Vue.js3中,甚至连创立应用的createApp函数也是烘托器的一部分
-
有了烘托器,我们能够用它来履行烘托使命了,如下:
const renderer = createRenderer() // 初次烘托 renderer.render(vnode, document.querySelector('#app'))
首先调用了createRenderer创立了一个烘托器,接着调用烘托器的render函数履行烘托,初次调用render函数时,只需求创立新的DOM元素即可,这个进程只触及挂载
当多次在同一个container上调用render函数进行烘托时,烘托器除了要履行挂载动作外,还要履行更新动作
// 初次烘托 renderer.render(oldVnode, document.querySelector('#app')) // 第2次烘托 renderer.render(newVnode, document.querySelector('#app'))
初次烘托已经将oldVnode烘托到container内了,再次调用render函数并测验烘托newVnode时,就不能简单地履行挂载动作了,在这种情况下,烘托器会运用newVnode与oldVnode进行比较,试图找到并更新变更点。这个进程叫做‘打补丁’(或更新patch),挂载动作自身也能够看作一种特别的打补丁,特别之处在于旧的vnode是不存在的,代码如下:
function createRenderer() { 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, } } const renderer = createRenderer() // 初次烘托 renderer.render(vnode1, document.querySelector('#app')) // 第2次烘托 renderer.render(vnode2, document.querySelector('#app')) // 第三次烘托 renderer.render(null, document.querySelector('#app'))
- 初次烘托时,烘托器将vnode1烘托为实在DOM。烘托完结后,vnode1会存储到容器元素的
container._vnode
特点中,它会在后续烘托中作为旧vnode运用 - 第2次烘托时,旧vnode存在,此时烘托器会把vnode2作为新vnode,并将新旧vnode一同传递给patch函数进行打补丁
- 第三次烘托时,新vnode的值为null,即什么都不烘托,但此时容器中烘托的是vnode2所描绘的内容,所以烘托器需求清空容器,上述代码运用
container.innerHTML = ''
来清空,需求留意这姿态清空容器是有问题的,不过暂时运用它来达到目的
- 初次烘托时,烘托器将vnode1烘托为实在DOM。烘托完结后,vnode1会存储到容器元素的
-
patch (container._vnode, vnode, container)
该函数是整个烘托器的中心入口,它承载了最重要的烘托逻辑
function patch(n1, n2, container) {}
至少接收三个参数:
- n1:旧vnode
- n2:新vnode
- container:容器
在初次烘托时,容器元素的container._vnode特点是不存在的,即undefined。这意味着,在初次烘托时传递给n1也是undefined。这时,patch函数会进行挂载动作,它会忽略n1,并直接将n2描绘的内容烘托到容器中。从这点能够看出,patch函数不只能够用来打补丁,也能够用来履行挂载
自界说烘托器
- 以下以浏览器作为烘托的目标渠道,编写一个烘托器,在这个进程中,看看哪些内容是能够笼统的,然后经过笼统,将浏览器特定的API抽离,这样就能够使得烘托器的中心不依靠于浏览器,在此基础上,再为那些被抽离的API供给可装备的接口,即可完结烘托器的跨渠道能力
-
从烘托一个一般的
<h1>
标签开端。const vnode = { type: 'h1', children: 'hello' } const renderer = createRenderer() renderer.render(vnode, document.querySelector('#app'))
运用type特点来描绘一个vnode的类型,不同类型的type特点值能够描绘多种类型的vnode 当type特点是字符串类型值时,能够认为它描绘的是一般标签,并运用该type特点的字符串值作为标签的称号。 关于这样一个vnode,我们能够运用render函数烘托它
-
为了完结烘托作业,需求补充patch函数
function createRenderer() { function patch(n1, n2, container) { if (!n1) { // 假如n1不存在 意味着挂载 则调用mountElement函数完结挂载 mountElement(n2, container) } else { // n1存在 意味着打补丁 } } function mountElement(vnode, container) { // 创立DOM元素 const el = document.createElement(vnode.type) // 处理子节点 假如子节点是字符串 代表元素具有文本节点 if (typeof vnode.children === 'string') { // 只需求设置元素的 textContent 特点即可 el.textContent = vnode.children } // 将元素增加到容器中 container.appendChildren(el) } function render(vnode, container) {} return { render } }
如上,将
patch
函数、mountElement
函数编写在createRenderer
函数内这姿态,就完结了一个vnode的挂载
-
剖析代码存在问题:mountElement函数内调用了大量依靠于浏览器的API,要规划通用的烘托器,需求将这些浏览器特有的API抽离。能够将这些操作DOM的API作为装备项,该装备项能够作为createRenderer函数的参数,如下:
在mountElement等函数内就能够经过装备项来取得操作DOM的API
重构后的mountElement函数在功能上没有任何改变。不同的是,它不直接依靠于浏览器的特有API
只需传入不同的装备项,就能完结非浏览器环境下的烘托作业
const renderer = createRenderer({ createElement(tag) { // 用于创立元素 return document.createElement(tag) }, setElementText(el, text) { // 用于设置元素的文本节点 el.textContent = text }, insert(el, parent, anchor = null) { // 用于在给定的parent下增加指定元素 parent.insertBefore(el, anchor) } }) function createRenderer(options) { const { createElement, setElementText, insert } = options function mountElement(vnode, container) { const el = createElement(vnode.type) if (typeof vnode.children === 'string') { setElementText(el, vnode.children) } insert(el, container) } return { render } } renderer.render(vnode, document.querySelector('#div'))
-
经过传入不同装备项,能够完结一个用来打印烘托器操作流程的自界说烘托器,如下:
const renderer = createRenderer({ createElement(tag) { console.log(`创立元素${tag}`); console.log({ tag }); return { tag } }, setElementText(el, text) { console.log(`设置${JSON.stringify(el)}的文本内容为${text}`); el.text = text }, insert(el, container, anchor = null) { console.log(`将${JSON.stringify(el)}增加到${JSON.stringify(container)}下`); } }) const container = { type: 'root' } // 设置一个挂载点 renderer.render(vnode, container)
- 在createElement内,不再调用浏览器的API,而是仅仅返回一个目标{tag},并将其作为创立出来的’DOM‘元素
- 在setElementText和insert函数内,同样没有调用浏览器相关的API,而是自界说了一些逻辑,并打印信息到控制台
- 上面完结的自界说烘托器不依靠浏览器特有的API,所以这段代码不只能够在浏览器中运转,也能够在Node.js中运转
自界说烘托器只是经过笼统的手法,让中心代码不再依靠渠道特有的API,再经过个性化装备的能力完结跨渠道