为了账号安全,请及时绑定邮箱和手机立即绑定

Vue 进阶系列(三)之Render函数原理及实现

标签:
JavaScript

高级前端进阶(id:FrontendGaoji)

作者:木易杨,资深前端工程师,前网易工程师,13K star Daily-Interview-Question 作者

https://img1.sycdn.imooc.com//5d6cf80700019da106760453.jpg

这是Vue进阶系列的第三篇,前面两篇可以点击页尾左下角的“阅读原文”观看。


Render函数原理

根据第一篇文章介绍的响应式原理,如下图所示。


https://img1.sycdn.imooc.com//5d6cf81a0001ab3006830436.jpg


在初始化阶段,本质上发生在auto run函数中,然后通过render函数生成Virtual DOMview根据Virtual DOM生成Actual DOM。因为render函数依赖于页面上所有的数据data,并且这些数据是响应式的,所有的数据作为组件render函数的依赖。一旦这些数据有所改变,那么render函数会被重新调用。

在更新阶段,render函数会重新调用并且返回一个新的Virtual Dom,新旧Virtual DOM之间会进行比较,把diff之后的最小改动应用到Actual DOM中。

Watcher负责收集依赖,清除依赖和通知依赖。在大型复杂的组件树结构下,由于采用了精确的依赖追踪系统,所以会避免组件的过度渲染。


https://img1.sycdn.imooc.com//5d6cf8270001e9c906880395.jpg


Actual DOM 和 Virtual DOM

Actual DOM 通过document.createElement('div')生成一个DOM节点。

1document.createElement('div')
2
3// 浏览器原生对象(开销大)
4"[object HTMLDivElement]"

Virtual DOM 通过 vm.$createElement('div')生成一个JS对象,VDOM对象有一个表示div的tag属性,有一个包含了所有可能特性的data属性,可能还有一个包含更多虚拟节点的children列表。

1vm.$createElement('div')
2
3// 纯JS对象(轻量)
4{ tag: 'div', data: { attrs: {}, ...}, children: [] }

因为Virtual DOM的渲染逻辑和Actual DOM解耦了,所以有能力运行在的非浏览器环境中,这就是为什么Virtual DOM出现之后混合开发开始流行的原因,React Native 和 Weex能够实现的原理就是这个。

JSX和Template

JSX和Template都是用于声明DOM和state之间关系的一种方式,在Vue中,Template是默认推荐的方式,但是也可以使用JSX来做更灵活的事。

JSX更加动态化,对于使用编程语言是很有帮助的,可以做任何事,但是动态化使得编译优化更加复杂和困难。

Template更加静态化并且对于表达式有更多约束,但是可以快速复用已经存在的模板,模板约束意味着可以在编译时做更多的性能优化,相对于JSX在编译时间上有着更多优势。

实例1:实现example组件

要求使用如下

1<example :tags="['h1', 'h2', 'h3']"></example>

要求输出如下

1<div>
2  <h1>0</h1>
3  <h2>1</h2>
4  <h3>2</h3>
5</div>

上面这个需求可以通过render函数来做,官方提供了createElement 函数用来生成模板。createElement('div', {}, [...])可接受的参数如下。

 1// @returns {VNode}
2createElement(
3  // {String | Object | Function}
4  // 一个 HTML 标签字符串,组件选项对象,或者
5  // 解析上述任何一种的一个 async 异步函数。必需参数。
6  'div',
7
8  // {Object}
9  // 一个包含模板相关属性的数据对象
10  // 你可以在 template 中使用这些特性。可选参数。
11  {
12
13  },
14
15  // {String | Array}
16  // 子虚拟节点 (VNodes),由 `createElement()` 构建而成,
17  // 也可以使用字符串来生成“文本虚拟节点”。可选参数。
18  [
19    '先写一些文字',
20    createElement('h1', '一则头条'),
21    createElement(MyComponent, {
22      props: {
23        someProp: 'foobar'
24      }
25    })
26  ]
27)

知道了用法之后,就可以在render中返回createElement生成的虚拟节点,外层是div,内层是三个锚点标题h1 h2 h3,所以内层需要遍历下,使用两个createElement就可以完成了。

通常使用h作为createElement的别名,这是Vue的通用惯例,也是JSX的要求。

实现如下

 1<!--引用-->
