本篇为阅读《Vue.js》所作笔记~ 感受一下烘托器的重要性

  • 烘托器是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++

烘托器的基本概念

  1. 一般用renderer来表明‘烘托器’,render表明‘烘托’。烘托器的作用是把虚拟DOM烘托为特定渠道上的实在元素。在浏览器渠道上,烘托器会把虚拟DOM烘托为实在DOM元素

    虚拟DOM一般用 virtual DOM来表达,简写成vdom 虚拟DOM和实在DOM的结构相同,都是由一个个节点组成的树型结构 所以经常能听到’虚拟节点‘这样的词,即 virtual node,简写成vnode 虚拟DOM是树型结构,这棵树中的任何一个vnode节点都能够是一颗子树 因此vdom和vnode有时可替换运用,以下一致运用vnode

  2. 烘托器把虚拟DOM节点烘托为实在DOM节点的进程叫做 挂载(mount),例如Vue.js组件中的mounted钩子就会在挂载完结时触发。这意味着,在mounted钩子中能够访问实在DOM元素。

  3. 烘托器将实在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函数也是烘托器的一部分

  4. 有了烘托器,我们能够用它来履行烘托使命了,如下:

        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'))
    
    1. 初次烘托时,烘托器将vnode1烘托为实在DOM。烘托完结后,vnode1会存储到容器元素的container._vnode特点中,它会在后续烘托中作为旧vnode运用
    2. 第2次烘托时,旧vnode存在,此时烘托器会把vnode2作为新vnode,并将新旧vnode一同传递给patch函数进行打补丁
    3. 第三次烘托时,新vnode的值为null,即什么都不烘托,但此时容器中烘托的是vnode2所描绘的内容,所以烘托器需求清空容器,上述代码运用container.innerHTML = ''来清空,需求留意这姿态清空容器是有问题的,不过暂时运用它来达到目的
  5. 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供给可装备的接口,即可完结烘托器的跨渠道能力
  1. 从烘托一个一般的<h1>标签开端。

        const vnode = {
            type: 'h1',
            children: 'hello'
        }
        const renderer = createRenderer()
        renderer.render(vnode, document.querySelector('#app'))
    

    运用type特点来描绘一个vnode的类型,不同类型的type特点值能够描绘多种类型的vnode 当type特点是字符串类型值时,能够认为它描绘的是一般标签,并运用该type特点的字符串值作为标签的称号。 关于这样一个vnode,我们能够运用render函数烘托它

  2. 为了完结烘托作业,需求补充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的挂载

  3. 剖析代码存在问题: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'))
    

烘托器的规划

  1. 经过传入不同装备项,能够完结一个用来打印烘托器操作流程的自界说烘托器,如下:

        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,再经过个性化装备的能力完结跨渠道