最近開了一個讀者回饋表單郵箱,無論是對文章的感想或是對部落格的感想,有什麼想回饋的都可以發郵箱跟我說:i_kkkp@163.com

Vuejs 组件的实现原理

前言

我们在前面讨论过了vue渲染器渲染器主要负责将虚拟 DOM 渲染为真实 DOM ,我们只需要使用虚拟 DOM来描述最终呈现的内容即可。

但当我们编写比较复杂的页面时,用来描述页面结构的虚拟 DOM 的代码量会变得越来越多,或者说页面模板会变得越来越大。这时,我们就需要组件化的能力。

有了组件,我们就可以将一个大的页面拆分为多个部分,每一个部分都可以作为单独的组件,这些组件共同组成完整的页面。组件化的实现同样需要渲染器的支持

渲染组件

从用户的角度来看,一个有状态组件就是一个选项对象,如下面的代码所示:

// MyComponent 是一个组件,它的值是一个选项对象
const MyComponent = {
name: 'MyComponent',
  data(){ 
  return  {foo: 1 }
  }
}

但是,如果从渲染器的内部实现来看,一个组件则是一个特殊类型的虚拟 DOM 节点。例如,为了描述普通标签,我们用虚拟节点的vnode.type 属性来存储标签名称,如下面的代码所示:

 // 该 vnode 用来描述普通标签
const vnode = {
type: 'div'
// ...
}

为了描述片段,我们让虚拟节点的 vnode.type 属性的值为Fragment, 而为了描述文本,我们让虚拟节点的 vnode.type 属性的值为Text,

还记得我们之前在渲染器讨论过的patch函数吗?

在patch函数中,Vue会根据新旧虚拟DOM节点的差异进行不同的处理逻辑。 如果是首次渲染,会执行首次渲染的逻辑;如果是更新渲染,会根据差异更新实际的DOM节点。

function patch(n1, n2, container, anchor) {
  // 02. 如果新旧节点类型不同,执行卸载操作
  if (n1 && n1.type !== n2.type) {
    unmount(n1);
    n1 = null;
  }

  // 07. 获取节点类型
  const { type } = n2;

  // 09. 判断节点类型
  if (typeof type === 'string') {
    // 10. 处理普通元素
    // TODO: 处理普通元素的逻辑
  } else if (type === Text) {
    // 11. 处理文本节点
    // TODO: 处理文本节点的逻辑
  } else if (type === Fragment) {
    // 13. 处理片段
    // TODO: 处理片段的逻辑
  }

  // 返回新节点的实际DOM元素
  return n2.el;
}

可以看到,渲染器会使用虚拟节点的 type 属性来区分其类型。对于不同类型的节点,需要采用不同的处理方法来完成挂载和更新。

实际上,对于组件来说也是一样的。为了使用虚拟节点来描述组件,我们可以用虚拟节点的 vnode.type 属性来存储组件的选项对象

function patch(n1, n2, container, anchor) {
  // 02. 如果新旧节点类型不同,执行卸载操作
  if (n1 && n1.type !== n2.type) {
    unmount(n1);
    n1 = null;
  }

  // 07. 获取节点类型
  const type = n2.type;

  // 09. 判断节点类型
  if (typeof type === 'string') {
    // 10. 处理普通元素
    // TODO: 处理普通元素的逻辑
  } else if (type === Text) {
    // 11. 处理文本节点
    // TODO: 处理文本节点的逻辑
  } else if (type === Fragment) {
    // 13. 处理片段
    // TODO: 处理片段的逻辑
  } else if (typeof type === 'object') {
    // 16. vnode.type 的值是选项对象,作为组件来处理
    if (!n1) {
      // 17. 挂载组件
      mountComponent(n2, container, anchor);
    } else {
      // 21. 更新组件
      patchComponent(n1, n2, anchor);
    }
  }

  // 返回新节点的实际DOM元素
  return n2.el;
}

在上面这段代码中,我们新增了一个 else if 分支,用来处理虚拟节点的 vnode.type 属性值为对象的情况,即将该虚拟节点作为组件的描述来看待,并调用 mountComponent 和 patchComponent 函数来完成组件的挂载和更新。

上面我们回顾了有关于组件的一些基本概念,组件的挂载和更新是如何实现的,其实我们更关注的还应该是用户应该如何编写组件?组件的选项对象必须包含哪些内容?以及组件拥有哪些能力?

因此,一个组件必须包含一个渲染函数,即 render 函数,并且渲染函数的返回值应该是虚拟 DOM。换句话说,组件的渲染函数就是用来描述组件所渲染内容的接口,如下面的代码所示:

// MyComponent 组件定义
const MyComponent = {
  // 组件名称,可选
  name: 'MyComponent',

  // 组件的渲染函数,其返回值必须为虚拟 DOM,一个组件必须包含一个渲染函数,即 render 函数,并且渲染函数的返回值应该是虚拟 DOM
  render() {
    // 返回虚拟 DOM
    return {
      type: 'div',
      children: ['text content']
    };
  }
};

// 用来描述组件的 vnode 对象,type 属性值为组件的选项对象
const CompNode = {
  type: MyComponent
};

// 调用渲染器来渲染组件
renderer.render(CompNode, document.querySelector('#app'));

// 渲染器中真正完成组件渲染任务的是 mountComponent 函数
function mountComponent(vnode, container, anchor) {
  // 通过 vnode 获取组件的选项对象,即 vnode.type
  const componentOptions = vnode.type;

  // 获取组件的渲染函数 render
  const render = componentOptions.render;

  // 执行渲染函数,获取组件要渲染的内容,即 render 函数返回的虚拟 DOM
  const subTree = render();

  // 最后调用 patch 函数来挂载组件所描述的内容,即 subTree
  patch(null, subTree, container, anchor);
}

总结

我们现在来回顾一下组件的基础,我们显式声明了一个组件的对象实例,该组件的选项对象必须包含一个渲染函数,即 render 函数,并且渲染函数的返回值应该是虚拟 DOM,之后我们再调用render和patch进行组件的更新和替换。换句话说,组件的渲染函数就是用来描述组件所渲染内容的接口,而渲染器的作用就是将组件的渲染函数的返回值渲染为真实 DOM。

Vuejs 组件事件与 emit 的实现 浏览器默认行为

評論