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

Vue.js设计与实现之十二-渲染器的核心功能:挂载与更新01

2023-02-28

1、写在前面在本文中主要讲述了如何实现虚拟DOM节点转成真实DOM树上,最终挂载到挂载点上。讨论了虚拟节点是如何挂载到DOM树,又是如何从DOM树上卸载的,对于属性又是如何在元素上进行设置的。2、挂载子节点和元素的属性在上篇文章中,在vnode.children值为字符串时,将其设置为元素的文本内容

1、写在前面

在本文中主要讲述了如何实现虚拟DOM节点转成真实DOM树上,最终挂载到挂载点上。讨论了虚拟节点是如何挂载到DOM树,又是如何从DOM树上卸载的,对于属性又是如何在元素上进行设置的。

2、挂载子节点和元素的属性

在上篇文章中,在vnode.children值为字符串时,将其设置为元素的文本内容,当vnode.children值为数组时,表示其有子节点需要遍历设置。我们知道vnode是一个虚拟DOM节点,vnode.children是一个虚拟DOM树,其每个元素都是一个虚拟DOM节点。

const vnode = {
  type:"div",
  //props是标签的属性
  props:{
    id:"super"
  },
  children:[
    {
      type:"p",
      children:"pingping"
    }
  ]
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

上面的代码是虚拟DOM树形结构,如果要实现将虚拟VNode转为真实DOM,需要通过挂载和渲染。可以通过mountElement函数实现节点的渲染:

function mountElement(vnode, container){
    const el = createElement(vnode.type);
    //处理children
    if(typeof vnode.children === "string"){
      // 字符串转为标签的文本内容
      setElementText(el, vnode.children)
    }else if(Array.isArray(vnode.children)){
      //虚拟DOM节点数组 需要遍历每个节点进行挂载
      vnode.children.forEach(child=>{
        patch(null,child,el);
      })
    }   
    // 在标签上添加属性
    if(vnode.props){
      for(const key in vnode.props){
        // 调用setAttribute在元素上设置属性
        el.setAttribute(key, vnode.props[key]);
        // 也可以使用DOM对象直接设置属性
        // el[key] = vnode.props[key];
      }
    }
    insert(el, container);
  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

在上面代码片段中,首先会根据vnode.type的值创建DOM节点,children的值判断为string类型时,直接将其设置为元素的文本内容;children的值判断数组时,则对数组内的虚拟DOM节点进行遍历,调用patch函数挂载节点。

在挂载阶段patch是没有旧vnode的,对此传递的第一个参数是null,而在patch函数执行时会递归调用mountElement函数完成挂载。而patch传递的第三个参数是虚拟节点要挂载的根节点,完成挂载后需要给元素遍历设置属性。

3、正确地设置元素属性

如何正确地设置元素属性,就得先了解HTML Attribute和DOM Properties的差异和关联。我们知道HTML Attribute是定义在HTML标签元素上的属性,浏览器会将其解析创建一个对应的DOM对象,这个对象上包含许多属性(DOM Properties)。

HTML Attribute的作用是设置与之对应的DOM Properties初始值,当值发生改变时,DOM Properties始终存储着当前值,那么通过getAttribute获取到的也是初始值。

HTML Attribute和DOM Properties。然而在使用Vue.js的单文件模板不会被浏览器解析,此时需要框架自己进行解析。会影响DOM属性的添加方式,浏览器会解析普通HTML元素代码,自定分析HTML Attribute设置合适的DOM Properties。然而在使用Vue.js的单文件模板不会被浏览器解析,此时需要框架自己进行解析。

那么,看看在Vue.js是如何实现的:

const renderer = createRenderer({
  //创建元素
  createElement(tag){
    return document.createElement(tag);
  },
  //设置元素的文本节点
  setElementText(el, text){
    el.textContent = text;
  },
  //用于给指定父节点添加指定元素
  insert(el, parent, achor = null){
    parent.insertBefore(el, anchor);
  }
  // 将属性设置相关操作进行封装,作为渲染器选项进行传值
  patchProps(el, key, prevValue, nextValue){
     if(shouldSetAsProps(el, key, nextValue)){
        const type = typeof el[key];
        if(key === "class"){
        //采用el.className方式设置clas,是因为其性能相对于setAttribute和el.classList更高
          el.className = nextValue || "";
        }else if(type === "boolean" && nextValue === ""){
          el[key] = true;
        }else{
          el[key] = nextValue;
        }
     }else{
       el.setAttribute(key, nextValue);
     }
  }
})
  • 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.
  • 27.
  • 28.
  • 29.
  • 30.

在上面代码中,shouldSetAsProps函数用于分析属性是否应该作为DOM Properties属性被设置,返回的是一个布尔值。当返回一个true值时,表示应该作为DOM Properties被设置,否则就应该使用setAttribute函数设置属性。在设置属性时,需要优先设置元素的DOM Properties,当其值为空字符串时,需要将其矫正为true。

在mountElement函数中,只需要调用patchProps函数传递参数即可:

function mountElement(vnode, container){
    const el = createElement(vnode.type);
    //处理children
    if(typeof vnode.children === "string"){
      // 字符串转为标签的文本内容
      setElementText(el, vnode.children)
    }else if(Array.isArray(vnode.children)){
      //虚拟DOM节点数组 需要遍历每个节点进行挂载
      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);
  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

在上面代码片段中,mountElement函数会检查每个vnode.props中的属性,调用patchProps函数去设置DOM Properties。

4、卸载操作

在前面两节中,讨论了如何将虚拟DOM挂载到挂载点上,是通过createRenderer函数结合mountElement实现的。而卸载操作发生在更新阶段,即初次挂载完成之后,后续渲染触发的更新。

//初次挂载
renderer.render(vnode, document.querySelector("#app"));
// 再次挂载新vnode,触发更新 当传递的是null,则进行卸载之前的操作
renderer.render(vnode, document.querySelector("#app"));
//renderer.render(null, document.querySelector("#app"));
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

在初次渲染完毕后,后续渲染时如果传递的是null作为新vnode,则表示需要卸载当前所有渲染的内容。

在上一篇文章中,使用innerHTML设置为空作为清空容器元素内容的方案是存在缺陷的,因为它不会移除绑定在DOM元素上的事件处理函数。对此,需要先根据vnode对象获取到与之关联的真实DOM元素,使用原生DOM操作方法将其进行移除。

function unmount(vnode){
  const parent = vnode.el.parentNode;
  if(parent){
    parent.removeChild(vnode.el);
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

unmount函数接收一个虚拟节点作为参数,并将该节点对应的真实DOM元素从父元素上移除。

注意:在新旧vnode描述内容不同时,即vnode.type的属性不同时,两个vnode之间就不存在打补丁的意义,此时应该使用unmount函数先将旧元素进行卸载,再将n1的值重置为null,最后将新元素进行挂载到容器中。

当然,即使新旧vnode描述内容相同,也要判断两者的类型是否相同,vnode可以描述普通标签也可以描述组件,对于不同类型的vnode需要使用不同的挂载或打补丁方式。

function patch(n1, n2, container){
  if(n1 && n2.type !== n1.type){
    unmount(n1);
    n1 = null;
  }
  const { type } = n2;
  if(typeof type === "string"){
    if(!n1){
      mountElement(n2, container);
    }else{
      patchElement(n1, n2);
    }
  }else if(typeof type === "object"){
    // n2.type是object对象类型,则描述的是组件
  }else if(type === "xxx"){
    //处理其他类型vnode
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

5、写在最后

挂载子节点只需要递归调用patch函数即可实现挂载,而节点属性的设置就取决于被设置属性的特点。在卸载操作时,通过直接使用innerHTML清空容器元素是存在诸多问题的,对此封装了一个新的卸载函数unmount。