2<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="../node_modules/vue/dist/vue.js"></script>
3
4<!--定义template -->
5<div id="app">
6  <example :tags="['h1', 'h2', 'h3']"></example>
7</div>
8
9<script>
10    // 定义example组件
11    Vue.component('example', {
12      props: ['tags'],
13      render (h) {
14
15        // 第二个参数是一个包含模板相关属性的数据对象,可选参数
16
17        // 子虚拟节点(VNodes)参数可以传入字符串或者数字,
18        // 通过createElement生成,可选参数
19        return h('div', this.tags.map((tag, i) => h(tag, i)))
20      }
21    })
22
23    // 实例化
24    new Vue({ el: '#app' })
25</script>

实例2:实现动态的``组件

要求如下

  • 实现一个Foo组件渲染

    foo

    ,实现一个Bar组件渲染

    bar



  • 实现一个组件,根据属性ok动态渲染Foo组件或者Bar组件。如果属性oktrue,那么最终的渲染应该是

    foo


  • 实现一个按钮控制属性ok,通过这个属性让Foo或者Bar之间切换。

根据上面的要求,在模板中调用<example>组件,然后定义<button>组件,同时绑定属性ok

实现如下

 1<!--引用-->
2<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="../node_modules/vue/dist/vue.js"></script>
3
4<!--定义template -->
5<div id="app">
6
7  <!--绑定属性ok-->
8  <example :ok="ok"></example>
9
10  <!--绑定点击事件-->
11  <button @click="ok = !ok">toggle</button>
12</div>
13
14<script>
15    // 定义Foo
16    const Foo = {
17      render (h) {
18        return h('div', 'foo')
19      }
20    }
21
22    // 定义Bar
23    const Bar = {
24      render (h) {
25        return h('div', 'bar')
26      }
27    }
28
29    // 定义example组件
30    // 根据ok属性动态切换
31    Vue.component('example', {
32      props: ['ok'],
33      render (h) {
34        return h(this.ok ? Foo : Bar)
35      }
36    })
37
38    // 实例化
39    new Vue({
40      el: '#app',
41      data: { ok: true }
42    })
43</script>

实例3:实现组件

要求如下

  • 实现一个withAvatarURL函数,要求传入一个带有url属性的组件,返回一个接收username属性的高阶组件,这个高阶组件主要负责获取相应的头像URL。

  • 在API返回之前,高阶组件将占位符URLhttp://via.placeholder.com/200x200传递给内部组件。

例子如下

1const SmartAvatar = withAvatarURL(Avatar)
2
3// 使用这个方式
4<smart-avatar username="vuejs"></smart-avatar>
5
6// 替换下面的方式
7<avatar url="/path/to/image.png"></avatar>
8

withAvatarURL函数返回一个对象,接收username属性,在生命周期created获取头像URL。Avatar对象接收src属性,src的内容从withAvatarURL中获取,然后展示在上。实例化的时候,传入新定义的组件名SmartAvatar

实现如下

 1<!--引用-->
2<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="../node_modules/vue/dist/vue.js"></script>
3
4<!--定义template-->
5<div id="app">
6  <smart-avatar username="vuejs"></smart-avatar>
7</div>
8
9<script>
10    // 获取头像URL
11    function fetchURL (username, cb) {
12      setTimeout(() => {
13        // 获取头像并回传
14        cb('https://avatars3.githubusercontent.com/u/6128107?v=4&s=200')
15      }, 500)
16    }
17
18    // 传递的InnerComponent
19    const Avatar = {
20      props: ['src'],
21      template: `<img :class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="src">`
22    }
23
24    function withAvatarURL (InnerComponent) {
25      return {
26        props: ['username'],
27        inheritAttrs: false, // 2.4 only,组件将不会把未被注册的props呈现为普通的HTML属性
28        data () {
29          return { url: null }
30        },
31        created () {
32          // 获取头像URL并回传给this.url
33          fetchURL(this.username, url => {
34            this.url = url
35          })
36        },
37        render (h) {
38          return h(InnerComponent, {
39            attrs: this.$attrs, // 2.4 only,获取到没有使用的注册属性
40            props: {
41              src: this.url || 'http://via.placeholder.com/200x200'
42            }
43          })
44        }
45      }
46    }
47
48    const SmartAvatar = withAvatarURL(Avatar)
49
50    // 实例化,新构造组件名为SmartAvatar或smart-avatar
51    new Vue({
52      el: '#app',
53      components: { SmartAvatar }
54    })
55</script>


点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消