美文网首页
Vue 3 Complier & Renderer API

Vue 3 Complier & Renderer API

作者: AizawaSayo | 来源:发表于2021-08-28 23:27 被阅读0次

    Vue 3 模板资源管理器 Vue 3 Template Explorer,它允许我们查看由 Vue 3 Complier 生成的渲染函数。左边是源模版,右边是生成的渲染函数代码。
    这个网页版工具很有用,当代码不能像预期一样工作,可以在左边黏贴自己的模版,就能看到代码被编译成了什么,然后看看模版出了什么问题。尤大正是用它来在开发过程中调试 Compiler。
    模版浏览器右侧有很多选项,可以用来在查看时启用一些特定类型的优化。尤大用这个浏览器向我们展示了 Vue 3 中的新优化。

    右侧选项

    1. 嵌套节点(静态节点提升)

    有一个嵌套的 div,我们可以启用hoistStatic option,来查看它是否被从渲染函数中提升,以便在每个渲染器(render)上复用它。

    这个 render 函数会在每次组件更新时被调用。每当一个节点被提升,它就会被在 render 函数外创建一次。在以后的每一次渲染中,被提升的那些节点(hosited_1hosited_2...)会在 render 函数内被复用。这有两个好处,一是避免重新创建对象,然后扔掉(垃圾收集的知识点),二是在我们的模式算法中,当两个节点在同一个位置时,在严格平等的情况下,我们可跳过它,因为知道它永远不会改变。

    2. 事件侦听器(处理程序缓存)

    在虚拟 DOM 中,当有东西改变,不会去检查所有节点、所有属性和元素,只检查特定的地方。因为编译器会生成提示 hints,以帮助 runtime 更高效。这在手写的渲染函数中很难实现,因为分析 JavaScript 比 分析模版困难得多。


    比如在这个 div 节点,我们有一个 onClick 侦听器。每次更新或 diff,我们都会根据标记看一下 onClick,以确认它没有改变。

    但大多数情况,当你绑定一个 event listener,并不会去更改 event handler(的变量名)。我们会在第一次渲染时把事件处理程序转化为一个内联函数,并把缓存它。在以后的渲染中,都会始终使用同一个内联处理程序(inline handler) 。然后里面的函数会访问_ctx.onClick,来保证总是最新的函数被调用。
    因此,如果onClick函数的内容变了,我们不需要对 vnode 本身做任何事情。这就是另一个层次的优化。

    这一点尤其重要,因为如果要将event handlers添加组件上,导致子组件不必要的渲染。
    当你编写像这样的内联处理程序:<Foo onClick="()=>foo()"/>或者
    <Foo onClick="foo(123)"/>(这也是一个隐式的内联处理程序),在 Vue 2 中即使这个Foo组件什么都没改变,event handler 仍然会导致它的子组件在Foo重新渲染时也再次渲染。这在大型应用中会导致连锁效应。
    而 Vue 3 通过 handle 缓存,减少了大型组件树中发生很大面积的不必要的渲染。这也是一个很不错的性能改进。
    事实上,这也是 React 中 一个常见的陷阱。这就是为什么有一个useMemouseCallback API,作用就是允许开发人员手动缓存像这样的 event handler 来防止子组件重新渲染。
    Vue 3 为用户自动完成这一切。

    动态插值(精准跟踪动态节点 避免其他无关 DOM 不必要的渲染)

    真实 DOM 更新时的弊端:runtime 并不能提供给我们信息。 比如节点顺序已经改变,或者从 div 变成了 p。所以 runtime 必须检查每个节点以确保 DOM 树结构稳定,把所有的 props 都区分开来再确保 props 有无改变。没有 children “随意走动”,或者 新的 children 加入。

    当我们调用渲染函数,生成的 JavaScript 结构就像这样:


    编译器渲染出的 vdom

    渲染器更新渲染这样的 JavaScript 结构时,会有两个像这样的虚拟 DOM 树的快照(snapshots)。某些部分可能已经改变了,但渲染器(renderer)并不真正知道发生了什么变化,如果我们不提供更多相关的 hints。所以它必须经过一系列相对暴力的算法,以递归地方式遍历整棵树,比较旧节点和新节点去找出变化。
    这在一般的中型应用程序也足够快,以至于你不会注意到任何性能的瓶颈。因为现代 JavaScript 引擎在迭代处理普通对象方面已经做了很好的优化。但在大型 App 中这些小的迭代成本会叠加,比如你点击一个按钮,10个组件同时被触发更新,JavaScript 开销一下子增加了,就可能会阻塞或卡顿你的程序。所以人们开始考虑如何手动优化组件树,避免不必要的重新渲染。这就是 Vue 的优势所在,让框架足够智能,那么用户甚至不用考虑这些事。

    如果没有编译器生成的提示,虚拟 DOM 渲染器只能看到 JavaScript 树,并不能知道哪个部分会改变,哪些部分不会变。编译器的工作就是提供这些信息。这样 runtime 就能跳过很多不必要的工作,直接去处理可能会改变的(比如动态插值)的部分。同时还会添加一些补丁标识(比如/* TEXT */)来表示这个节点是动态的,它应该被跟踪(tracked)

    我们实现这点的方式是将动态模版的根节点转化成所谓的 Block,通过_createElementBlock。当_openBlock()调用时,所有的 children 都会被评估。

    等 render 函数整个被调用,这个div将会有一个额外的属性,叫做动态子节点,只包含span这个节点。

    每个 Block 都有一个额外的数组,只跟踪其中的动态节点(无论嵌套得多深)。那么无论你的 DOM 结构多复杂,块只跟踪在一个扁平数组中的动态节点(可能改变的节点)。

    v-if 结构指令

    v-if这样的指令会改变节点结构,能控制节点从 DOM 树上出现或消失。

    这样一来因为span的父级div也变成动态的了。我们有嵌套的Block,每个 Block都将在扁平数组中跟踪自己的动态子对象。这样能在大多数情况下减少数量级的递归数量(因为不必检查每个 vnode 的变化)。现在只需要去查找 Blocks,还有提供的一些 hints,关于值内部可能发生的变化。

    还有对于每个节点,补丁标识本身还编码了关于要在这个节点上做的事情的种类信息,如/* TEXT */,表示当试图 diff 这个节点时,只需要检查它的文本内容,不必去管它的props

    将所有这些结合起来,编译器将真正生成一个 runtime 渲染函数,它允许 runtime 利用所有这些 hints 做尽可能少的工作。

    简化版渲染函数:

    codepen 地址

    <style>
      .red { color: red }
    </style>
    
    <div id="app"></div>
    
    <script>
      function h(tag, props, children) {
        return {
          tag,
          props,
          children
        }
      }
      function mount(vnode, container) {
        const el = document.createElement(vnode.tag)
        // props
        if (vnode.props) {
          for (const key in vnode.props) {
            const value = vnode.props[key]
            el.setAttribute(key, value)
          }
        }
        // children
        if (vnode.children) {
          if (typeof vnode.children === 'string') {
            el.textContent = vnode.children
          } else {
            vnode.children.forEach(child => {
              mount(child, el)
            })
          }
        }
        container.appendChild(el)
      }
      const vdom = h('div', { class: 'red' }, [
        h('span', null, 'hello')  
      ])
      
      mount(vdom, document.getElementById('app'))
    </script>
    

    这段代码允许我们使用渲染函数,返回一个简单的虚拟节点,然后挂载函数将进行适当的 DOM JavaScript 调用以在浏览器中创建虚拟节点。

    简化版 Patch 补丁函数:

    codepen 地址

    <style>
      .red { color: red }
      .green { color: green }
    </style>
    
    <div id="app"></div>
    
    <script>
      function h(tag, props, children) {
        return {
          tag,
          props,
          children
        }
      }
      function mount(vnode, container) {
        const el = vnode.el = document.createElement(vnode.tag)
        // props
        if (vnode.props) {
          for (const key in vnode.props) {
            const value = vnode.props[key]
            el.setAttribute(key, value)
          }
        }
        // children
        if (vnode.children) {
          if (typeof vnode.children === 'string') {
            el.textContent = vnode.children
          } else {
            vnode.children.forEach(child => {
              mount(child, el)
            })
          }
        }
        container.appendChild(el)
      }
      const vdom = h('div', { class: 'red' }, [
        h('span', null, 'hello')  
      ])
      
      mount(vdom, document.getElementById('app'))
      
      function patch(n1, n2) {
        if (n1.tag === n2.tag) {
          const el = n2.el = n1.el
          //props
          const oldProps = n1.props || {}
          const newProps = n2.props || {}
          for (const key in newProps) {
            const oldValue = oldProps[key]
            const newValue = newProps[key]
            if (newValue !== oldValue) {
              el.setAttribute(key, newValue)
            }
          }
          for (const key in oldProps) {
            if (!(key in newProps)) {
              el.removeAttribute(key)
            }
          }
          
          // children
          const oldChildren = n1.children
          const newChildren = n2.children
          if (typeof newChildren === 'string') {
            if (typeof oldChildren === 'string') {
              if (newChildren !== oldChildren) {
                el.textContent = newChildren
              }
            } else {
              el.textContent = newChildren
            }
          } else {
            if (typeof oldChildren === 'string') {
              el.innerHTML = ''
              newChildren.forEach(child => {
                mount(child, el)
              })
            } else {
              const commonLength = Math.min(oldChildren.length, newChildren.length)
              for (let i = 0; i < commonLength; i++) {
                patch(oldChildren[i], newChildren[i])
              }
              if (newChildren.length > oldChildren.length) {
                newChildren.slice(oldChildren.length).forEach(child => {
                  mount(child, el)
                })
              } else if (newChildren.length < oldChildren.length) {
                oldChildren.slice(newChildren.length).forEach(child => {
                  el.removeChild(child.el)
                })
              }
            }
          } 
        } else {
          //replace
        }
      }
      
      const vdom2 = h('div', { class: 'green'}, [
        h('span', null, 'changed!')
      ])
      
      patch(vdom, vdom2)
    </script>
    

    相关文章

      网友评论

          本文标题:Vue 3 Complier & Renderer API

          本文链接:https://www.haomeiwen.com/subject/ixvyiltx.html