vue原理学习系列(三):渲染函数(render funciton)和Virtual Dom

639 阅读7分钟

前言

vue原理学习系列,本期为主要学习下vue的渲染函数render function运作原理,和Virtual Dom是什么,欢迎大家参考和提出意见。

vue原理学习系列往期文章:

「内容速览」

  • render function介绍
  • 响应性和渲染函数的结合
  • virtual dom
  • 动态渲染标签
  • 动态渲染组件
  • 高阶组件

下面的内容是基于vue2来进行展开的

render function介绍

render function(渲染函数)实际上是一个返回虚拟节点 (virtual node)的函数,整个vnode树称为Virtual Dom,Vue基于虚拟dom生成真实的dom。

vue生成虚拟dom的过程本质上是调用渲染函数,在vue实例中渲染函数和data属性具有依赖关系,同时由于这些data属性是具有响应性的,因此这些data属性会帮助组件的渲染函数收集依赖,当这些依赖关系发生变化时,就会再次调用渲染函数,返回一个新的虚拟dom。

旧的虚拟dom会和新的虚拟dom进行比较和区分,也就是常说的diff算法。

render函数需要一个参数createElement,返回createElement创建virtual node

render: function(createElement) {
  return createElement()
}

此外createElement还有一个别名h,这是vue中通用的写法。

render: function(h) {
  return h()
}

当我们申明组件为函数式组件时(函数式组件没有this),render会提供context作为第二个参数来获取上下文。

render: function(h, context) { //当组件申明 functional: true
  return h()
}

响应性和渲染函数的结合

上面的图片很直观说明了响应性和渲染函数的运行流程,每一个组件都有自己的一个渲染函数,而这个渲染函数实际上是包装在之前(响应性)中实现的autorun函数,当组件渲染的时候,vue会通过调用data属性的getter来收集它们的依赖项,当调用属性setter方法会触发通知的执行。

这里我们可以看到还有一个额外的概念watcher(观察者),每个组件都有一个负责监视的观察者,它们主要作用保存收集的依赖项,通知所有内容,来触发重新渲染。

渲染函数则返回虚拟dom树。

整个流程是循环的,因为渲染函数是放在autorun中,只要我们依赖的渲染属性发生变化,渲染就会被反复调用。

每个组件都有自己的自动循环渲染,组件树由许多这些组件构成,每个组件都只负责自己的依赖。因此当组件树较大的时候,这实际上是一个优势,我们可以在任意地方对数据进行修改,因为在组件树中每个组件只负责自己那部分依赖,我们可以确切知道那些组件受到哪些数据影响,通过精确的依赖跟踪系统,不会造成过多的组件重新渲染。

virtual dom

  • actual dom(真实dom):

例如调用document.createElement('div')创建一个真实div节点并插入到文档流中,这种原生DOM API实际上是通过c++编写的浏览器引擎实现的。一个真实DOM节点会包含很多属性,它的底层实现很复杂,但我们可以通过原生代码的javascript接口来进行很多dom操作。

  • virtual dom(虚拟dom):

所谓虚拟dom,实际上是一个用于表示真实dom结构和属性的javascript对象。

在vue中,Virtual dom就是由 VNode节点构建的树,VNode可以理解为vue框架的虚拟dom的基类,每个DOM元素或组件都对应一个VNode对象,它包含的信息告诉 Vue 页面上需要渲染什么样的节点,节点属性,和其子节点的描述信息。

// 一个div标签
// Actual DOM
"[object HTMLDivElement]"

// Virtual DOM
{ tag: 'div', data: { attrs: {}, ... }, children: [] }

那么virtual dom的特点和相较和actual dom优势在哪?

  • 资源消耗: 虚拟DOM比真实DOM更节省资源。假设有1000个元素的列表,创建1000个javascript对象是相较于创建1000个div节点是非常节省,也是更快的。

虚拟DOM本质上是轻量javacript数据,用来表示真实DOM在特定时间的外观。vue能够在每次更新时生成虚拟DOM的副本正是因为虚拟DOM比真实DOM节省。

  • 性能和效率: 假设我们通过innerHtml去更新应用,此时我们需要丢弃之前的DOM节点,再从新生成所有的DOM节点,如果只有某一行数据改变了,重置整个innerHTML的代价无疑是巨大的。而虚拟DOM我们只需要修改对象的属性,并计算它们差异,再将这些更改应用到DOM上,性能上是具备优势的:

virtual dom使用能够减少页面操作,对于页面的操作,比较VNode的区别,在最后一步才对真实dom进行更新,减少dom频繁操作,提高性能。

