相关文章:
参考:
如有错误,欢迎指出~
更多学习笔记请戳:https://github.com/6fa/WebKno...
本文目录:
- new Vue()
5.1 _init - Vue实例挂载
6.1 $mount
6.2 mountComponent - 渲染
7.1 创建Vnode
7.2 将Vnode渲染为真实DOM
7.3 总结
5. new Vue()
_init
前面一节知道定义vue对象的地方是:src/core/instance/index
- new Vue时会执行 _init 方法
_init方法是由 initMixin方法添加到 Vue的原型上的,init方法位置在:src/core/instance/init.js。主要做的事:
- 初始化生命周期、事件侦听、渲染方法
- 调用beforeCreate钩子函数
- 初始化注入内容
- 初始化state:即props/data/watch/methods
- 初始化依赖内容
- 调用created钩子函数
- 调用$mount进行挂载
// src/core/instance/index
// vue的定义
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
// src/core/instance/init.js
// vue原型上的_init方法
import config from '../config'
import { initProxy } from './proxy'
import { initState } from './state'
import { initRender } from './render'
import { initEvents } from './events'
import { mark, measure } from '../util/perf'
import { initLifecycle, callHook } from './lifecycle'
import { initProvide, initInjections } from './inject'
import { extend, mergeOptions, formatComponentName } from '../util/index'
let uid = 0
export function initMixin (Vue: Class) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
//......
// 初始化生命周期、事件侦听、渲染方法
// 调用beforeCreate钩子函数
// 初始化注入内容
// 初始化state:即props/data/watch/methods
// 初始化依赖内容
// 调用created钩子函数
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
//......
// 调用$mount进行挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
6.Vue实例挂载
$mount
new Vue最后会调用$mount方法挂载,根据平台(浏览器或原生客户端)、模式(runtime only或runtime+compiler)的不同,$mount方法在多个文件中有不同的定义:
- src/platform/web/entry-runtime-with-compiler.js
- src/platform/web/runtime/index.js
- src/platform/weex/runtime/index.js
runtime+compiler模式会调用src/platforms/web/entry-runtime-with-compiler.js里的$mount:
- 将template或el的innerHTML 转换为 render函数(通过compileToFunctions编译)
- 调用原先Vue原型上的$mount(即src/platform/web/runtime/index.js下的$mount,这个$mount是共享的,方便复用)
- 而共享$mount实际会调用mountComponent。
// src/platforms/web/entry-runtime-with-compiler.js
// runtime+compiler模式下的$mount
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
//...
const options = this.$options
// 将template 或者 el的outerHTML 转换为render函数
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') { //template为字符串
if (template.charAt(0) === '#') {
template = idToTemplate(template) //idToTemplate:通过id来取得元素的innerHTML
//...
}
} else if (template.nodeType) { //template为节点
template = template.innerHTML
} else { //无效template
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) { //如果不存在template但有el
template = getOuterHTML(el)
}
if (template) {
//...
// compileToFunctions 将template转为render函数
const { render, staticRenderFns } = compileToFunctions(template, {
//...传递的选项
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
//...
}
}
return mount.call(this, el, hydrating)
}
// src/platform/web/runtime/index.js
// 共享的$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
mountComponent
mountComponent方法定义在src/core/instance/lifecycle.js:
- 调用beforeMount钩子函数
- 定义更新函数updateComponent
实例化Watcher来监听当前组件状态,传入更新函数updateComponent,且马上调用来完成初次渲染。当状态有变化,依然调用更新函数。而更新函数通过调用vm._update更新视图:
- _update 方法接收 vm._render方法结果为参数
- _render 方法的作用主要是生成 vnode
- 即 _update 负责将 _render 生成的 vnode渲染为真实DOM
- 同时值得注意的是,_render方法里会调用render方法,如果是runtime+compiler模式(写了template属性),则前面的compileToFunctions会将template转为render方法
- 挂载实例,调用mounted钩子函数
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// 注意这里将el赋值给了vm.$el
vm.$el = el
//......(如果没有获取解析的render函数,则会抛出警告)
// 调用beforeMount钩子函数
callHook(vm, 'beforeMount')
// 定义更新函数
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
//......
vm._update(vnode, hydrating)
//......
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 实例化Watcher:监听当前组件状态,当有数据变化时,更新组件
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
// 更新前调用 beforeupdate钩子函数
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// 实例已挂载,调用mounted钩子函数
// vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
7. 渲染
7.1 创建vnode
7.1.1 _render
vue最后都是通过render函数来生成虚拟DOM,写了template也会被转换成render函数。
vue实例挂载时,mountComponent会调用私有_render方法,而私有_render方法最主要的是调用了render方法。私有_render定义在src/core/instance/render.js,主要做了两件事
- 调用render函数,传入$createElement,拿到vnode节点
- 设置vnode节点的父节点
// src/core/instance/render.js
// 在new Vue时,会引入这个文件的 renderMixin 并调用,_render因此挂载到Vue原型上
export function renderMixin (Vue: Class) {
//......
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
vm.$vnode = _parentVnode
let vnode
try {
// 不需要维护一个堆栈
// 因为所有的渲染函数都是单独调用的。当父组件被打补丁时,子组件的渲染函数会被调用
//调用render函数,传入$createElement,即平时我们写reander时的createElement
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
//...处理错误
} finally {
currentRenderingInstance = null
}
//如果节点数组只包含一个节点,返回这个节点
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// 当渲染函数出错时,返回空的vnode
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// 设置vnode的父节点
vnode.parent = _parentVnode
return vnode
}
}
7.1.2 $createElement
当Vue初始化时,先执行了各种mixin(作用是在vue原型上拓展各种方法,比如上面的_render),然后调用vm._init,vm._init会从src/core/instance/render.js引入initRender并调用(initRender和上面的renderMixin在同一个文件),initRender里定义了vm.$createElement。
从代码中可以看到:
- vm.$createElement 和 vm._c 都返回 createElement方法(定义于src/core/vdom/create-element)
- vm.$createElement 用于用户手写render函数的情况;vm._c 用于template转换为render函数的情况
// src/core/instance/render.js
import { createElement } from '../vdom/create-element'
//......
export function initRender (vm: Component) {
//......
// 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)
//......
}
而createElement的作用是创建返回虚拟DOM节点(VNode,是对DOM节点的抽象描述,用js对象来实现,定义在 src/core/vdom/vnode.js )。
从代码中可知createElement是对_createElement的封装,_createElement位于同一个文件中。createElement传入5个参数:
- context:表示VNode的上下文环境
- tag:表示标签,可以是字符串也可以是component
- data:表示VNode的数据
- children:表示VNode的子节点
- normalizationType:表示子节点规范的类型,即 render 函数是编译生成的还是用户手写的
//src/core/vdom/create-element
// createElement
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array {
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)
}
而_createElement的两个主要流程是:
- 对子节点进行规范化(调用normalizeChildren或simpleNormalizeChildren), 因为传入的children 是任意类型的,因此我们需要把它们规范成类型为VNode 的数组
根据不同类型的tag创建不同类型的VNode:
tag为string
- 如果是内置节点,则直接通过new VNode()创建普通VNode
- 如果是已经注册的组件名,则通过createComponent创建组件类型VNode
- 否则创建一个未知标签的VNode
- tag为Component,直接通过createComponent创建组件类型VNode
//src/core/vdom/create-element
// _createElement
export function _createElement (
context: Component,
tag?: string | Class | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array {
//......
// 对子子节点进行规范化
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 = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(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 {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
//......
}
7.1.3 new Vnode()
VNode定义于src/core/vdom/vnode.js,new VNode时主要设置vnode对象的标签名、子对象、父节点、文本、对应的真实节点、option选项等属性:
// src/core/vdom/vnode.js
// VNode
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
functionalContext: Component | void; // only for functional component root nodes
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions
) {
/*当前节点的标签名*/
this.tag = tag
/*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.data = data
/*当前节点的子节点,是一个数组*/
this.children = children
/*当前节点的文本*/
this.text = text
/*当前虚拟节点对应的真实dom节点*/
this.elm = elm
/*当前节点的名字空间*/
this.ns = undefined
/*编译作用域*/
this.context = context
/*函数化组件作用域*/
this.functionalContext = undefined
/*节点的key属性,被当作节点的标志,用以优化*/
this.key = data && data.key
/*组件的option选项*/
this.componentOptions = componentOptions
/*当前节点对应的组件的实例*/
this.componentInstance = undefined
/*当前节点的父节点*/
this.parent = undefined
/*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.raw = false
/*静态节点标志*/
this.isStatic = false
/*是否作为跟节点插入*/
this.isRootInsert = true
/*是否为注释节点*/
this.isComment = false
/*是否为克隆节点*/
this.isCloned = false
/*是否有v-once指令*/
this.isOnce = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next https://github.com/answershuto/learnVue*/
get child (): Component | void {
return this.componentInstance
}
}
7.1.4 createComponent
通过createComponent创建组件类型的VNode,定义于src/core/vdom/create-component.js:
构建子类构造函数
- 如果组件是对象,则调用Vue.extend()将组件对象转换为继承Vue的构造器Sub(即Vue的子类)
- Vue.extned()会Sub对象本身扩展了一些属性,如扩展 options、添加全局 API 等;并且对配置中的 props 和 computed 做了初始化工作;最后对于这个 Sub 构造函数做了缓存,避免多次执行 Vue.extend 的时候对同一个子组件重复构造
- 实例化Sub时,依然会调用Vue的_init方法
- 安装组件钩子函数
- 实例化VNode:new VNode()
//src/core/vdom/create-component.js
// createComponent
export function createComponent (
Ctor: Class | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array,
tag?: string
): VNode | Array | void {
if (isUndef(Ctor)) {
return
}
// 构建子类构造函数
// 这里的baseCtor实际上就是Vue
const baseCtor = context.$options._base
// 组件为对象
if (isObject(Ctor)) {
// Vue.extend 的作用就是构造一个 Vue 的子类
// 把一个纯对象转换一个继承于 Vue 的构造器 并返回
Ctor = baseCtor.extend(Ctor)
}
// 如果它不是一个构造函数或异步组件函数
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// 异步组件
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// 安装组件钩子函数,把钩子函数合并到data.hook中
installComponentHooks(data)
//实例化一个VNode返回。组件的VNode是没有children的
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}
return vnode
}
7.2 将Vnode渲染为真实DOM
7.2.1 _update
调用_update的时机有两个,一是初次渲染,二是数据更新的时候。
前面知道vm._render创建了vnode,而vm._update将vnode渲染为真实的DOM。_update方法定义于:src/core/instance/lifecycle.js:
_update方法核心是调用vm.__patch__方法,会区分是初次渲染还是数据更新,传入不同的参数
- vm.__patch__核心是通过比对虚拟 DOM ,局部更新 DOM
注意区分代码中的vm._vnode 和 vm.$vnode
- vm.$vnode 指组件的虚拟父节点,也即占位符节点,比如例子下面代码中,child为vm.$vnode,vm.$vnode不存在则表示为vue根实例
- vm._vnode表示执行_render后返回的VNode节点,即子组件里面的div
// vm.$vnode 与 vm._vnode例子
//父组件
//子组件
text
// src/core/instance/lifecycle
// _update的定义
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
if (!prevVnode) {
// 初次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 数据更新时
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
7.2.2 patch
_update的核心是调用__patch__,而__patch__在不同平台定义是不一样的,web平台的定义在src/platforms/web/runtime/index.js,代码可知其指向patch:
Vue.prototype.__patch__ = inBrowser ? patch : noop
而patch定义在src/platforms/web/runtime/patch.js中:
- 通过createPatchFunction创建patch函数
- 传入nodeOps和modules为参数,nodeOps的作用是封装一系列DOM操作的方法,modules定义了模块的相应钩子函数的回调(用于patch.js中的createPatchFunction方法:添加hooks执行时对应的回调。具体可看patchVnode这一小节)
export const patch: Function = createPatchFunction({ nodeOps, modules })
createPatchFunction位于src/core/vdom/patch.js:
createPatchFunction在内部定义了一系列辅助方法,然后返回patch方法,patch方法的参数:
- oldVnode:旧的Vnode节点
- vnode:由_render创建返回的Vnode节点
- hydrating:是否服务端渲染
- removeOnly:与transition-group相关的参数
path方法通过判断新旧vnode的不同,来移除、创建、更新dom节点:
- 如果vnode不存在,oldVnode存在,表示移除,调用invokeDestroyHook销毁旧节点
如果vnode存在:
- oldNode不存在,表示新增,调用createElm创建新节点
oldNode存在:
- 如果oldNode不是真实元素,且oldNode与vnode是同一节点,表示需要修改。通过patchVnode方法来比对更新
- 如果oldVnode是真实元素,表示是初次渲染,oldVnode即传入的el,则将oldVnode转换为Vnode对象,然后调用createElm创建新节点
- 如果oldVnode不是真实元素,且oldNode与Vnode不是同一节点,直接调用createElm创建新节点
// src/core/vdom/patch.js
// createPatchFunction、patch
//...
export const emptyNode = new VNode('', {}, [])
//......
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
// vnode不存在,oldVnode存在,表示移除,销毁旧节点(旧DOM元素)
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// vnode存在,oldNode不存在,表示新增,创建新的DOM元素
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// oldNode与vnode都存在
// 再判断oldNode是否真实元素
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// oldNode不是真实元素,且oldNode与vnode是同一节点,表示需要修改
// 同一节点:key相同;元素标签相同;
// patchVnode作用:对children进行diff以决定该如何更新
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// oldNode与vnode不是同一节点,创建新的DOM元素
// oldNode是真实元素(一般是初始渲染,oldVnode的真实dom即传入的vm.$el)
if (isRealElement) {
//...
// oldVnode是真实元素表示是初次渲染时传入的el
// 则将oldVnode转换为Vnode对象
oldVnode = emptyNodeAt(oldVnode)
}
// 获取oldVnode的真实元素、父元素
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// createElm创建真实的元素,并插入到它的父节点DOM中
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
//oldNode与vnode不是同一节点且vnode.parent 存在,表示替换(更新),即异步组件
// 递归地更新父节点元素
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
const insert = ancestor.data.hook.insert
if (insert.merged) {
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// 销毁父元素(占位符节点)
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
7.2.3 createElm
createElm方法的作用是创建真实DOM并插入到它的父节点中,主要逻辑:
- 如果是一个组价,通过 createComponent 创建组件型vnode,最后createComponent会执行挂载( $mount )操作
如果不是,则通过createChildren递归创建子节点,最后添加到页面:
- createChildren 的逻辑很简单,实际上是遍历子虚拟节点,递归调用 createElm
// src/core/vdom/patch.js
// createElm
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
//......
// 如果是一个组价,那么 createComponent 会返回 true。不用往下执行
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// 如果不是组件,则通过createChildren递归创建节点(createChildren里会调用createElm)
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
//......
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
if (__WEEX__) {
// ...
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 添加到页面
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
7.2.4 patchVnode
patchVnode方法的作用是比较并更新元素的差异(注意进入patchVnode部分的vnode均不是真实元素)
- 如果新vnode是文本节点,则直接更新dom的文本内容
如果新vnode非文本节点,分为几种情况:
- 新旧都有子节点且不完全一致,则调用updateChildren对比更新子节点
- 新vnode有子节点而旧无,则不用比较,直接通过addVnodes来创建出新dom,插入父节点中
- 旧vnode有子节点而新无,则直接通过removeVnodes来删除旧dom节点
// src/core/vdom/patch.js
// createPatchFunction、patchVnode
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
//...
// 添加hooks执行时对应的回调
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
//......
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
// 让vnode.el引用到oldVnode的真实dom,当el修改时,vnode.el会同步变化
const elm = vnode.elm = oldVnode.elm
//......
const oldCh = oldVnode.children
const ch = vnode.children
// cbs.update 用于更新attributes
// cbs定义于上面的createPatchFunction,为每个cbs[hooks]添加相应的回调
// cbs.update 的回调有下面这些,可以看到都是更新属性相关
// updateAttributes
// updateClass
// updateDOMListeners
// updateDOMProps
// updateStyle
// update
// updateDirectives
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 新vnode非文本节点
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 新旧vnode都有子节点且不一致,则调用updateChildren对比更新子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 如果新vnode有子节点、旧vnode无子节点,则创建新节点
// 如果旧vnode为文本节点,去掉文本
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// elm已经引用了老的dom节点,在老的dom节点上添加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 如果新vnode无子节点、旧vnode有子节点,则删除旧子节点
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 如果老节点是文本节点
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 新旧vnode都是文本节点且不相等,则直接更新DOM的text
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
7.2.5 updateChildren【diff算法的核心】
patchVnode中调用updateChildren来对比更新新旧vnode的子节点,对比的核心算法即diff算法。diff算法整体思路是从两边到中间开始比较,深度优先、同级比较。
对比从新旧vnode的头尾开始,核心思路:
- 情况一:子节点相同且位置一样时,调用patchNode继续迭代比较这个子节点
- 情况二:子节点相同但位置不一样,调用patchNode继续迭代比较这个子节点,且相应移动oldVnode中的子节点
- 情况三:循环结束,如果这个子节点不存在与oldVnode中,不可以复用节点,则调用createElm新建节点
- 情况四:循环结束,如果这个子节点不存在与newVnode中,则移除该节点
// src/core/vdom/patch.js
// updateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0] // oldVnode的第一个child
let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0] // newVnode的第一个child
let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
// 如果oldEndIdx > oldStartIdx重合,或者newEndIdx > newStarIdx,证明diff完了,循环结束
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
// 如果oldVnode的第一个child不存在,oldStart索引右移
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
// 如果oldVnode的最后一个child不存在,oldEnd索引左移
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 新旧第一个子节点相同,调用patchVnode迭代比较这个子节点
// 头部索引继续右移
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 新旧最后一个子节点相同,调用patchVnode迭代比较这个子节点
// 尾部索引继续左移
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // 节点右移
// oldStartVnode和newEndVnode是同一个节点, 调用patchVnode迭代比较这个子节点
// 同时将该子节点插入到oldEndVnode后面
// oldStart索引右移,newEnd索引左移
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // 节点左移
// oldEndVnode和newStartVnode是同一个节点, 调用patchVnode迭代比较这个子节点
// 同时将该子节点插入到oldStartVnode前面
// oldEnd索引左移,newStart索引右移
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 如果都不匹配
// 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果都不存在,说明newStartVnode是一个新的节点,则新建
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 如果存在,定义为vnodeToMove
// 比较vnodeToMove与newStartVnode是否相同
// 相同则调用patchVnode继续迭代,且将vnodeToMove节点插入到oldNode最前面
// (只有key相同)不相同则新建
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
// newStarIdx右移
newStartVnode = newCh[++newStartIdx]
}
}
// 循环结束时,若oldStartIdx > oldEndIdx,表示oldVnode中找不到newVnode的子节点,需要新建
// 若newStarIdx > newEndidx,表示newVnode中找不到oldVnode的子节点,需要删除
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
// addVnodes内部同样调用了createElm新建节点
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
7.3 总结
- 初次渲染会调用更新函数updateComponent来完成视图渲染,数据更新会触发watcher,而watcher也是调用更新函数updateComponent来完成视图更新
- updateComponent核心是调用_update(),且传入_render()当作参数
- _render()的作用是生成vnode(虚拟dom)
_update()的作用是将vnode渲染为真实DOM,核心是调用__patch__,通过判断新旧vnode的不同,来决定移除、创建、更新dom节点:
- 如果是初次渲染,通过createElm创建新节点
- 如果是数据更新,通过patchVnode对比更新新旧vnode的差异,比对算法即Diff算法,如果有非文本差异且不能复用的节点,最后还是通过createElm创建新节点