vue 源码学习三 vue中如何生成虚拟DOM
vm._render 生成虚拟dom
我们知道在挂载过程中, $mount 会调用 vm._update和vm._render 方法,vm._updata是负责把VNode渲染成真正的DOM,vm._render方法是用来把实例渲染成VNode,这里的_render是实例的私有方法,和前面我们说的vm.render不是同一个,先来看下vm._render定义,vm._render是通过renderMixin(Vue)挂载的,定义在src/core/instance/render.js :
// 简化版本
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
...
// render self
let vnode
try {
// _renderProxy生产环境下是vm
// 开发环境可能是proxy对象
vnode = render.call(vm._renderProxy, vm.$createElement) // 近似vm.render(createElement)
} catch (e) {...}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {...}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
- 先缓存
vm.$options.render和vm.$options._parentVnode,vm.$options.render是在上节的$mount中通过comileToFunctions方法将template/el编译来的。 vnode = render.call(vm._renderProxy, vm.$createElement)调用了render方法,参数是vm._renderProxy,vm.$createElement- 拿到
vnode后,判断类型是否为VNode,如果有多个vnode,则是模板上有多个根节点,触发告警。 - 挂载
vnode父节点,最后返回vnode
简要概括,vm._render函数最后是通过render执行了createElement方法并返回vnode;下面就来具体看下vm._renderProxy,vm.$createElement,vnode
vm._renderProxy
首先来看下vm._renderProxy,vm._renderProxy是在_init()中挂载的:
Vue.prototype._init = function (options?: Object) {
...
if (process.env.NODE_ENV !== 'production') {
// 对vm对一层拦截处理,当使用vm上没有的属性时将告警
initProxy(vm)
} else {
vm._renderProxy = vm
}
...
}
如果是生产环境,vm._renderProxy直接就是vm;开发环境下,执行initProxy(vm),找到定义:
initProxy = function initProxy (vm) {
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
// 对vm对一层拦截处理
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
先判断当前是否支持Proxy(ES6新语法),支持的话会实例化一个Proxy, 当前例子用的是hasHandler(只要判断是否vm上有无属性即可),这样每次通过vm._renderProxy访问vm时,都必须经过这层代理:
// 判断对象是否有某个属性
const hasHandler = {
has (target, key) {
// vm中是否有key属性
const has = key in target
// 当key是全局变量或者key是私有属性且key没有在$data中,允许访问该key
const isAllowed = allowedGlobals(key) ||
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
// 没有该属性且不允许访问该属性时发起警告
if (!has && !isAllowed) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return has || !isAllowed
}
}
所以,_render中的vnode = render.call(vm._renderProxy, vm.$createElement),实际上是执行vm._renderProxy.render(vm.$createElement)
Virtual DOM 虚拟dom
vue.2.0中引入了virtual dom ,大大提升了代码的性能。所谓virtual dom ,就是用js对象去描述一个dom节点,这比真实创建dom快很多。在vue中,Virtual dom是用类vnode来表示,vnode在src/core/vdom/vnode.js中定义,有真实dom上也有的属性,像tag/text/key/data/children等,还有些是vue的特色属性,在渲染过程也会用到.
vm.$createElement
vue文档中介绍了render函数,第一个参数就是createElement,之前的例子转换成render函数就是:
<div id="app">
{{ message }}
</div>
// 转换成render:
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
}
可以看出,createElement就是vm.$createElement
找到vm.$createElement定义,在initRender方法中,
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
看到这里定义了2个实例方法都是调用的createElement,一个是用于编译生成的render方法,一个是用于手写render方法,createElement最后会返回Vnode,来看下createElement的定义:
export function createElement (
context: Component, //vm实例
tag: any,
data: any, //可以不传
children: any,// 子节点
normalizationType: any,
alwaysNormalize: boolean
) {
// 参数判断,不传data时,要把children,normalizationType参数往前移
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
先经过参数重载,根据alwaysNormalize传不同的normalizationType,调用_createElement(),实际上createElement是提前对参数做了一层处理
这里的参数重载有个小点值得注意,normalizationType是关系到后面children的扁平处理,没有children则不需要对normalizationType赋值,children和normalizationType就都是空值
_createElement()
- 首先校验
data,data是响应式的,调用createEmptyVNode直接返回注释节点:
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true//注释vnode
return node
}
- 处理
tag,没有tag时也返回注释节点 key做基础类型校验- 当
children中有function类型作slot处理,此处先不作分析 - 对
children做normalize变成vnode一维数组,有2种不同的方式:normalizeChildren和simpleNormalizeChildren - 创建
vnode
simpleNormalizeChildren
normalizeChildren和simpleNormalizeChildren是2种对children扁平化处理的方法,先来看下simpleNormalizeChildren定义:
export function simpleNormalizeChildren (children: any) {
for (let i = 0; i < children.length; i++) {
// 把嵌套数组拍平成一维数组
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children)
}
}
return children
}
如果children中有一个是数组则将整个children作为参数组用concat连接,可以得到每个子元素都是vnode的children,这适用于只有一级嵌套数组的情况
normalizeChildren
export function normalizeChildren (children: any): ?Array<VNode> {
// 判断是否基础类型,是:创建文本节点,否:判断是否数组,是:作normalizeArrayChildren处理
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined
}
普通的children处理:最后也是返回一组一维vnode的数组,当children是Array时,执行normalizeArrayChildren
normalizeArrayChildren
代码较长,此处就不贴了,可以自己对照源码来分析:
- 定义res
- 遍历children,当
children[i]是空或者是布尔值,跳过该次循环 - 如果
children[i]还是个数组,再对children[i]作normalizeArrayChildren处理if (Array.isArray(c)) {
if (c.length > 0) {
c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)// 返回vnode数组
// merge adjacent text nodes
// 优化:如果c的第一个vnode和children上一次处理的vnode都是文本节点可以合并成一个vnode
if (isTextNode(c[0]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
c.shift()
}
res.push.apply(res, c)
}
} else if (){...}
- children[i]是基础类型时
} else if (isPrimitive(c)) {
// 当c是基础类型时
// children上一次处理的vnode是文本节点,则合并成一个文本节点
if (isTextNode(last)) {
// merge adjacent text nodes
// this is necessary for SSR hydration because text nodes are
// essentially merged when rendered to HTML strings
// 这是SSR hydration所必需的,因为文本节点渲染成html时基本上都是合并的
res[lastIndex] = createTextVNode(last.text + c)
} else if (c !== '') {
// convert primitive to vnode
res.push(createTextVNode(c))// c不为空直接创建文本节点
}
} else {
- 其它情况,
children[i]是vnode时,} else {// 当c是vnode时
if (isTextNode(c) && isTextNode(last)) {
// merge adjacent text nodes
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
// default key for nested array children (likely generated by v-for)
// 特殊处理,先略过
if (isTrue(children._isVList) &&
isDef(c.tag) &&
isUndef(c.key) &&
isDef(nestedIndex)) {
c.key = `__vlist${nestedIndex}_${i}__`
}
// push到res上
res.push(c)
}
}
- 最后返回一组vnode
主要有2个点,一是normalizeArrayChildren的递归调用,二是文本节点的合并
创建vnode
- 创建
vnode,并返回
- 判断
tag类型,为字符串时:let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 判断tag是否是原生标签
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component组件部分先略过
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
// 未知标签,创建vnode
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}tag不是字符串类型时,vnode = createComponent(tag, data, context, children),先略过- 最后再对生成的
vnode作校验,返回vnode
小结
到此为止,我们分析了vm._render方法和_createElement方法,知道了创建vnode的整个过程,在$mount中的 vm._update(vm._render(), hydrating),vm._render返回了vnode,再传入vm._update中,由vm._update渲染成真实dom。
vue 源码学习三 vue中如何生成虚拟DOM的更多相关文章
- Vue源码学习三 ———— Vue构造函数包装
Vue源码学习二 是对Vue的原型对象的包装,最后从Vue的出生文件导出了 Vue这个构造函数 来到 src/core/index.js 代码是: import Vue from './instanc ...
- Vue源码学习二 ———— Vue原型对象包装
Vue原型对象的包装 在Vue官网直接通过 script 标签导入的 Vue包是 umd模块的形式.在使用前都通过 new Vue({}).记录一下 Vue构造函数的包装. 在 src/core/in ...
- Vue源码学习1——Vue构造函数
Vue源码学习1--Vue构造函数 这是我第一次正式阅读大型框架源码,刚开始的时候完全不知道该如何入手.Vue源码clone下来之后这么多文件夹,Vue的这么多方法和概念都在哪,完全没有头绪.现在也只 ...
- Vue源码学习一 ———— Vue项目目录
Vue 目录结构 可以在 github 上通过这款 Chrome 插件 octotree 查看Vue的文件目录.也可以克隆到本地.. Vue 是如何规划目录的 scripts ------------ ...
- Vue源码学习(一):调试环境搭建
最近开始学习Vue源码,第一步就是要把调试环境搭好,这个过程遇到小坑着实费了点功夫,在这里记下来 一.调试环境搭建过程 1.安装node.js,具体不展开 2.下载vue项目源码,git或svn等均可 ...
- 【Vue源码学习】依赖收集
前面我们学习了vue的响应式原理,我们知道了vue2底层是通过Object.defineProperty来实现数据响应式的,但是单有这个还不够,我们在data中定义的数据可能没有用于模版渲染,修改这些 ...
- 最新 Vue 源码学习笔记
最新 Vue 源码学习笔记 v2.x.x & v3.x.x 框架架构 核心算法 设计模式 编码风格 项目结构 为什么出现 解决了什么问题 有哪些应用场景 v2.x.x & v3.x.x ...
- Vue源码分析(二) : Vue实例挂载
Vue源码分析(二) : Vue实例挂载 author: @TiffanysBear 实例挂载主要是 $mount 方法的实现,在 src/platforms/web/entry-runtime-wi ...
- Vue 源码学习(1)
概述 我在闲暇时间学习了一下 Vue 的源码,有一些心得,现在把它们分享给大家. 这个分享只是 Vue源码系列 的第一篇,主要讲述了如下内容: 寻找入口文件 在打包的过程中 Vue 发生了什么变化 在 ...
随机推荐
- 基于范围的for循环(C++11)
C++11新增了一种循环:基于范围的for循环.这简化了一种常见的循环任务:对数组(或容器类,如vector和array)的每个元素执行相同的操作,如下例所示 for语句允许简单的范围迭代:(只遍历, ...
- 015_ICMP专项研究监控
一.数据demo cat /proc/net/snmp Ip: Forwarding DefaultTTL InReceives InHdrErrors InAddrErrors ForwDatagr ...
- MySQL ERROR 1054(42S22)
修改用户的密码,网上搜到的命令为如下 执行后报错 ERROR 1054(42S22) Unknown column 'password' in ‘field list’ 错误的原因是 5.7版本下的m ...
- 利用python进行数据加载和存储
1.文本文件 (1)pd.read_csv加载分隔符为逗号的数据:pd.read_table从文件.URL.文件型对象中加载带分隔符的数据.默认为制表符.(加载为DataFrame结构) 参数name ...
- echarts 隐藏Y轴最大最小值label及分割线 ----障眼大发好使
需求图 1====>label 最大最小值还好弄, yAxis{ axisLabel{ showMinLabel: false, showMaxLabel: false, }} 2====> ...
- PowerDesigner15连接Oracle数据库并导出Oracle的表结构
PowerDesigner连接Oracle数据库,根据建立的数据源进行E-R图生成.详细步骤如下: 1.启动PowerDesigner 2.菜单:File->Reverse Engineer - ...
- tp5 修改默认的分页url
默认分页url:xx.com/xxx?page=1 个人主要感觉不美观,想变成xx.com/xxx/list_1.html这样的 框架本身默认使用的boostrap分页类,目录位置 simplewin ...
- [转]Example Design - Using the AXI DMA in polled mode to transfer data to memory
Description Attached to this Answer Record is an Example Design for using the AXI DMA in polled mode ...
- tensorflow Tensorboard可视化-【老鱼学tensorflow】
tensorflow自带了可视化的工具:Tensorboard.有了这个可视化工具,可以让我们在调整各项参数时有了可视化的依据. 本次我们先用Tensorboard来可视化Tensorflow的结构. ...
- hbase 性能优化 (转载)
一.服务端调优 1.参数配置 1).hbase.regionserver.handler.count:该设置决定了处理RPC的线程数量,默认值是10,通常可以调大,比如:150,当请求内容很大(上MB ...