We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
今年给自己立了个 flag,每周要写一篇高质量的技术文章。没想到刚坚持到第 3 周就面临断档(这周公司需求爆发),还好之前有一些存货。这篇文章是 17 年写的,基于 [email protected],在思否和掘金的反馈都不错,技术细节可能有些过时,没有研究过相关技术的同学可以拿来开拓下思路。
DOM是文档对象模型(Document Object Model)的简写,在浏览器中我们可以通过js来操作DOM,但是这样的操作性能很差,于是Virtual Dom应运而生。我的理解,Virtual Dom就是在js中模拟DOM对象树来优化DOM操作的一种技术或思路。
DOM
Document Object Model
Virtual Dom
一个VNode的实例对象包含了以下属性
tag
data
types/vnode.d.ts
VNodeData
children
text
elm
ns
context
functionalContext
key
componentOptions
child
parent
raw
isStatic
isRootInsert
<transition>
false
isComment
isCloned
isOnce
v-once
VNode可以理解为vue框架的虚拟dom的基类,通过new实例化的VNode大致可以分为几类
VNode
new
EmptyVNode
TextVNode
ElementVNode
ComponentVNode
CloneVNode
true
...
const SIMPLE_NORMALIZE = 1 const ALWAYS_NORMALIZE = 2 function createElement (context, tag, data, children, normalizationType, alwaysNormalize) { // 兼容不传data的情况 if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } // 如果alwaysNormalize是true // 那么normalizationType应该设置为常量ALWAYS_NORMALIZE的值 if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE // 调用_createElement创建虚拟节点 return _createElement(context, tag, data, children, normalizationType) } function _createElement (context, tag, data, children, normalizationType) { /** * 如果存在data.__ob__,说明data是被Observer观察的数据 * 不能用作虚拟节点的data * 需要抛出警告,并返回一个空节点 * * 被监控的data不能被用作vnode渲染的数据的原因是: * data在vnode渲染过程中可能会被改变,这样会触发监控,导致不符合预期的操作 */ if (data && data.__ob__) { process.env.NODE_ENV !== 'production' && warn( `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` + 'Always create fresh vnode data objects in each render!', context ) return createEmptyVNode() } // 当组件的is属性被设置为一个falsy的值 // Vue将不会知道要把这个组件渲染成什么 // 所以渲染一个空节点 if (!tag) { return createEmptyVNode() } // 作用域插槽 if (Array.isArray(children) && typeof children[0] === 'function') { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } // 根据normalizationType的值,选择不同的处理方法 if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } let vnode, ns // 如果标签名是字符串类型 if (typeof tag === 'string') { let Ctor // 获取标签名的命名空间 ns = config.getTagNamespace(tag) // 判断是否为保留标签 if (config.isReservedTag(tag)) { // 如果是保留标签,就创建一个这样的vnode vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) // 如果不是保留标签,那么我们将尝试从vm的components上查找是否有这个标签的定义 } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) { // 如果找到了这个标签的定义,就以此创建虚拟组件节点 vnode = createComponent(Ctor, data, context, children, tag) } else { // 兜底方案,正常创建一个vnode vnode = new VNode( tag, data, children, undefined, undefined, context ) } // 当tag不是字符串的时候,我们认为tag是组件的构造类 // 所以直接创建 } else { vnode = createComponent(tag, data, context, children) } // 如果有vnode if (vnode) { // 如果有namespace,就应用下namespace,然后返回vnode if (ns) applyNS(vnode, ns) return vnode // 否则,返回一个空节点 } else { return createEmptyVNode() } }
简单的梳理了一个流程图,可以参考下
patch函数的定义在src/core/vdom/patch.js中,我们先来看下这个函数的逻辑
patch
src/core/vdom/patch.js
patch函数接收6个参数:
oldVnode
vnode
hydrating
removeOnly
<transition-group>
parentElm
refElm
patch的策略是:
invokeDestroyHook(oldVnode)
createElm
patchVnode
hydrate
oldVnode.elm
这里面值得一提的是patchVnode函数,因为真正的patch算法是由它来实现的(patchVnode中更新子节点的算法其实是在updateChildren函数中实现的,为了便于理解,我统一放到patchVnode中来解释)。
updateChildren
patchVnode算法是:
oldVnode.child
firstChild
lastChild
oldStartVnode
oldEndVnode
newStartVnode
newEndVnode
oldStartVnode.elm
oldEndVnode.elm
oldChildren
newStartVnode.elm
vnode.elm
vnode.text != oldVnode.text
patch提供了5个生命周期钩子,分别是
create
activate
update
remove
destroy
这些钩子是提供给Vue内部的directives/ref/attrs/style等模块使用的,方便这些模块在patch的不同阶段进行相应的操作,这里模块定义在src/core/vdom/modules和src/platforms/web/runtime/modules2个目录中
directives
ref
attrs
style
src/core/vdom/modules
src/platforms/web/runtime/modules
vnode也提供了生命周期钩子,分别是
init
prepatch
insert
postpatch
vue组件的生命周期底层其实就依赖于vnode的生命周期,在src/core/vdom/create-component.js中我们可以看到,vue为自己的组件vnode已经写好了默认的init/prepatch/insert/destroy,而vue组件的mounted/activated就是在insert中触发的,deactivated就是在destroy中触发的
src/core/vdom/create-component.js
mounted
activated
deactivated
最近有刚毕业不久的同行问过我技术成长相关的问题,我的观点是环境很重要。
17 年写这篇文章的时候,我在百度负责搜索前端的组件化改造,从纯Smarty模板改造成Smarty + Vue,需要对Vue进行深度定制,所以花了不少力气研究Vue的技术细节,这个阶段个人技术成长飞快,产出也很丰富,除了对框架原理的深入了解,还自研了基于php的vue ssr方案,用node开发了周边工具链。
Smarty
Smarty + Vue
Vue
php
vue ssr
node
在富有挑战性的环境下,人会被推着成长,技术学完就能用,比自己干学然后放在一边强太多。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
DOM
是文档对象模型(Document Object Model
)的简写,在浏览器中我们可以通过js来操作DOM
,但是这样的操作性能很差,于是Virtual Dom
应运而生。我的理解,Virtual Dom
就是在js中模拟DOM
对象树来优化DOM
操作的一种技术或思路。VNode对象
一个VNode的实例对象包含了以下属性
tag
: 当前节点的标签名data
: 当前节点的数据对象,具体包含哪些字段可以参考vue源码types/vnode.d.ts
中对VNodeData
的定义children
: 数组类型,包含了当前节点的子节点text
: 当前节点的文本,一般文本节点或注释节点会有该属性elm
: 当前虚拟节点对应的真实的dom节点ns
: 节点的namespacecontext
: 编译作用域functionalContext
: 函数化组件的作用域key
: 节点的key属性,用于作为节点的标识,有利于patch的优化componentOptions
: 创建组件实例时会用到的选项信息child
: 当前节点对应的组件实例parent
: 组件的占位节点raw
: raw htmlisStatic
: 静态节点的标识isRootInsert
: 是否作为根节点插入,被<transition>
包裹的节点,该属性的值为false
isComment
: 当前节点是否是注释节点isCloned
: 当前节点是否为克隆节点isOnce
: 当前节点是否有v-once
指令VNode分类
VNode
可以理解为vue框架的虚拟dom的基类,通过new
实例化的VNode
大致可以分为几类EmptyVNode
: 没有内容的注释节点TextVNode
: 文本节点ElementVNode
: 普通元素节点ComponentVNode
: 组件节点CloneVNode
: 克隆节点,可以是以上任意类型的节点,唯一的区别在于isCloned
属性为true
...
createElement解析
简单的梳理了一个流程图,可以参考下
patch原理
patch
函数的定义在src/core/vdom/patch.js
中,我们先来看下这个函数的逻辑patch
函数接收6个参数:oldVnode
: 旧的虚拟节点或旧的真实dom节点vnode
: 新的虚拟节点hydrating
: 是否要跟真是dom混合removeOnly
: 特殊flag,用于<transition-group>
组件parentElm
: 父节点refElm
: 新节点将插入到refElm
之前patch
的策略是:vnode
不存在但是oldVnode
存在,说明意图是要销毁老节点,那么就调用invokeDestroyHook(oldVnode)
来进行销毁oldVnode
不存在但是vnode
存在,说明意图是要创建新节点,那么就调用createElm
来创建新节点vnode
和oldVnode
都存在时oldVnode
和vnode
是同一个节点,就调用patchVnode
来进行patch
vnode
和oldVnode
不是同一个节点时,如果oldVnode
是真实dom节点或hydrating
设置为true
,需要用hydrate
函数将虚拟dom和真是dom进行映射,然后将oldVnode
设置为对应的虚拟dom,找到oldVnode.elm
的父节点,根据vnode创建一个真实dom节点并插入到该父节点中oldVnode.elm
的位置这里面值得一提的是
patchVnode
函数,因为真正的patch算法是由它来实现的(patchVnode中更新子节点的算法其实是在updateChildren
函数中实现的,为了便于理解,我统一放到patchVnode
中来解释)。patchVnode
算法是:oldVnode
跟vnode
完全一致,那么不需要做任何事情oldVnode
跟vnode
都是静态节点,且具有相同的key
,当vnode
是克隆节点或是v-once
指令控制的节点时,只需要把oldVnode.elm
和oldVnode.child
都复制到vnode
上,也不用再有其他操作vnode
不是文本节点或注释节点oldVnode
和vnode
都有子节点,且2方的子节点不完全一致,就执行更新子节点的操作(这一部分其实是在updateChildren
函数中实现),算法如下oldVnode
和vnode
的firstChild
、lastChild
,赋值给oldStartVnode
、oldEndVnode
、newStartVnode
、newEndVnode
oldStartVnode
和newStartVnode
是同一节点,调用patchVnode
进行patch
,然后将oldStartVnode
和newStartVnode
都设置为下一个子节点,重复上述流程oldEndVnode
和newEndVnode
是同一节点,调用patchVnode
进行patch
,然后将oldEndVnode
和newEndVnode
都设置为上一个子节点,重复上述流程oldStartVnode
和newEndVnode
是同一节点,调用patchVnode
进行patch
,如果removeOnly
是false
,那么可以把oldStartVnode.elm
移动到oldEndVnode.elm
之后,然后把oldStartVnode
设置为下一个节点,newEndVnode
设置为上一个节点,重复上述流程newStartVnode
和oldEndVnode
是同一节点,调用patchVnode
进行patch
,如果removeOnly
是false
,那么可以把oldEndVnode.elm
移动到oldStartVnode.elm
之前,然后把newStartVnode
设置为下一个节点,oldEndVnode
设置为上一个节点,重复上述流程oldChildren
中寻找跟newStartVnode
具有相同key
的节点,如果找不到相同key
的节点,说明newStartVnode
是一个新节点,就创建一个,然后把newStartVnode
设置为下一个节点newStartVnode
相同key
的节点,那么通过其他属性的比较来判断这2个节点是否是同一个节点,如果是,就调用patchVnode
进行patch
,如果removeOnly
是false
,就把newStartVnode.elm
插入到oldStartVnode.elm
之前,把newStartVnode
设置为下一个节点,重复上述流程oldChildren
中没有寻找到newStartVnode
的同一节点,那就创建一个新节点,把newStartVnode
设置为下一个节点,重复上述流程oldStartVnode
跟oldEndVnode
重合了,并且newStartVnode
跟newEndVnode
也重合了,这个循环就结束了oldVnode
有子节点,那就把这些节点都删除vnode
有子节点,那就创建这些子节点oldVnode
和vnode
都没有子节点,但是oldVnode
是文本节点或注释节点,就把vnode.elm
的文本设置为空字符串vnode
是文本节点或注释节点,但是vnode.text != oldVnode.text
时,只需要更新vnode.elm
的文本内容就可以生命周期
patch
提供了5个生命周期钩子,分别是create
: 创建patch时activate
: 激活组件时update
: 更新节点时remove
: 移除节点时destroy
: 销毁节点时这些钩子是提供给Vue内部的
directives
/ref
/attrs
/style
等模块使用的,方便这些模块在patch的不同阶段进行相应的操作,这里模块定义在src/core/vdom/modules
和src/platforms/web/runtime/modules
2个目录中vnode
也提供了生命周期钩子,分别是init
: vdom初始化时create
: vdom创建时prepatch
: patch之前insert
: vdom插入后update
: vdom更新前postpatch
: patch之后remove
: vdom移除时destroy
: vdom销毁时vue组件的生命周期底层其实就依赖于vnode的生命周期,在
src/core/vdom/create-component.js
中我们可以看到,vue为自己的组件vnode已经写好了默认的init
/prepatch
/insert
/destroy
,而vue组件的mounted
/activated
就是在insert
中触发的,deactivated
就是在destroy
中触发的题外话
最近有刚毕业不久的同行问过我技术成长相关的问题,我的观点是环境很重要。
17 年写这篇文章的时候,我在百度负责搜索前端的组件化改造,从纯
Smarty
模板改造成Smarty + Vue
,需要对Vue
进行深度定制,所以花了不少力气研究Vue
的技术细节,这个阶段个人技术成长飞快,产出也很丰富,除了对框架原理的深入了解,还自研了基于php
的vue ssr
方案,用node
开发了周边工具链。在富有挑战性的环境下,人会被推着成长,技术学完就能用,比自己干学然后放在一边强太多。
The text was updated successfully, but these errors were encountered: