美文网首页
【Vue3.0】- 如何渲染组件

【Vue3.0】- 如何渲染组件

作者: 啦啦啦喽啰 | 来源:发表于2020-11-23 09:46 被阅读0次

    组件

    • 组件是一个抽象的概念,它是对一棵 DOM 树的抽象
    • 可以描述组件信息的JavaScript对象
    • 从表现上来看
      • 组件的模板决定了组件生成的DOM标签
      • Vue.js内部,一个组件想要真正的渲染生成DOM
        image.png

    应用程序初始化

    • 整个组件树是由根组件开始渲染的
    • 为了找到根组件的渲染入口,从应用程序的初始化过程开始分析
    • 对比vue2.0vue3.0入口
    // 在 Vue.js 2.x 中,初始化一个应用的方式如下
    import Vue from 'vue'
    import App from './App'
    const app = new Vue({
      render: h => h(App)
    })
    app.$mount('#app')
    
    // 在 Vue.js 3.0 中,初始化一个应用的方式如下
    import { createApp } from 'vue'
    import App from './app'
    const app = createApp(App)
    app.mount('#app')
    
    • Vue.js 3.0中导入了一个createApp,这是个入口函数,它是 Vue.js对外暴露的一个函数

    createApp内部实现

    const createApp = ((...args) => {
      // 创建 app 对象
      const app = ensureRenderer().createApp(...args)
      const { mount } = app
      // 重写 mount 方法
      app.mount = (containerOrSelector) => {
        // ...
      }
      return app
    })
    
    • createApp主要做了两件事情
      • 1)创建app对象
      • 2)重写app.mount方法

    创建app对象

    • ensureRenderer().createApp() 来创建 app 对象
    • 实现了跨平台渲染
    const app = ensureRenderer().createApp(...args)
    
    • ensureRenderer()用来创建一个渲染器对象
    ensureRenderer 内部实现
    // 渲染相关的一些配置,比如更新属性的方法,操作 DOM 的方法
    const rendererOptions = {
      patchProp,
      ...nodeOps
    }
    let renderer
    // 延时创建渲染器,当用户只依赖响应式包的时候,可以通过 tree-shaking 移除核心渲染逻辑相关的代码
    function ensureRenderer() {
      return renderer || (renderer = createRenderer(rendererOptions))
    }
    function createRenderer(options) {
      return baseCreateRenderer(options)
    }
    function baseCreateRenderer(options) {
      function render(vnode, container) {
        // 组件渲染的核心逻辑
      }
      return {
        render,
        createApp: createAppAPI(render)
      }
    }
    function createAppAPI(render) {
      // createApp createApp 方法接受的两个参数:根组件的对象和 prop
      return function createApp(rootComponent, rootProps = null) {
        const app = {
          _component: rootComponent,
          _props: rootProps,
          mount(rootContainer) {
            // 创建根组件的 vnode
            const vnode = createVNode(rootComponent, rootProps)
            // 利用渲染器渲染 vnode
            render(vnode, rootContainer)
            app._container = rootContainer
            return vnode.component.proxy
          }
        }
        return app
      }
    }
    
    • 首先用ensureRenderer()来延时创建渲染器
      • 好处是当用户只依赖响应式包的时候,就不会创建渲染器
      • 可以通过tree-shaking的方式移除核心渲染逻辑相关的代码
    • 通过createRenderer创建一个渲染器
      • 这个渲染器内部会有一个createApp方法
        • 它是执行createAppAPI方法返回的函数
        • 接受了rootComponentrootProps两个参数
      • 我们在应用层面执行createApp(App)方法时:
        • 会把App组件对象作为根组件传递给rootComponent
        • 这样,createApp内部就创建了一个app对象
        • 它会提供mount方法,这个方法是用来挂载组件的。

    值得注意的是

    • app对象创建过程中,Vue.js利用闭包和函数柯里化的技巧,很好地实现了参数保留

    重写app.mount方法

    为什么重写?
    • createApp返回的app对象已经拥有了mount方法了,为什么还有在入口重写?
      • 为了支持跨平台渲染
      • createApp函数内部的app.mount方法是一个标准的可跨平台的组件渲染流程:
    mount(rootContainer) {
      // 创建根组件的 vnode
      const vnode = createVNode(rootComponent, rootProps)
      // 利用渲染器渲染 vnode
      render(vnode, rootContainer)
      app._container = rootContainer
      return vnode.component.proxy
    }
    
    • 主要流程是,先创建vnode,再渲染 vnode
    • 参数rootContainer根据平台不同而不同,
    • 这里面的代码不应该包含任何特定平台相关的逻辑,因此我们需要在外部重写这个方法
    app.mount 重写都做了哪些事情?
    app.mount = (containerOrSelector) => {
      // 标准化容器
      const container = normalizeContainer(containerOrSelector)
      if (!container)
        return
      const component = app._component
       // 如组件对象没有定义 render 函数和 template 模板,则取容器的 innerHTML 作为组件模板内容
      if (!isFunction(component) && !component.render && !component.template) {
        component.template = container.innerHTML
      }
      // 挂载前清空容器内容
      container.innerHTML = ''
      // 真正的挂载
      return mount(container)
    }
    
    • 首先是通过normalizeContainer标准化容器(这里可以传字符串选择器或者DOM对象,但如果是字符串选择器,就需要把它转成 DOM对象,作为最终挂载的容器)
    • 然后做一个if判断,如果组件对象没有定义render函数和 template模板,则取容器的innerHTML作为组件模板内容
    • 接着在挂载前清空容器内容,最终再调用app.mount的方法走标准的组件渲染流程

    优势

    • 跨平台实现
    • 兼容vue2.0写法
    • app.mount既可以传dom,又可以传字符串选择器

    核心渲染流程:创建 vnode 和渲染 vnode

    创建 vnode

    • 1、 vnode本质上是用来描述DOMJavaScript对象

    它在Vue.js中可以描述不同类型的节点,比如普通元素节点、组件节点等

    vnode如何描述
    // vnode 这样表示<button>标签
    const vnode = {
      type: 'button',
      props: { 
        'class': 'btn',
        style: {
          width: '100px',
          height: '50px'
        }
      },
      children: 'click me'
    }
    
    • type属性表示DOM的标签类型

    • props属性表示DOM的一些附加信息,比如styleclass

    • children属性表示DOM的子节点,它也可以是一个vnode数组,只不过vnode可以用字符串表示简单的文本

    • 2、 vnode除了用于描述一个真实的DOM,也可以用来描述组件

    vnode其实是对抽象事物的描述

    // vnode 这样表示 <custom-component>
    const CustomComponent = {
      // 在这里定义组件对象
    }
    const vnode = {
      type: CustomComponent,
      props: { 
        msg: 'test'
      }
    }
    
    • 3、其他的,还有纯文本vnode,注释vnode
    • 4、Vue.js 3.0中,vnodetype,做了更详尽的分类,包括 SuspenseTeleport等,且把vnode的类型信息做了编码,以便在后面的patch阶段,可以根据不同的类型执行相应的处理逻辑
    vode优势
    • 抽象
    • 跨平台
    • 但是,和手动修改dom对比,并不一定有优势

    如何创建vnode

    • app.mount函数的实现,内部是通过createVNode函数创建了根组件的vnode
    const vnode = createVNode(rootComponent, rootProps)
    
    createVNode 函数的大致实现
    function createVNode(type, props = null,children = null) {
      if (props) {
        // 处理 props 相关逻辑,标准化 class 和 style
      }
      // 对 vnode 类型信息编码
      const shapeFlag = isString(type)
        ? 1 /* ELEMENT */
        : isSuspense(type)
          ? 128 /* SUSPENSE */
          : isTeleport(type)
            ? 64 /* TELEPORT */
            : isObject(type)
              ? 4 /* STATEFUL_COMPONENT */
              : isFunction(type)
                ? 2 /* FUNCTIONAL_COMPONENT */
                : 0
      const vnode = {
        type,
        props,
        shapeFlag,
        // 一些其他属性
      }
      // 标准化子节点,把不同数据类型的 children 转成数组或者文本类型
      normalizeChildren(vnode, children)
      return vnode
    }
    
    • props做标准化处理
    • vnode的类型信息编码
    • 创建vnode对象
    • 标准化子节点children

    渲染 vnode

    render(vnode, rootContainer)
    const render = (vnode, container) => {
      if (vnode == null) {
        // 销毁组件
        if (container._vnode) {
          unmount(container._vnode, null, null, true)
        }
      } else {
        // 创建或者更新组件
        patch(container._vnode || null, vnode, container)
      }
      // 缓存 vnode 节点,表示已经渲染
      container._vnode = vnode
    }
    
    • 如果它的第一个参数vnode为空,则执行销毁组件的逻辑
    • 否则执行创建或者更新组件的逻辑
    patch函数
    const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
      // 如果存在新旧节点, 且新旧节点类型不同,则销毁旧节点
      if (n1 && !isSameVNodeType(n1, n2)) {
        anchor = getNextHostNode(n1)
        unmount(n1, parentComponent, parentSuspense, true)
        n1 = null
      }
      const { type, shapeFlag } = n2
      switch (type) {
        case Text:
          // 处理文本节点
          break
        case Comment:
          // 处理注释节点
          break
        case Static:
          // 处理静态节点
          break
        case Fragment:
          // 处理 Fragment 元素
          break
        default:
          if (shapeFlag & 1 /* ELEMENT */) {
            // 处理普通 DOM 元素
            processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
          }
          else if (shapeFlag & 6 /* COMPONENT */) {
            // 处理组件
            processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
          }
          else if (shapeFlag & 64 /* TELEPORT */) {
            // 处理 TELEPORT
          }
          else if (shapeFlag & 128 /* SUSPENSE */) {
            // 处理 SUSPENSE
          }
      }
    }
    
    • 这个函数有两个功能:
      • 一个是根据vnode挂载DOM
      • 一个是根据新旧vnode更新DOM。对于初次渲染
    • patch函数入参
      • 第一个参数 n1 表示vnode,当 n1null 的时候,表示是一次挂载的过程;
      • 第二个参数 n2 表示vnode 节点,后续会根据这个 vnode 类型执行不同的处理逻辑;
      • 第三个参数container表示 DOM 容器,也就是 vnode 渲染生成 DOM 后,会挂载到 container 下面。

    渲染节点

    • 对组件的处理
    • 对普通DOM元素的处理
    对组件的处理
    processComponent函数实现
    • 用来处理组件
    const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
      if (n1 == null) {
       // 挂载组件
       mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      }
      else {
        // 更新组件
        updateComponent(n1, n2, parentComponent, optimized)
      }
    }
    
    • 如果n1null,则执行挂载组件的逻辑
    • 否则执行更新组件的逻辑
    mountComponent挂载组件的实现
    const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
      // 创建组件实例
      const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
      // 设置组件实例
      setupComponent(instance)
      // 设置并运行带副作用的渲染函数
      setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
    }
    

    主要做三件事情

    • 1、 创建组件实例
      • Vue.js 3.0虽然不像Vue.js 2.x那样通过类的方式去实例化组件,但内部也通过对象的方式去创建了当前渲染的组件实例
    • 2、 设置组件实例
      • instance保留了很多组件相关的数据,维护了组件的上下文,包括对props、插槽,以及其他实例的属性的初始化处理
    • 3、 设置并运行带副作用的渲染函数(setupRenderEffect)
    const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
      // 创建响应式的副作用渲染函数
      instance.update = effect(function componentEffect() {
        if (!instance.isMounted) {
          // 渲染组件生成子树 vnode
          const subTree = (instance.subTree = renderComponentRoot(instance))
          // 把子树 vnode 挂载到 container 中
          patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
          // 保留渲染生成的子树根 DOM 节点
          initialVNode.el = subTree.el
          instance.isMounted = true
        }
        else {
          // 更新组件
        }
      }, prodEffectOptions)
    }
    
    • 该函数利用响应式库的effect函数创建了一个副作用渲染函数 componentEffect

    副作用
    当组件的数据发生变化时,effect函数包裹的内部渲染函数 componentEffect会重新执行一遍,从而达到重新渲染组件的目的

    • 渲染函数内部也会判断这是一次初始渲染还是组件更新,在初始渲染流程中

    初始渲染主要做两件事情

    • 1、 渲染组件生成subTree
      注意,不要弄混subTree(执行renderComponentRoot生成的子树vnode)和initialVNode(组件 vnode
      • 每个组件都有render函数,template也会编译成render函数
      • renderComponentRoot函数就是去执行 render 函数创建整个组件树内部的 vnode
      • 把这个 vnode 再经过内部一层标准化,就得到了该函数的返回结果:subTree(子树vnode
    • 2、把subTree挂载到container
      • 继续调用 patch 函数把子树 vnode 挂载到 container
      • 继续对这个子树vnode类型进行判断,此时子树vnode为普通元素vnode
    对普通 DOM 元素的处理
    processElement函数
    • 用来处理普通DOM 元素
    const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
      isSVG = isSVG || n2.type === 'svg'
      if (n1 == null) {
        //挂载元素节点
        mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      }
      else {
        //更新元素节点
        patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
      }
    }
    
    • 如果n1null,走挂载元素节点的逻辑
    • 否则走更新元素节点逻辑
    mountElement 函数
    const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
      let el
      const { type, props, shapeFlag } = vnode
      // 创建 DOM 元素节点
      el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is)
      if (props) {
        // 处理 props,比如 class、style、event 等属性
        for (const key in props) {
          if (!isReservedProp(key)) {
            hostPatchProp(el, key, null, props[key], isSVG)
          }
        }
      }
      if (shapeFlag & 8 /* TEXT_CHILDREN */) {
        // 处理子节点是纯文本的情况
        hostSetElementText(el, vnode.children)
      }
      else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
        // 处理子节点是数组的情况
        mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', optimized || !!vnode.dynamicChildren)
      }
      // 把创建的 DOM 元素节点挂载到 container 上
      hostInsert(el, container, anchor)
    }
    
    • 主要做四件事
    • 1、 创建DOM元素节点
      通过hostCreateElement方法创建,这是一个平台相关的方法,在web端实现:
    // 调用了底层的 DOM API document.createElement 创建元素
    function createElement(tag, isSVG, is) {
      isSVG ? document.createElementNS(svgNS, tag)
        : document.createElement(tag, is ? { is } : undefined)
    }
    
    • 2、 处理props
      给这个DOM节点添加相关的 classstyleevent 等属性,并做相关的处理
    • 3、 处理children
      • 子节点是纯文本,则执行hostSetElementText方法,它在 Web环境下通过设置DOM元素的textContent属性设置文本:
    function setElementText(el, text) {
      el.textContent = text
    }
    
    * 如果子节点是数组,则执行`mountChildren`方法
    
    const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, start = 0) => {
      for (let i = start; i < children.length; i++) {
        // 预处理 child
        const child = (children[i] = optimized
          ? cloneIfMounted(children[i])
          : normalizeVNode(children[i]))
        // 递归 patch 挂载 child
        patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      }
    }
    
    • 遍历children获取到每一个child vnode

    • mountChildren函数的第二个参数是container,传入的是mountElement时创建的DOM节点,很好的建立了父子关系

    • 通过递归patch这种深度优先遍历树的方式,我们就可以构造完整的DOM树,完成组件的渲染。

    • 4、 挂载DOM元素到container
      调用hostInsert方法

    function insert(child, parent, anchor) {
      if (anchor) {
        parent.insertBefore(child, anchor)
      }
      else {
        parent.appendChild(child)
      }
    }
    
    嵌套组件的处理
    • mountChildren的时候递归执行的是 patch 函数,而不是 mountElement函数,这是因为子节点可能有其他类型的vnode,比如组件vnode

    组件渲染流程图

    image.png

    相关文章

      网友评论

          本文标题:【Vue3.0】- 如何渲染组件

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