03-Vue3设计思路

107 阅读4分钟

Vue3设计思路

声明式地描述UI

vue3是一个声明式框架

模板描述UI

前端页面涉及的内容:

  • DOM元素,例如div标签a标签
  • 属性:标签的属性,如id等
  • 事件:click事件等
  • 元素层级结构:DOM树的层级结构

vue3声明式表述上述内容:

  • 使用HTML标签一致的方式描述DOM元素
  • 使用HTML标签一致的方式描述属性
  • 使用:v-bind动态绑定属性,如<div :id="testId"></div>
  • 使用@v-on描述事件,如<div @click="handler"></div>
  • 使用HTML标签一致的方式描述层级结构

手写虚拟DOM描述UI

可以使用JS对象来描述:

 const title = {
     tag: 'h1',
     props: {
         onClick: handler
     },
     children: [
         {tag: 'span'}
     ]
 }
 //解析完:<h1 @click="handler"><span></span></div>

模板和手写虚拟DOM的不同

使用JS对象描述更加灵活:

 let level = 3
 cosnt title = {
     tag: `h${level}`
 }

使用声明式,需要穷举:

 <h1 v-if="level === 1"></h1>
 <h2 v-else-if="level === 2"></h2>
 <h3 v-else-if="level === 3"></h3>
 <h4 v-else-if="level === 4"></h4>
 <h5 v-else-if="level === 5"></h5>
 <h6 v-else-if="level === 6"></h6>

vue的选择

vue中主要使用模板描述UI,但是也支持使用虚拟DOM描述UI(也就是使用JS对象)

vue中提供的写法,手写渲染函数,vue会通过render函数的返回值拿到虚拟DOM在渲染:

 import {h} from 'vue'
 export default {
     render(){
         //这里的h函数返回了一个JS对象,目的是让我们省去写tag等属性名的代码
         return h('h1', {onCilck: handle})   //虚拟DOM
     }
 }

渲染器

将虚拟DOM(JS对象描述真实的DOM结构)转化为真实DOM渲染到浏览器页面中

渲染器实现

其中vnode是虚拟DOM对象,container是一个真实的DOM,作为挂载点

 function render(vnode, container){
   // 根据tag创建标签
   const el = document.createElement(vnode.tag)
   // 遍历props 将属性、事件添加到DOM上
   for(const key in vnode.props){
     // 匹配事件
     if(/^on/.test(key)){
       el.addEventListener(
          key.substring(2).toLowerCase(),
          vnode.props[key]
       )
     }else{
        el.setAttribute(key, vnode.props[key])
     }
   }
   let children = vnode.children
   // 处理children
   if(typeof children === 'string'){
      // 判断字符串的时候 创建文本节点
      const textNode = document.createTextNode(children)
      el.appendChild(textNode)
   }else if( Array.isArray(children) ){
      // 子节点 递归渲染
      children.forEach( item=> render(item, el) )
   }
   container.appendChild(el)
 }

渲染器实现思路

  • 创建元素
  • 为元素添加属性和事件
  • 处理children

渲染器精髓

渲染器的精髓并不在创建DOM元素,而是在更新节点的阶段

如果只是对vnode做小小的修改,并不用重走一遍创建元素的过程,只需要更改更新元素的地方

组件的本质

组件的本质就是一组DOM元素的封装,这组DOM元素就是组件要渲染的内容

使用函数描述组件

函数返回值就是组件要渲染的内容:

 const MyComponent = function(){
     return {
         tag: 'div',
         props: {
             onClick: () => alert('hello')
         },
         children: 'click me'
     }
 }

构造虚拟DOM对象:

 const vnode = {
     tag: MyComponent
 }

使用渲染函数:

 function render(vnode, container){
     //说明vnode描述的是标签元素
     if(typeof vnode.tag === 'string'){
         mountElement(vnode, container)  //实现与上述的render函数一样
     }else if(typeof vnode.tag === 'function'){
         //说明vnode描述的是组件
         mountComponent(vnode, container)    //将vnode.tag()的结果传递给render而已
     }
 }

使用JS对象描述组件

JS对象表达组件:

const MyComponent = function(){
    render(){
        return {
            tag: 'div',
            props: {
                onClick: () => alert('hello')
            },
            children: 'click me'
        }
    }
}

其余步骤同理,略

模板的工作原理

模板的工作主要用到了另一个重要部分:编译器

编译器和渲染器一样,只是一段程序,不过工作内容不同

编辑器的作用是将模板编译成渲染函数

编译器工作

给出模板:

<div @click="handler">click me</div>

对编译器来说,模板就是字符串,转化成渲染函数:

render(){
    return h('div', {onClick: handler}, 'click me')
}

vue文件的转换

<template>标签中的内容就是模板内容,会将其编译成渲染函数并添加到<script>

所以无论使用模板还是渲染函数,对应vue组件来说,渲染的内容最终都是通过渲染函数生成的,然后渲染器再将渲染函数返回的虚拟DOM渲染为真实DOM,这就是Vue渲染页面的流程

Vue是各个模块组成的有机整体

组件的实现依赖于渲染器,模板的编译依赖于编译器,生成后的代码都是根据渲染器和虚拟DOM设计决定的,因此各个模块相互关联

两者配合工作使性能提升:

给出模板:

<div :class="cls"></div>

编译成渲染函数:

render(){
    return h('div', {class: cls})
}

现在问题:

cls是一个变量,渲染器的作用是寻找并且只更新变化的内容

所以cls变化时,渲染器需要自行寻找变更点,这样会消耗性能

解决:

编译器有能力分析动态内容,因为一眼就能看出:class="cls"是可能变化的

生成代码时可以附带这些信息:

render(){
    return {
    	tag: 'div',
        props: {
            class: cls
        },
        patchFlags: 1	//假设数字1代表class是动态的
    }
}

总结:这样能省去渲染器寻找变更点的工作量,性能自然也就提升了,而虚拟DOM对象中也会包含多种数据字段表示一定的含义