学习vue源码(10)手写render渲染函数__Vue.js
发布于 3 年前 作者 banyungong 1007 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

compile 部分已经讲完了

compile部分分为 解析器 + 优化器+ 代码生成器),

终于走到了 render,今天就来给自己记录下渲染三部曲的第二部render,

渲染三部曲= compile + render生成Vnode + 将Vnode通过 update 挂载到 页面上),

update里 有一系列 diff操作。

咦,render 内容不多的

噔噔噔噔

render 的作用大家应该清楚

就是 执行 compile 生成的 render函数,然后得到返回的 vnode 节点

比如现在存在这个简单的模板

经过 compile 之后,解析成了对应的 render 函数,如下

function render() { with(this) { return _c('div', { attrs: { "data": 111 } }, [_v(111)]) } }

看着这个莫名其妙的 render 函数,里面都是些什么东西?

不怕,主要是出现了两个函数,我们要探索的就是这两个东西

_c , _v

这个两个函数的作用,都是创建 Vnode,但是创建的过程不一样

并且 render 函数执行的时候,会绑定上 模板对应的实例 为上下文对象

模板是属于哪个实例的,就绑定哪个实例

render.call(实例) 再通过 with 的作用

调用 _c 和 _v 就相当于 vm._c 和 vm._v

什么是 vm._v

现在就来看看 vm._v 是哪里来的

function installRenderHelpers(target) { target._v = createTextVNode; } installRenderHelpers(Vue.prototype);

由上面可知,每个Vue 实例都会继承有 _v 这个方法,所以可以通过 vm._v 直接调用

再来看看 _v 对应的 createTextVNode 的作用是什么

创建文本节点!!

看下源码

function createTextVNode(val) { return new VNode( undefined, undefined, undefined, String(val) ) }

比如这个模板

{{data}} 虽然是字符串,但是也要作为一个子节点存在,所以就当做是 文本节点

而 data 的值是 111

然后 上面的模板就会得到这样的 Vnode 结构如下

什么是 vm._c

_c 是一个大头,render 的重中之重,先来看看他是怎么来的

function initRender(vm) { vm._c = function(a, b, c, d) { return createElement(vm, a, b, c, d); }; } Vue.prototype._init = function(options) { initRender(this) }

在实例初始化的时候,就会给实例绑定上 _c 方法

所以,vm 可以直接调用到 _c

看了上面的源码,看到 _c 内部调用了 createElement

那就来看看createElement 的源码吧

个人已经简化得非常简单,觉得不偏离我们的主题就可以

function createElement( context, tag, data, children ) { return _createElement( context, tag, data, children ) }

