深圳幻海软件技术有限公司 欢迎您!

Vue.js设计与实现之十一-渲染器的设计

2023-02-28

1、写在前面在Vue.js框架很多功能依赖渲染器实现,也是框架性能的核心,能够直接影响框架性能。对此,Vue.js3的渲染器通过快捷路径更新的方式,利用编译器提供的信息提升性能。2、渲染器和响应系统的结合渲染器是用来执行渲染任务的,可以在浏览器平台来渲染真实DOM元素,它还能实现框架跨平台能力。前面

1、写在前面

在Vue.js框架很多功能依赖渲染器实现,也是框架性能的核心,能够直接影响框架性能。对此,Vue.js3的渲染器通过快捷路径更新的方式,利用编译器提供的信息提升性能。

2、渲染器和响应系统的结合

渲染器是用来执行渲染任务的,可以在浏览器平台来渲染真实DOM元素,它还能实现框架跨平台能力。

前面已经实现了reactivity的源码,为了后续方便讲解后续的操作,对此可以直接使用vue.js3的源码导入使用。

<script src="https://unpkg.com/@vue/reactivity@3.0.5/dist/reactivity.global.js" />
  • 1.

这样就可以通过暴露的API使用:

const {effect, ref} = VueReactivity;
function renderer(domString, container){
  container.innerHTML = domString;
}
const count = ref(1);

effect(()=>{
  renderer(`<h1>${count.value}</h1>`,document.getElementById("app"))
});
count.value++
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

渲染器把虚拟DOM节点渲染为真实DOM节点的过程叫做挂载,在Vue中mounted钩子函数会在挂载完成时触发,从而实现虚拟DOM的挂载。此外,渲染器还需要一个挂载点作为参数,用于指定具体的挂载位置,渲染器会将此DOM元素作为容器元素,可以实现内容的渲染。

在Vue.js3中是通过createRenderer函数来创建渲染器renderer,而renderer不仅包含渲染器函数render,还包括服务端渲染使用的hydrate函数。用于创建应用的createApp函数也是渲染器的一部分。

const renderer = createRenderer();
//首次渲染
renderer.render(oldVNode, document.querySelector("#app"));
//第二次渲染
renderer.render(newVNode, document.querySelector("#app"));
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

在上面代码中,首先调用createRenderer函数创建一个渲染器,通过调用渲染器的renderer.render函数创建新的DOM元素,而后将虚拟DOM节点vnode渲染到挂载点上,这就是挂载。

在同一个挂载DOM上,调用renderer.render函数进行渲染时,渲染器除了实现常规的DOM挂载外,还需要执行页面更新操作,也就是DOM更新。在渲染器会使用vnode与上次渲染的oldVNode进行比较,从而找到并更新变更点,此过程叫做"打补丁"(patch)即更新。

function createRenderer(){
  function render(domString, container){
    if(vnode){
      // 新vnode存在,将其与旧vnode传递给patch函数,进行打补丁
      patch(container._vnode, vnode, container);
    }else{
      if(container._vnode){
        // 如果旧节点vnode存在,则需要清空挂载点的DOM元素
        container.innerHTML = "";
      }
    }
    // 将vnode存储到container._vnode中
    container._vnode = vnode
  }
  return {
    render
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

在上面代码中,是render函数的基本实现。patch函数是整个渲染器的核心入口,承载了最重要的渲染逻辑。

patch(n1, n2, container){
  /***/
}
  • 1.
  • 2.
  • 3.
  • n1:旧VNode。
  • n2:新VNode。
  • container:挂载容器。

3、自定义渲染器

我们看看如何自定义实现渲染器,实现跨平台能力,那就从普通的h1标签开始。

const vnode = {
  type:"h1",
  children:"pingping"
}
  • 1.
  • 2.
  • 3.
  • 4.

在上面代码中实现了一个简单的vnode,使用type属性描述vnode类型,type属性是字符串类型值时是普通标签。

const vnode = {
  type:"h1",
  children:"pingping"
}
const renderer = createRenderer();
//首次渲染
renderer.render(vnode, document.querySelector("#app"));
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

这样,我们就可以渲染为<h1>pingping</h1>为了完成渲染工作,下面可以补充patch函数。

function createRenderer(){
  function patch(n1, n2, container){
    //如果n1不存在的时候,意味着挂载,需要调用mountElement函数完成挂载
    if(!n1){
      mountElement(n2, container);
    }else{
      //n1存在,即打补丁更新DOM节点
    }
  }
  function render(domString, container){
    if(vnode){
      // 新vnode存在,将其与旧vnode传递给patch函数,进行打补丁
      patch(container._vnode, vnode, container);
    }else{
      if(container._vnode){
        // 如果旧节点vnode存在,则需要清空挂载点的DOM元素
        container.innerHTML = "";
      }
    }
    // 将vnode存储到container._vnode中
    container._vnode = vnode
  }
  return {
    render
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

在上面代码中,patch函数中参数n1不存在时,意味着没有旧VNode,此时只需要进行挂载即可。

function mountElement(vnode, container){
  const el = document.createElement(vnode.type);
  if(typeof vnode.children === "string"){
    el.textContent = vnode.children;
  }
  container.appendChild(el);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

在上面代码中,简单实现了一个普通标签元素的挂载工作。Vue.js3是个跨平台框架,得益于一个不依赖平台的通用渲染器,而在mountElement函数中依赖了大量浏览器API。想要设计通用渲染器,就必须剥离与平台相关的API,当然可以将这些操作DOM的API作为createRenderer的配置项。

const renderer = createRenderer({
  //创建元素
  createElement(tag){
    return document.createElement(tag);
  },
  //设置元素的文本节点
  setElementText(el, text){
    el.textContent = text;
  },
  //用于给指定父节点添加指定元素
  insert(el, parent, achor = null){
    parent.insertBefore(el, anchor);
  }
})
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

上面代码中,可以将操作DOM的API封装为一个对象,并把它传递到createRenderer函数,可以通过配置项来取得操作DOM的api。

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);
  }
  function patch(n1, n2, container){
    //...
  }
  function render(domString, container){
     //...
  }
  return {
     render
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

现在我们对自定义渲染器有个更深刻的认识,自定义渲染器不是什么黑盒子,只是通过代码抽象,让核心代码不再强依赖于平台特定API,通过自定义配置项实现跨平台。

4、写在最后

在本文中,我们了解到渲染器的作用和实现,知道其是将虚拟DOM转为真实DOM挂载到节点上,在进行节点更新的时候是通过patch实现的。为了实现跨平台渲染,那么就需要用户通过自定义配置项渲染器,从而实现DOM的渲染挂载。