尤大对InnerHTML和Virtual DOM 的重绘性能消耗比较:

  • innerHTML: render html string O(template size) + 重新创建所有 DOM 元素 O(DOM size)

  • Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)

  • 函数式的UI编程: virtual DOM为函数式的 UI 编程方式打开了大门。

  • 虚拟DOM的架构优势: virtual DOM让我们可以渲染到 DOM 以外的 backend。virtual DOM是抽象的javascript节点,我们可以创建相同应用程序虚拟运行在任何支持javascript的环境中,可以是原生渲染引擎,例如(Ios,Andriod),让虚拟DOM进行原生渲染,使得React Native、Native script成为可能。

动态渲染标签

编写一个组件,根据组件的tags属性,利用渲染函数渲染出相应的HTML标签:

// 实现目标
Implement the "example" component which given the following usage:
<example :tags="['h1', 'h2', 'h3']"></example>

which renders the expected output:
<div>
  <h1>0</h1>
  <h2>1</h2>
  <h3>2</h3>
</div>
<div id="app">
  <example :tags="['h1', 'h2', 'h3']"></example>
</div>

<script>
Vue.component('example', {
  functional: true,
  // 验证props
  props: {
    tags: {
      type: Array,
      validator(arr) { return !!arr.length; }
    }
  },
  render: (h, context) => {
    const tags = context.props.tags;
    // tags.map 返回动态渲染的children
    return h('div', context.data, tags.map((tag, index) => h(tag, index)));
  }
})
</script>

动态渲染组件

渲染函数除了渲染html标签以外,还可以渲染组件,根据ok的值渲染Foo和Bar:

<div id="app">
  <example :ok="ok"></example>
  <button @click="ok = !ok"></button>
</div>

<script>
const Foo = {
  functional: true,
  render: h => h('div', 'foo')
}
const Bar = {
  functional: true, 
  render: h => h('div', 'bar')
}
Vue.component('example', {
  functional: true,
  props: {
    ok: Boolean
  },
  // ok为true渲染Foo,为false渲染Bar
  render: (h, context) => h(context.props.ok ? Foo: Bar)
})

// ok值随button点击改变
new Vue({
  el: '#app',
  data: {
    ok: true
  }
})
</script>

高阶组件

  • 高阶函数:

指对其他函数进行操作的函数,这里的操作可以是将函数作为参数,或将函数作为返回值。简单来说,就是一个函数的参数或返回值为函数称为高阶函数。

  • 高阶组件

高阶组件同样是一个函数(不是组件),它接受一个组件作为参数并返回一个新组件,利用高阶组件我们将组件中某些相同逻辑抽离出来,通过props来给需要进行包装的组件传递数据。利用高阶组件的灵活性,我们能够更好复用组件。

  • 高阶组件实现 实现目标:avatar是一个接受src属性来展示头像的组件,现在希望实现SmartAvatar组件,组件接受用户名字,返回对于用户头像URL。
<div id="app">
  <smart-avatar username="vuejs"></smart-avatar>
</div>

<script>
function fetchURL (username, cb) {
  setTimeout(() => {
    cb('https://avatars3.githubusercontent.com/u/6128107?v=4&s=200')
  }, 500)
}

const Avatar = {
  props: ['src'],
  template: `<img :src="src">`
}

// 高阶组件
function withAvatarURL (InnerComponent) {
  return {
    props: {
      username: String
    },
    data() {
      return {
        url: 'http://via.placeholder.com/200x200'
      }
    },
    created() {
      fetchURL(this.username, (url) => { this.url = url })
    },
    render(h) {
      return h(InnerComponent, {
        props: {
          src: this.url
        }
      })
    } 
  }
}

const SmartAvatar = withAvatarURL(Avatar);
new Vue({
  el: '#app',
  components: {
    SmartAvatar
  }
})
</script>
  • 高阶组件和mixin的区别和选择:

实际刚刚上面的高阶组件所实现的功能通过mixin也能够实现,那么高阶组件和mixin有什么区别呢?

  1. 高阶组件更具有重用性,特别是在我们想把组件应用在很多地方,高阶组件实际上只是调用了原始组件,并没有修改原始组件的内容。

  2. 高阶组件更易于测试,高阶组件没有逻辑上耦合,对于高阶组件和原始组件可以分别测试,可以分为渲染和数据获取逻辑这两个部分。

  3. 实际上高阶组件和mixin应看场景来使用,过多使用高阶组件,会使得组件层级变得复杂,难以弄清层叠关系,多层嵌套时数据可能需要经过多层传递,此外嵌套多层也会导致一些性能的开销。

自己前端博客:前端学习笔记

参考链接:

www.zhihu.com/question/31…

zhuanlan.zhihu.com/p/136784818

www.cnblogs.com/tommymarc/p…

zhuanlan.zhihu.com/p/85809958