写在前面
因为拉勾教育大前端高薪训练营的关系开始接触到 Vue 的源码。
回想这三年半的前端生涯中只有最近的几个月代码中才会有让我真的去查看源码的场景。
比如使用 uni-app 开发组件时会想到去看看 ElementUI 中是怎么实现的。
比如使用 EggJS 做文件上传下载时苦于文档写的不够清晰,会想到去看看 egg-multipart 是怎么实现的。
但对于 Vue 的原理认识还一直还停留在 Object.defineProperty 响应式处理 和 document.createDocumentFragment 创建 fragment 元素处理双向数据绑定 上。花了一个星期,逼着自己一步一步调试源码,探求原理,终于是写完了这篇关于 Vue 原理的探索文章。其中存在错误是不可避免的,毕竟我的理解也许会出现差错,如果大家可以从这篇文章中收获一些,我还是觉得挺欣慰的。如果大家可以从中找出我对于 Vue 原理的理解错误,也请指正,感谢。
template
<div id="app">
<div>
<h1>Hello Aeorus</h1>
<h1>{{msg}}</h1>
<input type="text" v-model="keyword" v-focus v-border>
<h1 v-if="isShow">hide</h1>
<child :msg="msg"></child>
<child2 :msg="msg"></child2>
<button @click="handleClick">transform</button>
</div>
</div>
Vue.directive('focus', {
inserted(el) {
el.focus()
}
})
Vue.component('child2', {
props: ['msg'],
template: '<h2>father said: {{msg}}</h2>',
beforeCreate() {
console.log('--------beforeCreate child2')
},
created() {
console.log('--------created child2')
},
beforeMount() {
console.log('--------beforeMount child2')
},
mounted() {
console.log('--------mounted child2')
},
beforeUpdate() {
console.log('--------beforeUpdate child2')
},
updated() {
console.log('--------updated child2')
}
})
const vm = new Vue({
el: '#app',
components: {
'child': {
props: ['msg'],
template: '<h2>father said: {{msg}}</h2>',
beforeCreate() {
console.log('--------beforeCreate child')
},
created() {
console.log('--------created child')
},
beforeMount() {
console.log('--------beforeMount child')
},
mounted() {
console.log('--------mounted child')
},
beforeUpdate() {
console.log('--------beforeUpdate child')
},
updated() {
console.log('--------updated child')
}
}
},
data: {
msg: 'hello Vue',
keyword: '',
isShow: true
},
directives: {
'border': {
inserted(el) {
el.style.border = '1px solid blue'
}
}
},
beforeCreate() {
console.log('---beforeCreate father')
},
created() {
console.log('---created father')
},
beforeMount() {
console.log('---beforeMount father')
},
mounted() {
console.log('---mounted father')
},
beforeUpdate() {
console.log('---beforeUpdate father')
},
updated() {
console.log('---updated father')
},
methods: {
handleClick() {
this.isShow = !this.isShow
setTimeout(() => {
this.isShow = !this.isShow
}, 1000)
}
}
})
vm._data.msg = '123'
core/instance/index.js
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)
}
// 注册 vm 的 _init 方法,初始化 vm
initMixin(Vue)
// 注册 vm 的 $data / $props / $set / $delete / $watch
stateMixin(Vue)
// 初始化事件相关方法 $on / $once / $off / $emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法 _update / $forceUpdate / $destroy
lifecycleMixin(Vue)
// 混入 render $nextTick / _render
renderMixin(Vue)
- 定义构造函数 Vue,并将 new Vue(options) 中的 options 传入 this._init 方法
- 调用 initMixin 为 Vue 添加 _init 方法
- 调用 stateMixin 为 Vue 添加 $data / $props / $set / $delete / $watch 方法
- 调用 eventsMixin 为 Vue 添加 $on / $once / $off / $emit 方法
- 调用 lifecycleMixin 为 Vue 添加 _update / $forceUpdate / $destroy 方法
- 调用 renderMixin 为 Vue 添加 $nextTick / _render 方法
- Vue 构造函数构建完毕,可以进行实例化
- new Vue 即调用 this._init 方法,则进入 initMixin(Vue) 中,目录: core/instance/init.js
core/instance/init.js
Vue.prototype._init = function (options?: Object) {
const vm = this
// a uid
vm._uid = uid++
// a flag to avoid this being observed
vm._isVue = true
// merge options
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
// expose real self
vm._self = vm
// vm 生命周期相关变量初始化
// $children / $parent / $root / $refs
initLifecycle(vm)
// vm 的事件监听初始化,父组件绑定在当前组件的事件上
initEvents(vm)
// vm 的编译 render 初始化
// $slots / $scopedSlots / _c / $createElement / $attrs / $listeners
initRender(vm)
// 调用 beforeCreate 生命周期
callHook(vm, 'beforeCreate')
// 把 inject 的成员注入到 vm 上
initInjections(vm) // resolve injections before data/props
// 初始化 vm 的 _props / methods / _data / computed / watch
initState(vm)
// 初始化 provide
initProvide(vm) // resolve provide after data/props
// 调用 created 生命周期
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
- 先记录 vm 等于 this,因为调用者是 Vue 构造函数内部的 this,所以 vm 等于 Vue 的实例
const vm = this // -> Vue 实例
- 赋值 _isVue 定义该对象是一个 Vue 对象
vm._isVue = true
- 根据 vm 是否是组件初始化 vm.$options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
- 调用 initLifecycle 为 vm 添加 $children / $parent / $root / $refs 属性
- 调用 initEvents 初始化 vm 的事件监听机制
- 调用 initRender 为 vm 添加 $slots / $scopedSlots / _c / $createElement / $attrs / $listeners 属性
- 调用 beforeCreate 生命周期
- 调用 initInjections 为 vm 添加 inject 属性
- 调用 initState 初始化 vm 添加 _props / methods / _data / computed / watch 属性
- 调用 initProvide 为 vm 添加 resolve provide after data / props 属性
- 调用 created 生命周期
- 调用 vm.$mount 进入页面渲染流程
- $mount 方法目录: platforms/web/runtime/index.js
- 但因为平台是 web 端,所以 vm.$mount 在入口文件中被重新定义了,目录: platforms/web/entry-runtime-with-compiler.js
initState core/instance/state.js
- 处理 options 内部的数据
- initProps / initMethods / initData 都是将 options 中的 props / data / methods 挂载到 vm 身上,以便于通过 this 获取到
- initData 在将 options.data 挂载到 vm 上后还对其做了响应式处理,调用 observe 方法
- initComputed 是初始化 vm 的 计算 watcher
- 在 vm 身上挂载 _computedWatchers 属性,是一个记录 watcher 的键值对
- 遍历 options.computed,将每个计算属性定义为一个 计算 watcher,即 new Watcher,并将每个 计算 watcher 保存到 _computedWatchers 中,然后将每个计算属性挂载到 vm 上,以便于通过 this 获取到,其值是 计算 watcher 内保存的 value
- initWatch 是将 options.watch 中的每个监听对象建立一个 侦听 watcher,其内部调用了 $watch 方法
function initState (vm) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
observe core/observer/index.js
- 数据响应式 _data = options.data
- 检测数据是否是 object 类型,如果不是则直接返回
- 检测数据是否有 ob 属性
- 有则直接返回 ob
- 没有则返回 new Observer(_data)
- 检测数据是否有 ob 属性
- 对象、数组都属于 object 类型,所以可以进入响应式处理阶段
- 给当前 _data 添加 ob 属性,如果有这个属性则说明已经被响应式处理过
- 判断 _data 类型
- 如果是数组则将 _data 的 proto 指向 arrayMethods,然后遍历 _data,将每个元素再次递归调用 observe,为元素进行响应式处理
- 如果是对象则调用 walk 方法,遍历 _data 的键值对,将每个属性调用 defineReactive 方法 重置 getter 和 setter
- 在 getter 中收集依赖,在 setter 中触发依赖
- getter -> 如果该属性是对象且有子属性,则也需要为子属性进行依赖收集
- setter -> 为 newValue 进行响应式处理
// index.js
import { arrayMethods } from './array'
class Observer {
constructor (value) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) return
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
function defineReactive (
obj,
key,
val,
customSetter,
shallow // 浅拷贝
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) return
const getter = property && property.get
const setter = property && property.set
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
core/observer/array.js
- 改写会改变原数组的方法
- 原数组改变时为新增的元素进行响应式处理 -> ob.observeArray(inserted)
- 原数组改变时触发依赖 -> ob.dep.depend()
// array.js
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
Dep 类 core/observer/dep.js
- 在依赖收集前会对 target 赋值,即当前 watcher,全局只有一个 target,所以一次只能触发一个 watcher
- subs 中存储的是存储了 dep 对象数组的 watcher 对象
class Dep {
static target;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Watcher
Watcher 类 core/observer/watcher.js
let uid = 0
class Watcher {
vm;
expression;
cb;
id;
deep;
user;
lazy;
sync;
dirty;
active;
deps;
newDeps;
depIds;
newDepIds;
getter;
value;
constructor (
vm,
expOrFn,
cb,
options
) {
this.vm = vm
vm._watchers.push(this)
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy ? undefined : this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
addDep (dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value || isObject(value) || this.deep
) {
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
evaluate () {
this.value = this.get()
this.dirty = false
}
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
teardown () {
if (this.active) {
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
const seenObjects = new Set()
function traverse (val) {
seenObjects.clear()
_traverse(val, seenObjects)
}
function _traverse (val, seen) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || !Object.isExtensible(val)) return
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) return
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
watcher 有三种: 渲染 watcher 计算 watcher 侦听 watcher
渲染 watcher
- 初始化 渲染 watcher 时会进入 get 方法,get 方法中会调用 this.getter,即调用 () => { vm._update(vm._render(), hydrating) }
- get 方法中会先将当前 渲染 watcher 的实例赋值给 Dep.target,vm._render 方法返回的是 虚拟DOM,因此会对 vnode 中访问到的所有属性进行访问,所以在进入它们的 getter 的时候 Dep.target 就变成了 true,依赖收集也便可以开始进行了,因为要在收集完的时候将 Dep.target 改回null,以便于下次依赖收集时不会出错,所以使用 try…catch…finally 方式,在 finally 中重置 Dep.target
- _update 方法中会调用 patch 方法对页面开始渲染 ( 结合最后的 调用 $mount 理解 )
const updateComponent = () => {
vm._update(vm._render(), hydrating)
}
vm._watcher = new Watcher(vm, updateComponent, noop)
/*
new Watcher(
vm,
() => { vm._update(vm._render(), hydrating) },
() => {}
)
this.vm = vm
vm._watchers.push(this)
this.cb = () => {}
this.expression = this.getter = () => { vm._update(vm._render(), hydrating) }
this.value = this.get()
*/
计算 watcher
- 在 initComputed 中生成 计算 watcher 后还调用了 defineComputed 方法
- defineComputed 方法的用途有两个
- 找到 vm._computedWatchers 上当前 计算 watcher 的实例,调用当前实例的 evaluate 方法,然后进行依赖收集
- evaluate 方法会调用 get 方法获取该计算属性的值,也就是调用了这个计算属性的方法,即 () => this.firstName + this.lastName
- get 方法中会先将当前 计算 watcher 的实例赋值给 Dep.target,因为该计算属性的方法又访问了 vm.firstName 和 vm.lastName,所以在进入它们的 getter 的时候 Dep.target 就变成了 true,依赖收集也便可以开始进行了,因为要在收集完的时候将 Dep.target 改回null,以便于下次依赖收集时不会出错,所以使用 try…catch…finally 方式,在 finally 中重置 Dep.target
- 将 computed 中定义的属性绑定到 vm 上
- 找到 vm._computedWatchers 上当前 计算 watcher 的实例,调用当前实例的 evaluate 方法,然后进行依赖收集
/*
// 计算属性的两种写法
computed: {
fullName() {
return this.firstName + this.lastName
},
address: {
get() {
return this.province + this.city
},
set(newVal) {
const location = newVal.split('-')
this.province = location[0]
this.city = location[1]
}
}
}
*/
for (const key in vm.$options.computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
vm._computedWatchers[key] = new Watcher(
vm,
getter || noop, // () => this.firstName + this.lastName
noop,
{
lazy: true
}
)
}
/*
new Watcher(
vm,
() => this.firstName + this.lastName,
() => {},
{
lazy: true
}
)
this.vm = vm
vm._watchers.push(this)
this.cb = () => {}
this.lazy = true
this.dirty = true
this.expression = this.getter = () => this.firstName + this.lastName
this.value = undefined
*/
侦听 watcher
- 初始化 侦听 watcher 时会进入 get 方法,get 方法中会调用 this.getter 得到当前侦听对象的现值,所以会进入侦听对象的 getter
- get 方法中会先将 Dep.target 设置为当前 侦听 watcher 的实例,且当前 侦听 watcher 的实例必然是一个响应式对象,所以在 getter 中的 Dep.target 就变成了 true,依赖收集也便可以开始进行了,因为要在收集完的时候将 Dep.target 改回null,以便于下次依赖收集时不会出错,所以使用 try…catch…finally 方式,在 finally 中重置 Dep.target
/*
// 监听器的两种写法
watch: {
fullName(newVal, oldVal) {
console.log(newVal, oldVal)
},
address: {
deep: true,
immediate: true,
handler(newVal, oldVal) {
console.log(newVal, oldVal)
}
}
}
*/
for (const key in vm.$options.watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
function createWatcher (vm, keyOrFn, handler, options) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(keyOrFn, handler, options)
}
Vue.prototype.$watch = function (expOrFn, cb, options) {
const vm = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
/*
new Watcher(
vm,
'fullName'
(newVal, oldVal) => { console.log(newVal, oldVal) },
{
user: true
}
)
new Watcher(
vm,
'address'
(newVal, oldVal) => { console.log(newVal, oldVal) },
{
user: true,
deep: true,
immediate: true
}
)
this.vm = vm
vm._watchers.push(this)
this.user = true
this.deep = false // address 的 watcher 是 true
this.cb = (newVal, oldVal) => { console.log(newVal, oldVal) }
this.expression = 'fullName'
this.getter = parsePath(expOrFn) // parsePath('fullName')
this.value = this.get() -> vm.fullName
*/
platforms/web/runtime/index.js
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
platforms/web/entry-runtime-with-compiler.js
- 入口文件,Vue 的构造函数也是从这个页面中被引入的
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
// 没有 render 则进行编译 template 属性,说明 template 和 render 同时存在时只会编译 render 里面的内容
if (!options.render) {
let template = options.template
if (template) { // 有 template 属性
if (typeof template === 'string') { // template 内容是选择器或 HTML 字符串
if (template.charAt(0) === '#') { // template: '#app'
template = idToTemplate(template) // 返回是否存在该节点
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) { // getElementById('app')
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) { // 既没有 template 也没有 render 时获取 el 的 html
template = getOuterHTML(el)
}
if (template) {
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
}
- 将 platforms/web/runtime/index.js 中定义的 $mount 方法保存起来,记录为 mount
- 重新定义 $mount 方法
- 查询 el 所对应的模板,如果 el 是 body 或者是 html 元素,则报错
- 检测是否 new Vue(options) 中是否传入了 render 方法
- 有 render 则调用之前记录的 mount 方法
- 没有则说明是通过 template 传入了模板,或者只有一个模板根节点( el )
- 如果只有一个模板根节点 el,则获取根节点的 outerHTML赋值给 template
- 调用 compileToFunctions 方法,传入 template,将模板字符串转换成渲染函数
- 该文件作用就是将 options 统一成拥有 render 方法的对象
- 调用之前记录的 mount 方法
compileToFunctions
- 是一个高阶函数,由 createCompiler 方法返回,目录: platforms/web/compiler/index.js
const { compile, compileToFunctions } = createCompiler(baseOptions)
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
delimiters: options.delimiters,
comments: options.comments
}, this)
createCompiler
- 是一个高阶函数,由 createCompilerCreator 方法返回,目录: compiler/index.js
- 调用 createCompilerCreator 方法时传入将 template 转换成 AST语法树 的回调
- 调用 parse 方法,将 template 转换成 AST语法树
- 调用 optimize 方法,优化 AST语法树,标记静态节点和静态根节点
- 调用 generate 方法,将 AST语法树 拼接成一段 new Function 可执行的 JavaScript 代码字符串
- 返回 AST语法树 / new Function 可执行的 JavaScript 代码字符串
const createCompiler = createCompilerCreator(function baseCompile (
template,
options
): CompiledResult {
const ast = parse(template.trim(), options)
optimize(ast, options)
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
createCompilerCreator
- 是一个高阶函数,返回 compile 和 compileToFunctions 方法,目录: compiler/create-compiler.js
- compiled 就是上面 createCompiler 方法返回的 AST语法树 / new Function 可执行的 JavaScript 代码字符串的对象
- 返回 compile 和 compileToFunctions
- compileToFunctions 是一个高阶函数 createCompileToFunctionFn(compile),内部调用了 compile 方法,compile 方法返回了 compiled,然后将 compiled 解构返回,内容为 render 和 staticRenderFns
- 因此在入口函数处调用了 compileToFunctions 就可以得到这两个值
function createCompilerCreator (baseCompile) {
return function createCompiler (baseOptions) {
function compile (
template,
options
) {
const finalOptions = Object.create(baseOptions)
if (options) {
// merge custom modules
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules)
}
// merge custom directives
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives),
options.directives
)
}
// copy other options
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}
const compiled = baseCompile(template, finalOptions)
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
调用 $mount
- 即上述的 mount 方法
- 其中返回了 mountComponent 方法,该方法定义在 core/instance/lifecycle.js 中
- 调用 beforeMount 生命周期
- 定义 updateComponent 方法创建渲染组件的具体实现
- 调用 _update 方法创建每次数据更新时视图变化的具体实现,其中调用了 patch 方法来对比新旧节点,目录: core/instance/lifecycle.js
- _update 方法需要传入 虚拟DOM,因此其中传入了 vm._render 的调用,其结果是一个 虚拟DOM
- _render 方法会去当前实例身上的 options 中找到 render 方法并调用它,render 方法本质就是返回 h函数 的结果,所以会得到当前模板的 虚拟DOM,即 vnode,目录: core/instance/render.js
- _render 方法在内部调用 h函数 是通过调用 _c 和 $createElement 实现的,这两个方法都指向了 createElement 方法,目录: core/vdom/create-element.js
- 定义 渲染 watcher,将 updateComponent 传入,每当数据更新时就会调用 updateComponent 更新视图
- 渲染函数会在定义成功后立刻执行一次,则 _update 方法中的 patch 便会立刻执行,得到新旧节点后经过 diff 算法挂载到 DOM树 上,最后渲染到页面
- 调用 mounted 生命周期
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode // 空字符串的 虚拟DOM
}
callHook(vm, 'beforeMount')
let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
vm._watcher = new Watcher(vm, updateComponent, noop)
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
}
createElement / _createElement core/vdom/create-element.js
- createElement 方法内部调用 _createElement 方法去创建 虚拟DOM
└─ 判断传入的 data 是否有 is 属性
│ └─ 如果有,则说明这是一个子组件标签,将 tag 从 ‘child’ 转换成子组件对象
│
├─ 判断 tag 是否不存在,防止 is 属性指向了错误的路由地址
│ └─ 如果不存在则创建一个空的 虚拟DOM
│
├─ 判断传入的 data 是否有 children 属性
│ └─ 如果有,则说明不是单纯的 ,内部存在插槽 slot,将 children 赋值给 data.scopedSlots,并将 children 清空
│
├─ 判断 tag 是否是字符串形式
│ └─ 如果是字符串形式,则说明是个元素节点的 AST
│ │ ├─ 如果该 AST 的标签是 HTML 保留标签,则为其创建 虚拟DOM
│ │ └─ 如果该 AST 的 Vue 的实例对象身上存在 components,说明是 方式创建的子组件,然后调用 createComponent 为该子组件创建 虚拟DOM
│ │
│ └─ 如果不是字符串形式,则说明是个 形式的子组件( 上面已将 tag 赋值为子组件对象 ),调用 createComponent 为该子组件创建 虚拟DOM
└─ createComponent 内部会调用 resolveConstructorOptions 方法为组件绑定 init / insert / prepatch / destroy 四个钩子,方便于之后 patch 时做处理
function _createElement (
context,
tag,
data,
children,
normalizationType
) {
if (isDef(data) && isDef((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()
}
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)
) {
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
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (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(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (isDef(vnode)) {
if (ns) applyNS(vnode, ns)
return vnode
} else {
return createEmptyVNode()
}
}
渲染页面流程
patch
- 在 渲染 watcher 中提到了 patch 方法
- 定义目录: platforms/web/runtime/index.js
- patch 是一个高阶函数,其内部返回了真正的 patch 方法,目录: core/vdom/patch.js
- patch 方法有两个入参,modules 和 nodeOps
- modules 是一个返回了钩子模块数组,内部有共有的 create / update / destroy 钩子,还有基于 web 平台的其他钩子
- 共有钩子目录: core/vdom/modules/index
- web 平台钩子目录: web/runtime/modules/index
- nodeOps 是一个返回了各种 DOM 操作方法的对象,目录: platforms/web/runtime/node-ops.js
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
// platforms/web/runtime/patch.js
const modules = platformModules.concat(baseModules)
const patch = createPatchFunction({ nodeOps, modules })
patch
└─ 判断新节点是否存在
│ └─ 如果新节点不存在,则判断旧节点是否存在
│ └─ 如果旧节点存在,则说明组件即将被销毁,调用 invokeDestroyHook 方法销毁旧节点和其子组件,并终止运行下面的程序
│
├─ 定义 isInitialPatch 为 false,表示不是第一次被创建
├─ 定义 insertedVnodeQueue 为空数组,等待子组件插入
├─ 判断旧节点是否存在
│ ├─ 如果旧节点不存在,则将 isInitialPatch 设置为 true,表示是第一次被创建,并且调用 createElm 方法,创建新节点的 真实DOM 并挂载到 DOM 树上进行渲染
│ └─ 如果旧节点存在,则判断旧节点是否是 真实DOM
│ ├─ 如果旧节点不是 真实DOM( 即 虚拟DOM ),则判断新旧节点是否是同一节点
│ │ └─ 如果是同一节点,则调用 patchVnode 方法进行节点对比
│ │
│ └─ 如果旧节点是 真实DOM,则说明新旧节点不可能是同一节点,可以直接将旧节点从 DOM 树移除
│ ├─ 调用 emptyNodeAt 方法为其创建 虚拟DOM -> oldVnode = emptyNodeAt(oldVnode)
│ ├─ 调用 createElm 方法为新节点创建 真实DOM 并插入到旧节点之后
│ ├─ 判断新节点是否有 parent 属性
│ │ └─ 如果新节点有 parent 属性
│ │ ├─ 调用 isPatchable 方法判断新节点是否是一个子组件( 元素返回 true,文本节点返回 false )
│ │ │ ├─ 如果新节点是子组件,则将子组件的 虚拟DOM 赋值给新节点 -> vnode = vnode.componentInstance._vnode,返回子组件的 tag 是否存在的布尔值
│ │ │ └─ 如果新节点不是子组件,则返回新节点的 tag 是否存在的布尔值
│ │ │
│ │ ├─ 定义 patchable 变量接收 isPatchable 方法的返回值
│ │ ├─ while 循环新节点的 parent 属性,销毁 parent
│ │ └─ 将 parent 的 parent 属性赋值给 parent,直到新节点的 parent 全部销毁
│ │
│ └─ 判断旧节点是否有 parentNode -> nodeOps.parentNode(oldElm)
│ ├─ 如果旧节点有 parentNode,则调用 removeVnodes 将旧节点从 真实DOM 树上移除
│ └─ 如果旧节点没有 parentNode,则调用 invokeDestroyHook 将旧节点销毁
│
├─ 调用 invokeInsertHook 将子组件插入 DOM 树进行渲染
└─ 返回挂载了 elm 属性的 虚拟DOM -> vnode.elm
function patch (
oldVnode,
vnode,
hydrating,
removeOnly,
parentElm,
refElm
) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
if (isRealElement) {
oldVnode = emptyNodeAt(oldVnode)
}
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
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(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
createElm
- 调用 createComponent 判断当前节点是否是子组件
- 如果当前节点是子组件,则创建子组件,并且直接跳到下一轮节点
- 判断当前节点的类型并调用不同的创建节点方法创建节点
- 如果当前节点的 tag 存在,则说明该节点是元素节点,调用 nodeOps.createElement 方法创建当前节点的 真实DOM,并挂载到当前虚拟节点的 elm 属性上,然后调用 createChildren 方法创建该节点的 children,将 children 生成的 真实DOM 通过 nodeOps.appendChild 方法追加到该节点的 DOM 树上,最后调用 insert 方法,将该节点追加挂载到父节点的 DOM 树上
- 如果当前节点是注释节点,调用 nodeOps.createComment 方法创建当前节点的 真实DOM,并挂载到当前虚拟节点的 elm 属性上,然后调用 insert 方法,将该节点追加挂载到父节点的 DOM 树上
- 如果当前节点是文本节点,调用 nodeOps.createTextNode 方法创建当前节点的 真实DOM,并挂载到当前虚拟节点的 elm 属性上,然后调用 insert 方法,将该节点追加挂载到父节点的 DOM 树上
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested
) {
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) return
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
inPre++
}
if (
!inPre &&
!vnode.ns &&
!(
config.ignoredElements.length &&
config.ignoredElements.some(ignore => {
return isRegExp(ignore)
? ignore.test(tag)
: ignore === tag
})
) &&
config.isUnknownElement(tag)
) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
inPre--
}
} 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)
}
}
createComponent
- 如果是子组件,则 vnode.data 中会有元素行内属性和组件 hook 属性
- hook 属性中包含 init / insert / prepatch / destroy 四个钩子
- 定义 isReactivated 变量判断子组件是否已经被挂在过,且是一个 keepAlive 组件
- 通过 if (isDef(i = i.hook) && isDef(i = i.init)) 的方式给 i 赋值为 vnode.data.hook.init,如果是组件则可以调用 i,进入组件的初始化,目录: core/vdom/create-component.js
- 通过 i 的调用( 即 hook.init )可以为当前子组件创建 componentInstance 属性
- hook.init 内部调用了 createComponentInstanceForVnode 方法,返回了一个 VueComponent 的实例对象,因为 VueComponent 继承自 Vue 的构造函数,所以当 return new VueComponent() 时会调用 this._init 方法
- 在 createComponentInstanceForVnode 内部会为当前实例的 options 身上挂载 _isComponent / parent / _parentVnode 属性,因此在 Vue 构造函数的 _init 方法中会走入 initInternalComponent 方法的调用,为当前实例的 $options 身上挂载上 parent / _parentVnode 属性
- 在 Vue 构造函数的 _init 方法的调用中还会调用 initLifecycle 方法,这里面有一段对于 $parent 和 $children 嵌套的处理
- hook.init 内部最后调用了该 VueComponent 实例对象的 $mount 方法对子组件进行挂载
由此可知,在 Vue 的渲染机制中,如果存在子组件,会先将父组件创建,然后将子组件创建,接着先挂载子组件,再挂载父组件,因为子组件的 $mount 存在于父组件的 $mount 中
- hook.init 内部调用了 createComponentInstanceForVnode 方法,返回了一个 VueComponent 的实例对象,因为 VueComponent 继承自 Vue 的构造函数,所以当 return new VueComponent() 时会调用 this._init 方法
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */, parentElm, refElm)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
事件绑定
└─ 创建 真实DOM 的过程之后中其实还有事件绑定等对于 真实DOM 的处理,源码中通过调用 invokeCreateHooks 方式实现的
├─ createElm 方法中判断如果当前节点的 tag 存在时调用了 invokeCreateHooks 方法,因为当前节点如果是文本节点或者是注释节点是没办法绑定事件的
├─ createComponent 方法中调用的 initComponent 方法里面也调用了 invokeCreateHooks 方法
├─ invokeCreateHooks 中遍历了 cbs.create 数组,将数组中的钩子依次调用,那么 cbs 是怎么来的?
├─ 之前说过 patch 是一个高阶函数,其内部返回了真正的 patch 方法,patch 方法又是一个调用了 createPatchFunction 方法的返回值,createPatchFunction 方法中传入了两个参数,modules 和 nodeOps,这个 modules 就是上面 patch 章节中提到的返回了钩子模块数组的 modules,里面集成了 create / update / destroy 等钩子
├─ 调用 createPatchFunction 方法时会遍历 modules 初始化 cbs
│ ├─ 已知 modules 是一个二维数组,且内部一定有两个 create / update / destroy 钩子( 共有钩子集成的 ),目录: core/vdom/modules/ref.js 和 core/vdom/modules/directives.js
│ ├─ 此外在 platforms/web/runtime/modules 目录下也集成了其他的钩子
│ │ ├─ attrs.js 中集成了对于 虚拟DOM 中 attrs 的 create / update 钩子( 即标签中的 :src 等 )
│ │ ├─ class.js 中集成了对于 标签class 的 create / update 钩子( 即标签中的 class 和 :class )
│ │ ├─ dom-props.js 中集成了对于 虚拟DOM 中 domProps 的 create / update 钩子( 即直接给子组件标签行内添加属性 )
│ │ ├─ events.js 中集成了对于 虚拟DOM 中 on 的 create / update 钩子
│ │ ├─ style.js 中集成了对于 虚拟DOM 中 staticStyle 和 style 的 create / update 钩子( 即标签中的 :style )
│ │ └─ transition.js 中集成了对于 Vue 官方提供的 过渡 组件的 create / activate / remove 钩子
│ │
│ └─由此可知 modules 中一共有19个钩子,将 modules 中的钩子以相同的属性名划分到同一个数组中,然后分别存入 cbs
│ ├─ create: [updateAttrs, updateClass, updateDOMListeners, updateDOMProps, updateStyle, _enter, create, updateDirectives, ]
│ ├─ activate: [_enter, ]
│ ├─ update: [updateAttrs, updateClass, updateDOMListeners, updateDOMProps, updateStyle, update, updateDirectives, ]
│ ├─ remove: [remove, ]
│ └─ destroy: [destroy, unbindDirectives, ]
│
├─ 回到 invokeCreateHooks 方法中,其内部遍历了 cbs.create 数组,然后将 create 中的8个钩子依次调用
├─ 执行到 updateDOMListeners 钩子时会去判断 虚拟DOM 上面是否存在 on 属性( 即 vnode.data.on )
│ └─ 如果该 虚拟DOM 上面存在 on 属性,则取出该 虚拟DOM 的 真实DOM( vnode.elm ) 作为 target,并调用 updateListeners 方法为该 target 绑定事件监听 ( target.addEventListener )
│ ├─ updateListeners 方法目录: core/vdom/helpers/update-listeners.js
│ └─ target.addEventListener 方法来自于 platforms/web/runtime/modules/events.js 中的 add 方法,但作为参数传入 updateListeners 方法的调用中
│
└─ 执行其他钩子同理
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
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 invokeCreateHooks(vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
return function patch() {/* invokeCreateHooks() */}
}
Diff
- 上文中提到的是页面首次渲染的流程,当数据改变时会触发 渲染 watcher,从而调用 _update 方法进行新旧节点的对比渲染流程
- diff 算法的核心在于新旧节点为同一节点且都有子节点时,如何做到精细化比较,最小量更新 DOM 树
patchVnode
patch 方法中说过,如果新旧节点是同一节点,则调用 patchVnode 方法进行节点对比
└─ 判断新旧节点是否是全等,如果全等说明新旧节点不需要渲染,直接退出本轮比较
├─ 定义 elm,并为新节点的 elm 属性重新赋值为旧节点的 elm 属性( const elm = vnode.elm = oldVnode.elm )
├─ 判断新旧节点是否都是静态节点( 上面 createCompiler 章节中提到调用 optimize 方法标记静态节点和静态根节点 )
│ └─ 如果新旧节点都是静态节点,直接退出本轮比较
│
├─ 判断新节点的 data 中是否存在,并且判断 data.hook.prepatch 钩子是否存在
│ └─ 如果存在,则说明新节点是一个子组件,立刻调用子组件在创建时身上所挂载的 prepatch 钩子
│
└─ 判断新节点中是否存在 text 属性
├─ 如果新节点中不存在 text 属性
│ ├─ 如果新旧节点都存在 children 属性,调用 updateChildren 进行精细化比较( 即 diff 算法 )
│ ├─ 如果只有新节点存在 children 属性,则旧节点为文本节点或空节点,调用 nodeOps.setTextContent 清空旧节点内的文本,然后调用 addVnodes 将新节点插入旧节点之后,最后删除旧节点
│ ├─ 如果只有旧节点存在 children 属性,则新节点是一个空节点,调用 removeVnodes 方法移出旧节点
│ └─ 如果旧节点存在 text 属性,则新节点是一个空节点,调用 nodeOps.setTextContent 方法将旧节点内容清空
│
└─ 如果新节点中存在 text 属性,但是新旧节点的 text 属性的值不同,调用 nodeOps.setTextContent 方法将旧节点内容替换为新节点的 text 属性的值
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
if (oldVnode === vnode) return
const elm = vnode.elm = oldVnode.elm
if (isTrue(vnode.isStatic) &&
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)
}
const oldCh = oldVnode.children
const ch = vnode.children
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)
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
updateChildren
└─ 四项命中策略: 新前旧前 新后旧后 新后旧前 新前旧后
│ ├─ 新前: 新节点的 children 属性序列为 ( newStartIdx = 0 ) 的节点 newStartVnode
│ ├─ 新后: 新节点的 children 属性序列为 ( newEndIdx = newChildren.length - 1 ) 的节点 newEndVnode
│ ├─ 旧前: 旧节点的 children 属性序列为 ( oldStartIdx = 0 ) 的节点 oldStartVnode
│ └─ 旧后: 旧节点的 children 属性序列为 ( oldEndIdx = oldChildren.length - 1 ) 的节点 oldEndVnode
│
├─ 设置循环条件为 (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) 的判断
│ └─ 如果跳出循环,说明新旧节点其中之一的 children 必定遍历完了
│ │
│ ├─ 如果旧前不存在
│ │ ├─ 说明旧前被标记为 undefined
│ │ ├─ 将 oldStartIdx 后移一位
│ │ └─ 更新旧前所对应的 oldStartVnode
│ │
│ ├─ 如果旧后不存在
│ │ ├─ 说明旧后被标记为 undefined
│ │ ├─ 将 oldEndIdx 前移一位
│ │ └─ 更新旧后所对应的 oldEndVnode
│ │
│ ├─ 命中新前旧前
│ │ ├─ 调用 patchVnode 方法对比 新前旧前
│ │ ├─ 将 newStartIdx / oldStartIdx 后移一位
│ │ └─ 更新 新前旧前 所对应的 newStartVnode 和 oldStartVnode
│ │
│ ├─ 命中新后旧后
│ │ ├─ 调用 patchVnode 方法对比 新后旧后
│ │ ├─ 将 newEndIdx / oldEndIdx 前移一位
│ │ └─ 更新 新后旧后 所对应的 newEndVnode 和 oldEndVnode
│ │
│ ├─ 命中新后旧前
│ │ ├─ 调用 patchVnode 方法对比 新后旧前
│ │ ├─ 将命中节点 ( 旧前 ) 插入旧后之后
│ │ ├─ 将 newEndIdx 前移一位,将 oldStartIdx 后移一位
│ │ └─ 更新 新后旧前 所对应的 newEndVnode 和 oldStartVnode
│ │
│ ├─ 命中新前旧后
│ │ ├─ 调用 patchVnode 方法对比 新前旧后
│ │ ├─ 将命中节点 ( 旧后 ) 插入旧前之前
│ │ ├─ 将 newStartIdx 后移一位,将 oldEndIdx 前移一位
│ │ └─ 更新 新前旧后 所对应的 newStartVnode 和 oldEndVnode
│ │
│ └─ 定义一个 map 记录无法匹配四项命中策略的新旧节点
│ ├─ 如果 oldKeyToIdx 不存在,则 map 为空表,调用 createKeyToOldIdx 方法创建新表
│ │ ├─ 记录旧前旧后之间的所有的 child 到 map 表中
│ │ ├─ map 表中的每个键值对以当前 child 的 key 为键,在 oldChildren 中所处的序列为值
│ │ └─ 返回 map 表赋值给 oldKeyToIdx
│ │
│ ├─ 判断新前是否有 key 属性
│ │ ├─ 如果新前有 key 属性,则去 oldKeyToIdx 中找到该 key 所对应的旧节点的 children 中的序列,然后赋值给 idxInOld
│ │ └─ 如果新前没有 key 属性,则调用 findIdxInOld 方法找到新前在旧节点的 children 中所对应的 child 的序列,然后赋值给 idxInOld
│ │
│ └─ 判断 idxInOld 是否存在
│ │ ├─ 如果 idxInOld 不存在,则说明新前是个新增节点
│ │ │ └─ 调用 createElm 方法创建该节点,并将新节点的 真实DOM 挂载到新前的 elm 属性上
│ │ │
│ │ └─ 如果 idxInOld 存在,将旧节点中对应的这个 child 赋值给 vnodeToMove
│ │ └─ 判断 vnodeToMove 和新前是否是同一节点
│ │ └─ 当新前有 key 属性时会去 map 中找同样 key 的元素并赋值给 idxInOld,有可能 map 表中正好有 key 相同但其他属性不同的节点
│ │ │
│ │ ├─ 如果 vnodeToMove 和新前是同一节点
│ │ │ ├─ 调用 patchVnode 方法对比 vnodeToMove 和新前,并将 vnodeToMove 所对应的旧节点的 child 设置为 undefined
│ │ │ └─ 因为 child 是对象,所以在循环体内的会出现 如果旧前不存在 和 如果旧后不存在 的情况
│ │ │
│ │ └─ 如果 vnodeToMove 和新前不是同一节点,则说明新前是个新增节点
│ │ └─ 调用 createElm 方法创建该节点,并将新节点的 真实DOM 挂载到新前的 elm 属性上
│ │
│ ├─ 将 newStartIdx 后移一位
│ └─ 更新 新前 所对应的 newStartVnode
│
└─ 跳出循环体
└─ 判断条件 oldStartIdx > oldEndIdx 是否成立
├─ 如果成立,说明旧节点先全部遍历完
│ └─ 判断新前的后一位 child 是否存在 isUndef(newCh[newEndIdx + 1])
│ ├─ 如果不存在,说明新节点也全部遍历完成
│ └─ 如果存在,说明新节点没有全部遍历完成
│ └─ 定义 refElm 为新前的后一位 child.elm
│ ├─ 如果 elm 存在,说明旧节点中也有该 child,调用 addVnodes 方法将新节点中未处理项插入该 child 之前
│ └─ 如果 elm 不存在,说明旧节点中没有该 child,调用 addVnodes 方法将新节点中未处理项追加到旧节点的最后
│
└─ 如果不成立,说明新节点先全部遍历完
├─ 同时说明旧节点中剩下的 child 在新节点中并不存在
└─ 调用 removeVnodes 将旧节点中剩下的 child 全部删除
function createKeyToOldIdx(children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
function updateChildren(
parentElm,
oldCh,
newCh,
insertedVnodeQueue,
removeOnly
) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
const canMove = !removeOnly
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
} else {
vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
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)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
Vue 的全局方法
core/global-api/index.js
- platforms/web/entry-runtime-with-compiler.js 中 Vue 的构造函数来自于这个文件
- 该文件定义了 Vue 的全局 API
- 调用 initUse 使 Vue 具备了可以通过 Vue.use 来注册插件的功能
- 调用 initMixin 使 Vue 具备了可以通过 Vue.mixin 来实现混入的功能
- 调用 initExtend 使 Vue 具备了可以通过 Vue.extend 来实现生成一个继承 Vue 构造函数的子类的功能( 一般用来创建组件 )
- 调用 initAssetRegisters 使 Vue 具备了可以通过 Vue.directive 来实现指令的功能、通过 Vue.component 来生成组件的功能、通过 Vue.filter 来自定义过滤器的功能
Vue.use core/global-api/use.js
- 简而言之就是调用了 install 方法,因此自定义插件必须有 install 方法( 对象需要有 install 属性,类需要有 install 静态方法 )
- 如果自定义插件本身就是一个方法,则调用它
function initUse(Vue) {
Vue.use = function (plugin) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
const args = toArray(arguments, 1)
args.unshift(this) // 将 Vue 放到 arguments 第一项再调用是为了 -> install(Vue)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}
Vue.mixin core/global-api/mixin.js
- 内部实现比较复杂,简而言之就是将两个对象进行合并( Object.assign(vm, mixin) )
Vue.extend core/global-api/extend.js
- 可以理解为 class Vue.extend extends Vue {}
- 调用者是 Vue 的构造函数,因此 this 指向了 Vue 的构造函数
- 定义 Super = this,指向 Vue 的构造函数
- 定义 Sub,将其原型指向 Super,其 constructor 指向自身,完成了继承
- 将 Sub 赋值为 VueComponent 方法,其 constructor 则指向了 VueComponent 方法
- 内部调用 this._init,因为 Sub 的原型指向了 Vue 的构造函数,所以就是调用了父类 Vue 的 _init 方法
- 合并父类和自己的 options -> Sub.options = mergeOptions(Super.options, extendOptions)
- 调用 initProps 和 initComputed 初始化自身的 props 和 computed 中定义的属性
- 继承父类的全局方法 extend / mixin / use
- 继承父类的属性 component / directive / filter,即 initAssetRegisters 中定义的属性
- 返回 Sub 构造函数
Vue.component / Vue.directive / Vue.filter
- 因为入参相同,因此一起定义
- 目标仅仅只是为 vm 绑定 components / directives / filters 属性,然后将定义的方法以键值对的方式存进去
/*
Vue.component('child', {
props: ['msg'],
template: '<h2>father said: {{msg}}</h2>',
mounted() {
console.log('child mounted')
}
})
Vue.directive('focus', {
inserted(el) {
el.focus()
}
})
Vue.filter('child', options)
*/
const ASSET_TYPES = [
'component',
'directive',
'filter'
]
function initAssetRegisters (Vue) {
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id,
definition
) {
if (!definition) {
return this.options[type + 's'][id]
} else {
if (process.env.NODE_ENV !== 'production') {
if (type === 'component' && config.isReservedTag(id)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + id
)
}
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}