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 发生了什么变化 在 ...
随机推荐
- Leetcode#442. Find All Duplicates in an nums(数组中重复的数据)
题目描述 给定一个整数数组 a,其中1 ≤ a[i] ≤ n (n为数组长度), 其中有些元素出现两次而其他元素出现一次. 找到所有出现两次的元素. 你可以不用到任何额外空间并在O(n)时间复杂度内解 ...
- excel2json
原文链接 在游戏项目中一般都需要由策划制作大量的游戏内容,其中很大一部分是使用Excel表来制作的.于是程序就需要把Excel文件转换成程序方便读取的格式. 之前项目使用的Excel表导入工具都是通过 ...
- QT学习之如何在QToolBar中添加带图标的QToolButton并设置图标大小
在网上查到了三种方法,找到一种比较好理解的. 使用QIcon类: QToolButton *toolBtn1 = new QToolButton(this); //创建QToolButton tool ...
- 使用Postman测试https接口时的小问题记录
测试本地的WebApi接口时,接口是https,自己写的用httpclient测试是可以的, 用postman一直连接不了.原因正是由于https,不过postman在界面上已经给出了可能的原因和解决 ...
- js 去掉数组对象中的重复对象
export function deteleObject(obj) { var uniques = []; var stringify = {}; for (var i = 0; i < obj ...
- 记一次WordPress 安装的过程
安装WordPress你我他大家都会,记得10年的时候,哥已经玩转WordPress.dedecms.sns,那为何现在要记一次WordPress安装过程呢? 因为现在不会了! 之前安装都是在Wind ...
- js过滤html标签
function deleteHtmlTag(str){ str = str.replace(/<[^>]+>|&[^>]+;/g,"").trim ...
- android studio 虚拟机adb.exe已停止工作的处理
在搭建完android studio 后使用虚拟机或真机调试程序,出现如下错误. 在运行里输入cmd,打开命令行工具,使用netstat -aon|findstr 5037查看adb.exe的50 ...
- 课堂小记---JavaScript(2)
本阶段难点疑点梳理 1.关于switch中default的使用: default同case功能一样,区别在于并不匹配任何信息,只有当case中无任何匹配的时候才会执行default.需要注意的是,这是 ...
- 做IT,必备的安全知识!
以前的认知 以前刚接触IT行业,而我身为运维,我以为我所需要做的安全就是修改服务器密码为复杂的,ssh端口改为非22,还有就是不让人登录服务器就可以保证我维护的东西安全. 现在的认知 工作也好几年了, ...