function _createElement( context, tag, data, children ) { var vnode; if (如果tag是正常html标签) { vnode = new VNode( tag, data, children, undefined, undefined, context ); } .....如果tag是组件名,就特殊处理 ,处理流程已经省略 if (Array.isArray(vnode)) return vnode else { // ...动态绑定 style ,class,代码已经省略 return vnode } }

你一看就可以看到,createElement 主要就是调用了 new VNode,当然了,render 就是为了创建 vnode 的嘛

你在前面也看到了 render 函数,有传了很多参数给 _c,如下,_c 再把这些参数传给构造函数 VNode

_c('div', { attrs: {"data": 111} }, [_v(111)] )

上面这些参数都会传给 Vnode,并保存在创建的 Vnode 中

function VNode( tag, data, children, text ) { this.tag = tag; this.data = data; this.children = children; this.text = text; }

然后得到这么一个 Vnode

{ tag:"div", data:{ attrs: {"data": 111} }, children:[{ tag:undefined, data:undefined, text:111 }] }

说到这里,已经能很清楚 render 内部是如何创建Vnode 了

但是这里只是其中一种小小的简单 render

要是项目中的render,数据是很多,很复杂的

而我们主要要把握的是主要流程就可以了

不过,还有必要记录其他 render,那就是遍历

遍历相关

看下面这个 template

解析成下面的render

function render() { with(this) { return _c('div', _l(2,function(item, index) { return _c('span') }) ) } }

看到一个 _l, 他必定就是遍历生成 Vnode 的幕后黑手了

同样的,_l 和 _v 在同一个地方 installRenderHelpers 注册的

function installRenderHelpers(target) { target._l = renderList; }

不客气地搜出 renderList 源码出来

先跳到后面的分析啊,源码有点长了,虽然很简单

function renderList(val, _render) { var ret, i, l, keys, key; // 遍历数组 if ( Array.isArray(val) ) { ret = new Array(val.length); // 调用传入的函数,把值传入,数组保存结果 for (i = 0, l = val.length; i < l; i++) { ret[i] = _render(val[i], i); } } // 遍历数字 else if (typeof val === 'number') { ret = new Array(val); // 调用传入的函数,把值传入,数组保存结果 for (i = 0; i < val; i++) { ret[i] = _render(i + 1, i); } } // 遍历对象 else if (typeof val =="object") { keys = Object.keys(val); ret = new Array(keys.length); // 调用传入的函数,把值传入,数组保存结果 for (i = 0, l = keys.length; i < l; i++) { key = keys[i]; ret[i] = _render(val[key], key, i); } } // 返回 vnode 数组 return ret }

看到 renderList 接收两个参数,val 和 render,而 _l 调用的时候,也就是传入的这两个参数,比如下面

_l(2,function(item, index) { return _c('span') })

val 就是 2,_render 就是上面的函数

1 遍历的数据 val

遍历的数据分为三种类型,一种是对象,一种是数字,一种是数组

2 单个 vnode 渲染回调 _render

重要是这个回调

1、renderList 每次遍历都会执行回调,并把的每一项 item 和 index 都传入 回调中

2、回调执行完毕,会返回 vnode

3、使用数组保存 vnode,然后 遍历完毕就返回 数组

于是可以看上面的 render 函数 ,传入了 数字2,和 创建 span 的回调

_l(2,function(item, index) { return _c('span') })

_l 执行完毕,内部遍历两次,最后返回 两个 span vnode 的数组,然后传给外层的 _c ,作为 vnode.children 保存

render 执行完毕,得到这样的 vnode

{ tag:"div", data:undefined, children:[{ tag:"span", data:undefined },{ tag:"span", data:undefined }] }

都灰常简单啊,没写之前,我还觉得内容应该挺多的,写完发现还可以

当然还有其他的 render

比如要模板含有 filter,我们来看看

Filters - 源码版

下面的讲解会以下面例子 作为讲解模板

这里有一个过滤器 all,用来过滤 parentName

<div>{{parentName|all }}</div> new Vue({ el:document.getElementsByTagName("div")[0], data(){ return { parentName:111 } }, filters:{ all(){ return "我是过滤器" } } })

页面的 filter 解析成什么

首先,上面的例子会被解析成下面的渲染函数

(function() { with(this) { return _c('div',[ _v(_s(_f("all")(parentName))) ]) } }

这段代码继续解释下

  1. _c 是渲染组件的函数,这里会渲染出根组件
  2. 这是匿名自执行函数,后面渲染的时候调用,会 绑定当前实例为作用域
  3. with 的作用是,绑定大括号内代码的 变量访问作用域,所以里面的所有变量都会从 实例上获取

然后,你可以看到 ' parentName | all ' 被解析成 _f('all')( parentName )

怎么解析的?

简单说就是,当匹配到 | 这个符号,就知道你用过滤器,然后就解析成 _f 去获取对应过滤器 并调用,这个过程不赘述

_f 是什么?

_f 是获取具体过滤器的函数

1_f 会在Vue 初始化的时候,注册到 Vue 的原型上

// 已简化 function installRenderHelpers(target) { target._s = toString; target._f = resolveFilter; } installRenderHelpers(Vue.prototype);

所在在 上面的 渲染函数 with 绑定当前实例vm为作用域 之后,_f 从vm 获取,成了这样 vm._f

_f 是 resolveFilter,一个可以获取 具体filter 的函数

使用 _f(“all”) 就能获取到 all 这个过滤器,resolveFilter 下面会说

怎么获取下面继续…

设置的 filter 如何被调用

由上面可以看到,_f 是 resolveFilter 赋值的,下面是 resolveFilter 源码

// 已简化 function resolveFilter(id) { return resolveAsset( this.$options, 'filters', id, true ) || identity }

要是你看过学习vue源码(3) 手写Vue.directive、Vue.filter、Vue.component方法,相信这里你很熟悉,其实就是从this.options.filters里找对应的过滤器函数来调用,如图所示

this.options 会拿到当前组件的所有选项

你问我为什么?

根据上一个问题知道

  1. _f 会使用 实例去调用 ,vm._f 类等 vm.resolveFilter
  2. 所以,resolveFilter 的 执行上下文 this 是 vm
  3. 所以,this.$options 就是 实例的 options 啦

接着,调用 resolveAsset ,目的就是拿到 组件选项中的 具体 filter

传入 当前组件的选项 ,指定要其选项 filters ,指定具体 filter 名

function resolveAsset( options, type, id, warnMissing ) { // g:拿到 filters 选项 var assets = options[type]; // g:返回 调用的 filter return assets[id] }

_f(“all”) 流程 就成了下面这样

  1. 拿到 组件选项 中的 filters
  2. 然后再从 filters 中,拿到 all 这个filter
  3. 执行返回的 all 过滤函数时,传入需要过滤的值 parentName
  4. 得到 返回了 过滤后的值

所以,当渲染函数解析的时候,碰到使用过滤器的地方,按流程拿到过滤值后,就可以渲染到页面上了

_f(“all”)(parentName)) 就会变成 “我是过滤器” 放到 渲染函数中,最后,就是渲染到页面了

总结

fitler 其实就是从组件选项 filters 获取你设置的某个filter,并调用,然后使用你函数执行的返回值渲染

太简单了,总结跟没总结一样…

render 什么时候开始执行?

如果你看过学习vue源码(4) 手写vm.$mount方法,我相信你已经知道了

如图所示,是在挂载阶段执行的。

总结

每个模板经过 compile 都会生成一个 render 函数

render 作为 渲染三部曲的第二部,主要作用就是 执行 render,生成 Vnode

把 template 上绑定的数据,都保存到 vnode 中

然后,生成 Vnode,就是为了给 渲染三部曲的 第三部 Diff 提供源动力

从而完成 DOM 挂载

到这里其实基本就已经结束了render的思路,但是源码中有个静态render,这个 对渲染性能的提高有极大的帮助,所以必须看下。

没错,就是 静态 render,看过学习vue源码(8)手写优化器的人,应该知道什么是 静态 render

静态 render 就是用于渲染哪些不会变化的节点

大家可以先看看,Vue 是怎么判断某个节点是否是静态节点

好,下面开始我们的正文,想了想,我们还是以几个问题开始吧

1、静态 render 是什么样子的

2、静态 render 是怎么生成和 保存

3、静态 render 怎么执行

什么是 静态Render

静态 render 其实跟 render 是一样的,都是执行得到 Vnode

只是静态 render,没有绑定动态数据而已,也就是说不会变化

比如说,一个简单 render 是这样的

绑定了动态数据,需要从实例去获取

_c('div',[_v(_s(aa))])

而静态 render 是这样的

没有动态数据,这个静态render 的执行结果是永远不会变的

_c('div',[_c('span',[_v("1")])])

生成保存静态Render

静态 render 是在 generate 阶段生成的,生成的方式和 render 是一样的

比如在一个模板中,有很多个静态 根节点,像这样

首先,Vue 会在遍历模板的时候,发现 span 和 strong 本身以及其子节点都是静态的

那么就会给 span 和 strong 节点本身设置一个属性 staticRoot,表示他们是静态根节点

然后这两个静态根节点就会生成自己专属的 静态 render

如果你有一直看我的Vue 笔记的话,你应该这里是会有点印象的

之后

静态 render 生成之后是需要保存的,那么保存在哪里呢?

保存在一个数组中,名叫 staticRenderFns,就是直接push 进去

当然了,此时的 push 进去的 静态 render 还是字符串,并没有变成函数

以上面的模板为例,这里的 staticRenderFns 就是这样,包含了两个字符串

staticRenderFns = [ "_c('span',[_c('b',[_v("1")])])", "_c('strong',[_c('b',[_v("1")])])" ]

但是在后面会逐个遍历变成可执行的函数

staticRenderFns = staticRenderFns.map(code => { return new Function(code) });

那么 这个 staticRenderFns 又是什么啊?

每个 Vue 实例都有一个独立的 staticRenderFns,用来保存实例本身的静态 render

staticRenderFns 的位置是

vm.$options.staticRenderFns

执行静态Render

静态 render 需要配合 render 使用,怎么说

看个例子

这个模板的 render 函数是

_c('div',[ _m(0), _v(_s(a), _m(1) ])

_m(0) , _m(1) 就是执行的就是 静态 render 函数,然后返回 Vnode

于是 render 也可以完成 vnode 树的构建了

那么 _m 是什么呢?

在 Vue 初始化时,给Vue的原型便注册了这个函数,也就是说每个实例都继承到 _m

function installRenderHelpers(target) { target._m = renderStatic; } installRenderHelpers(Vue.prototype);

再来看 renderStatic

function renderStatic(index) { var cached = this._staticTrees || (this._staticTrees = []); var tree = cached[index]; // 如果缓存存在,就直接返回 if (tree) return tree // 这里是执行 render 的地方 tree = cached[index] = this.$options.staticRenderFns[index].call( this, null, this ); // 只是标记静态 和 节点id 而已 markStatic(tree, "__static__" + index, false); return tree }

这个函数做的事情可以分为几件

1、执行静态render

2、缓存静态render 结果

3、标记 静态 render 执行得到的 Vnode

我们来一个个说

**1 执行静态render **

上面我们说过了,静态render 保存在 数组 staticRenderFns

所以这个函数接收一个索引值,表示要执行数组内哪个静态render

取出静态render 后,执行并绑定 Vue 实例为上下文对象

然后得到 Vnode

2 缓存静态render 结果

这一步就是要把上一步得到的 Vnode 缓存起来

那么缓存在哪里呢?

_staticTrees

这是一个数组,每个实例都会有一个独立的 _staticTrees,用来存在自身的静态 render 执行得到的 Vnode

看一下上个模板中实例保存的 _staticTrees

3 标记 静态 render 执行得到的 Vnode

我们已经执行静态render得到了 Vnode,这一步目的是标记

标记什么呢

1、添加标志位 isStatic

2、添加 Vnode 唯一id

renderStatic 中我们看到标记的时候,调用了 markStatic 方法,现在就来看看

function markStatic( tree, key ) { if (Array.isArray(tree)) { for (var i = 0; i < tree.length; i++) { if ( tree[i] && typeof tree[i] !== 'string') { var node = tree[i] node.isStatic = true; node.key = key + "_" + i; } } } else { tree.isStatic = true; tree.key = key } }

为什么添加标志位 isStatic?

前面我们添加的所有静态标志位都是针对 模板生成的 ast

这里我们是给 Vnode 添加 isStatic,这才能完成Vue的目的

Vue 目的就是性能优化,在页面改变时,能尽量少的更新节点

于是在页面变化时,当 Vue 检测到该 Vnode.isStatic = true,便不会比较这部分内容

从而减少比对时间

Vnode 唯一id

每个静态根Vnode 都会存在的一个属性

我也没想到 静态Vnode 的 key 有什么作用,毕竟不需要比较,也许是易于区分??

最后

静态 render 我们就讲完了,是不是很简单,在没看源码之前,我以为很难

现在看完,发现也简单的,不过我也是看了几个月的。。。。

鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,领取红包

本文使用 mdnice 排版

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 阳光是sunny 原文链接:https://juejin.im/post/6844904184903385102

回到顶部