学习内容:
异步更新
异步更新的核心在于Wather
类,我们打开Wather
所在的文件:
src/core/observer/watcher.js
我们需要格外注意的是,将来有人需要调入我们的更新方法的时候,我们需要做什么事情,在这个文件里有一个updata
方法。
这个方法什么时候会调用?
是不是我们上一讲的响应化机制的时候,set
被执行的时候会执行notify
,notify
会通知所有的watcher
执行updata
方法,所以整个的入口是这个方法,我们实验一下。
我们在examples/todomvc/
下新建一个test.html
文件,文件内容如下
<script src="../../dist/vue.js"></script>
<div id="demo">
<p v-for = "(item,i) of arr" :key='i'>{{item}}</p>
</div>
<script>
new Vue({
el:'#demo',
data:{
arr:['foo','bar']
},
mounted(){
setTimeout(()=> {
this.arr.push('banba')
},1000)
}
})
</script>
用谷歌浏览器打开test.html
文件,并打开调试模式,找到src/core/observer/watcher.js
`的文件并打开,在166行代码打上断点,我们可以非常清晰的看到数组更新后的执行流程。
接下来看看,Wather里的关键性代码:
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this) //{1}
}
}
{1}、我们找到了一个非常关键的代码queueWatcher(this)
,我们在前面调试更改的时候,我们发现我们进行程序更新的时候,它不会正常的改,而是会进入队列机制里头,也就是会走代码queueWatcher
这个方法。
接下来我们进入queueWatcher
所在的文件:
src/core/observer/scheduler.js
代码如下:
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
//{1-2} 判断重复
if (has[id] == null) {
has[id] = true
if (!flushing) {
//{2}入队操作
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// {3} queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
//{4}
nextTick(flushSchedulerQueue)
}
}
}
它的核心作用就是Wather
的入队操作,我们该怎么理解呢?
我们在一次更新周期中,可能会有很多次更新。那么我们可不可以在这么多的更新的情况下一次刷新到页面中,而不是做页面的数次刷新,浏览器的效率就提升了,用户体验就就会好很多。
我们来看一下它的运行流程:
{1-2}、判断重复,意思如果已经在队列里头了,就不需要进来了。
我们知道data
多个属性在一个方法里更新,其实它们只更新一次,因为这些属性关联wather
就一个。
{2}入队操作;
{3}接下来尝试执行队列;
{4}执行队列里面的所有任务;
接下来我们来研究nextTick
参数里的flushSchedulerQueue
函数
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id) //{5}
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) { //{6}
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run() //{7}
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
{5}、对队列进行排序;
{6}、开始尝试执行队列;
{7}、尝试走当前的watcher
;
我们需要关注当前的watcher
的异步是怎么实现的,接下来我们切换到src/core/observer/watcher.js
里边有一代码:
run () {
if (this.active) {
const value = this.get() //{8}
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue) //{9}
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
{8}、通过get
方法可以得到当前关联watcher
的值;
接下来就尝试做更新操作,核心是执行{9}的cb
函数,这个cb
是wather
一开始创建的时候传递进来的;
接下来看一下整个程序异步的实现机制,我们打开src/core/util/next-tick.js
文件,其实整个程序的异步还是依赖与浏览器的异步,看代码:
/* @flow */
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// {9} start
// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
// {9} end
//首先会尝试使用微任务去启动我们的队列
let timerFunc
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) { //{10}
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && ( //{11}
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => { //{}
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { //{12}
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0) //{13}
}
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
{9}、这段注释的意思是,在vue里面,这个异步机制的实现依赖于微任务
、宏任务
,vue首先会尝试微任务
的实现方式去启动我们的队列;
我们来看,微任务
是如何来启动的?
{10}、通过这个if
条件判断,它首选用的是Promise
,Promise
用的是微任务;
{11}、如果浏览器不支持Promise
,就用MutationObserver
,因为MutationObserver
也是一种微任务;
{12}、如果MutationObserver
也不支持,就是尝试用setImmediate
,而setImmediate
是宏任务;
{13}、最后一种选择就用setTimeout
;
所以在Vue的整个周期里面,队列的执行机制其实依赖于这个的,也就是我们的刷新周期是依赖于这个控制的,我们怎么确定一系列的任务是在一个刷新周期呢?就是利用微任务和宏任务;
虚拟DOM
概念
对DOM
抽象,实际是JS对象,也就是可以利用这个对象去描述真实DOM
的所有属性,以及它的结构。
优点
-
轻量、快速
因为它是js,它在执行的时候,它的属性的数量比起真实的DOM
要少得多,而且它的操作不会造成浏览器的刷新,所以相对来说会快得多,它是通过花费cpu 额外的计算以及内存的空间来换取这样的一个结果。 -
跨平台
这个虚拟DOM
可以描述界面的具体信息,但是将来我们将它渲染到见面中的时候,其实我们可以有不同的渲染器,这个渲染器的不同,将来直接决定平台的不同。 -
兼容性
我们在把虚拟DOM
转换成真实DOM
的过程中,我们可以进行兼容性的优化,我们可以把一些兼容性的问题消化在这个层级上。 -
性能改进
基与vue来说,虚拟DOM
又有特殊的意义,在vue早期版本中,是没有虚拟DOM
的,wather
特别多,页面有多少个绑定就有多少个wather
,最后程序大的时候,程序就会受不了,那能不能一个组件一个wather
呢?也可以,当一个组件有多个属性,当任何一个属性发生变化的时候,都需要页面去渲染,去刷新,但我们需要知道到底是那个属性发生变化了,这个时候我们需要去比对,这个时候就需要虚拟DOM
的diff
算法,把新旧DOM
进行一个比对,从而得道一个真实的操作,到底变化发生在什么地方。所以说对于vue2来说说是一个性能上的优化。
虚拟DOM的实现过程
虚拟DOM的入口是那里呢?
src/platforms/web/runtime/index.js
这是最佳入口点,文件一下代码:
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating) //{14}
}
{14}、$mount
只做了一件事,就是调了mountComponent
方法,我们找到此方法对应的文件src/core/instance/lifecycle.js
,找到对应方法的代码:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
//组件更新函数
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => { //{15}
//获得一个全新的虚拟dom,并且做更新
vm._update(vm._render(), hydrating) //{16}
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
{15}、组件更新函数,它在执行的时候调了两个实例更新的方法,分别是_update()
和_render()
,由于_render()
是 _update()
的参数,所以我们先研究_render()
函数,因为这个函数是真正用来渲染虚拟DOM的方法。
{16}、这段代码的意思是,我们得到一个虚拟DOM ,并且呢,我们再做更新。
接下啦我们着重来看一下_render()
方法,它对应的文件为:
src/core/instance/render.js
,我们找到renderMixin
这个方法:
export function renderMixin (Vue: Class<Component>) {
// install runtime convenience helpers
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
//返回虚拟dom
Vue.prototype._render = function (): VNode { //{17}
const vm: Component = this
const { render, _parentVnode } = vm.$options //{18}
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
//{19} 执行render函数湖区vnode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
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()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
}
{17}、这里核心的方法就是实现了渲染函数,它的核心功能就是返回虚拟DOMVNode
;
{18}、首先解构当前选项中,拿到的了render
函数;
{19}、执行render
函数获取vnode
,render
函数里的一个参数vm.$createElement
,它是当前实例的$createElement
方法。
接下来我们来看看$createElement
这个方法到底是干嘛的,打开此方法所在的文件:
src/core/instance/render.js
,在这里面我们找到initRender
方法。
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// 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) //{20}
// normalization is always applied for the public version, used in
// user-written render functions.
//render函数中h就是它
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) //{21}
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
{20}、这行代码里,我们看到了_c
,这个方法专门是给内部使用的;
{21}、这个方法是专门给用户使用的,我们进入createElement
所在方法;
src/core/vdom/create-element.js
,以后我们想要研究组件化相关的代码,都要在vdom
这个文件里找。
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
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)
}
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data: any).__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()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (process.env.NODE_ENV !== 'production' &&
isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
if (!__WEEX__ || !('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
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
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
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 = new VNode( //{22}
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
{22}、首先我们先拿普通的标签举例。
说了这么多我们来总结一下:
入口:mountComponent, src/core/instance/lifecyle.js
做了两件事,一、创建组件更新函数,创建Watcher
实例;
//组件更新函数
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => { //{24}
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
//获得一个全新的虚拟dom,并且做更新
vm._update(vm._render(), hydrating) //{25}
}
}
new Watcher(vm, updateComponent, noop, { //{23}
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}
{23}、我们发现这个Watcher
就是和当前主见挂钩一对一的Watcher
,我们会把updateComponent
做为参数传入Watcher
,将来Watcher
更新的时候就是将来我们调updateComponent
传入的方法;
{24}、updateComponent
方法中,有一个{25} _update
方法,此方法还是在src/core/instance/lifecyle.js
里:
export function lifecycleMixin (Vue: Class<Component>) {
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { //{25}
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
// {26}
if (!prevVnode) { //{27}
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else { //{28}
// updates
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.
}
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
}
{25}、这个方法的核心思想是什么呢?是一个虚拟节点;
{26}、而虚拟节点掉了一个__patch__
方法,这个方法给我们返回一个真实的DOM
元素,这个过程分成两个可能性,一是初始化时的新创建,二是组件更新的时候;
接下来我们来研究__patch__
,打开它所在的文件:
src/platforms/web/runtime/index.js
,从文件目录我们可得知,它已经不是核心代码了,而是和平台相关的代码。
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
这个名字起得这么奇怪,说明它不是让用户调的,而是内部用的,我们进入这个涵数,所在的文件src/platforms/web/runtime/patch.js
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules }) //{27}
{27}、这个方法的核心就是调了一个工厂函数createPatchFunction
,它的作用就是传入一个配置项,真正的生成一个patch
方法;
这时我们又好奇了,createPatchFunction
方法里的配置项{ nodeOps, modules }
是干嘛的呢?
我们先来看看nodeOps
,打开它所在的文件:
src/platforms/web/runtime/node-ops.js
/* @flow */
import { namespaceMap } from 'web/util/index'
export function createElement (tagName: string, vnode: VNode): Element {
const elm = document.createElement(tagName)
if (tagName !== 'select') {
return elm
}
// false or null will remove the attribute but undefined will not
if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
elm.setAttribute('multiple', 'multiple')
}
return elm
}
export function createElementNS (namespace: string, tagName: string): Element {
return document.createElementNS(namespaceMap[namespace], tagName)
}
export function createTextNode (text: string): Text {
return document.createTextNode(text)
}
export function createComment (text: string): Comment {
return document.createComment(text)
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
export function removeChild (node: Node, child: Node) {
node.removeChild(child)
}
export function appendChild (node: Node, child: Node) {
node.appendChild(child)
}
export function parentNode (node: Node): ?Node {
return node.parentNode
}
export function nextSibling (node: Node): ?Node {
return node.nextSibling
}
export function tagName (node: Element): string {
return node.tagName
}
export function setTextContent (node: Node, text: string) {
node.textContent = text
}
export function setStyleScope (node: Element, scopeId: string) {
node.setAttribute(scopeId, '')
}
通篇下来,我们发现此文件全都的DOM操作,也就是节点操作,它的核心就是让path
真实DOM
核心的的操作。
我们再来看看modules
,有是节点操作还不够,还缺少了一样,那就是属性操作,modules
里包含了所有关于节点的属性操作的方法。
接下来我们看看createPatchFunction
的代码:
src/core/vdom/patch.js
而次文件是我们研究虚拟DOM
的核心
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend //{28}
return function patch (oldVnode, vnode, hydrating, removeOnly) { //{29}
if (isUndef(vnode)) { //{30}
if (isDef(oldVnode)) invokeDestroyHook(oldVnode) //{31}
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) { //{32}
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue) //{33}
} else {
// {34}
const isRealElement = isDef(oldVnode.nodeType) //{35}
if (!isRealElement && sameVnode(oldVnode, vnode)) { //{36}
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) //{37}
} else {
if (isRealElement) { //{38}
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode) //{39}
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm) //{40}
// create new node
createElm( //{41}
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
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)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
{28}、createPatchFunction
参数里的backend
,正式我们传进来的modules
,nodeOps
的属性和节点操作模块;
{29}、我们看到700行,path
方法,这正是我们在前面打补丁时的返回方法,这个打补丁的函数的作用是什么呢?两个做用:
- 初始化的时候直接生成DOM元素;
- 如果是在做更新周期时把新就两个
Vnode
做比对,从而知道怎么做DOM操作;
说白了,path
就是把vdom
变成dom
;
我们来看下张图片pic1,来方便理解:
pic1
1、path
的同层比较
由于两颗树的比较,算法的复杂度极高,为了降低复杂度,虚拟DOM
优化成同层比较,而不会跨层比较。
我们来看看同层比较在代码中是如何表现出来的?
其实核心就做3件事:
- 节点的增删改;
我们一pic1为例,新节点有而老节点没有,它就会做一个新增操作,如果老节点有,新节点没有,那它就会做删除。具体的我们来看看代码是如何实现的。
{30}、说明Vnode
没有定义,新的不存在;
{31}、老的存在;
从{30}和{31}结合起来说明是删除操作,调invokeDestroyHook(oldVnode)
删除;
{32}、老的不存在,就说明就有可能是新增了;
{33}、老的不存在,createElm
就开始创建新增了;
{34}、否则就是更新操作了;
{35}、判断老节点的类型,这里有可能是DOM
;
{36}、如果不是真实的节点就开始做比对了,运用diff
算法;
{37}、执行补丁算法,转换成真实的DOM
操作;
{38}、这种情况就是我们初始化的过程,也就是真实DOM
;
{39}、创建一个空节点;
{40}、拿到真实的DOM
,然后删掉;
{41}、拿到老节点后,创建最新的节点;
{42}、用它老爹把老节点删掉,用它最新的节点,追加进去;
pathVnode
什么时候需要path
呢?
是在diff
发生的地方,我们再来查看src/core/vdom/patch.js
文件里的path
方法。
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) { //{43}
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
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)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
{43}、diff发生的地方,怎么发生比较呢?现在既有老节点又有新节点,而且呢当前的节点还是相同的节点,sameVnode
的比较方式很简单,主要有一下几种:
- 我先看一下这两个标签的名字是否一样,比如说,之前的是"p",现在是"div",不管里面内容是否相同,我都认为是发生了变化,直接做了替换;
- 在便签上加一个
key
属性,目的就是让它快速的知道这是一个相同的节点还是不同的节点,如果是相同的节点我们才有必要去比较他们的虚拟DOM
;
接下来我们来看看它的比较方式
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)
}
const elm = vnode.elm = oldVnode.elm
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
if (isTrue(vnode.isStatic) && //{44}
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
//{45}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) { //属性更新 {46}
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)
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) { //新老孩子都有 {47}
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
//{48}
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
//清空文本
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
//放入新增的孩子
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1) //{49}
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text) //清空 {50}
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
patchVnode
patchVnode
方法主要是三种类型的操作:属性更新PROPS、文本更新TEXT、子节点更新REORDER(重排)。
那么他们的具体原则是什么呢?
-
先去判断静态节点 {44};
因为我们的程序有些节点是静态的,是不会变的,不比较,直接跳过; -
新老节点都有
children
{47};
新老节点都有children
,我们只需比较孩子就可以了 -
老节点没有子节点(children),新的存在,清空老节点dom的文本,把新的children加进去 {48};
-
把3反过来,老的不存在,新的存在,删掉所有的孩子{49};
-
都没children,文本替换{50};
{45}、获取新旧两个节点;
{46}、属性更新,那属性是如何更新的呢?
{47}、新老孩子都存在,
总结
_update
的核心作用就是吧虚拟DOM编程真实的DOM;