原文链接(格式更好):《3-4 Vue2-核心源码讲解》
Vue2 源码仓库:github.com/vuejs/vue
为什么还要看 vue2 的源码:
因为 vue3 结构比较最新的,并且细节很多,不利于了解核心的东西
源码入口
查找顺序:
-
- 可以看到
dev:*的命令都是Rollup打包逻辑,我们重点跟踪dev命令
- 可以看到
-
- 该文件主要是处理、生成
rollup打包的配置项,dev命令对应的配置为full-dev
- 该文件主要是处理、生成
'full-dev': {
// 入口配置
entry: resolve('web/entry-runtime-with-compiler.ts'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
-
- 该文件完整代码如下:
import Vue from './runtime-with-compiler' // 关键代码
import * as vca from 'v3'
import { extend } from 'shared/util'
extend(Vue, vca)
import { effect } from 'v3/reactivity/effect'
Vue.effect = effect
export default Vue
-
- 该文件里面主要定义
$mount,关键代码如下:
- 该文件里面主要定义
import Vue from './runtime/index'
// .....
export default Vue as GlobalAPI
-
- 该文件也是一些
Vue的配置,关键代码如下:
- 该文件也是一些
import Vue from 'core/index'
// ...
export default Vue
-
- 该文件是
Vue.prototype的配置,关键代码如下:
- 该文件是
import Vue from './instance/index'
// ...
export default Vue
-
- 该文件为
new 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'
import type { GlobalAPI } from 'types/global-api'
// 构造函数 Vue,这也是为什么我们在使用时用的为:new Vue(...)
// 因为 Vue 本质就是个构造函数
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options) // ⭐️ 初始化的核心代码,执行 _init 方法,传参为:配置项
}
//@ts-expect-error Vue has function type
initMixin(Vue) // ⭐️ 调用 initMixin 函数,将 _init 方法挂载到 Vue.prototype 上
//@ts-expect-error Vue has function type
stateMixin(Vue)
//@ts-expect-error Vue has function type
eventsMixin(Vue)
//@ts-expect-error Vue has function type
lifecycleMixin(Vue)
//@ts-expect-error Vue has function type
renderMixin(Vue)
export default Vue as unknown as GlobalAPI
-
- 该文件为
new Vue时执行的_init方法的定义(真正的入口):
- 该文件为
// ...
Vue.prototype._init = function (options?: Record<string, any>) {
// ...
}
// ...
初始化
我们在使用 Vue 时,是这样初始化的
new Vue({
el: '#app',
data: {
count: 1
},
methods: {
addCount() {
this.count++
}
}
})
现在我们开始学习源码后,那要关注下new Vue到底做了什么
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options) // 执行 _init 方法
}
_init 代码解析
核心源码:
// ...
// 在 Vue 原型上定义一个 _init 方法,用于初始化实例。
// 这个方法接收一个可选参数 options,类型为记录(Record)类型,键为字符串,值为任意类型
// 通常用来传入组件的选项对象。
Vue.prototype._init = function (options?: Record<string, any>) {
// 定义一个常量 vm,类型为 Component,指向当前正在创建的 Vue 实例。
const vm: Component = this
// 给每个 Vue 实例设置一个唯一的 _uid 标识符,用以区分不同的实例。
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
// 如果处于开发环境 (__DEV__) 并且支持性能标记(config.performance && mark)
// 则进行 Vue 初始化性能检测的相关操作。
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// 设置 _isVue 属性为 true,表明这是一个 Vue 实例。
vm._isVue = true
// 避免 Vue 监听器观察到此实例。
vm.__v_skip = true
// 创建一个新的副作用作用域(EffectScope),用于追踪和执行副作用函数,如计算属性、watcher 等。
// 这里指定为独立作用域,即不受父作用域的影响。
vm._scope = new EffectScope(true /* detached */)
// 设置 Vue 实例的副作用作用域的父级为 undefined
vm._scope.parent = undefined
// 以及标识其与 Vue 实例关联。
vm._scope._vm = true
if (options && options._isComponent) {
// 如果是内部组件,则调用 initInternalComponent 进行优化
initInternalComponent(vm, options as any)
} else {
// ⭐️ 否则通过 mergeOptions 合并构造函数的默认选项和用户传入的选项
// 并将结果赋值给 $options。
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
/* istanbul ignore else */
if (__DEV__) {
// 如果在开发环境下,调用 initProxy 函数来设置代理访问实例数据的逻辑
initProxy(vm)
} else {
// 非开发环境下,直接设置 _renderProxy 指向自身。
vm._renderProxy = vm
}
// 将实例自身暴露给实例自身的 _self 属性。
vm._self = vm
// 调用内部方法初始化 生命周期
initLifecycle(vm)
// 调用内部方法初始化 事件系统
initEvents(vm)
// 调用内部方法初始化 渲染相关
initRender(vm)
// ⭐️ 调用钩子函数 beforeCreate
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
// 在解析[数据/props]之前解析依赖项(injections)。
initInjections(vm)
// ⭐️ 初始化实例的状态:包括 data、props、methods、computed、watch
initState(vm)
// 在解析[数据/props]之后解析提供项(provide)。
initProvide(vm)
// ⭐️ 调用钩子函数 created
callHook(vm, 'created')
/* istanbul ignore if */
if (__DEV__ && config.performance && mark) {
// 开发环境下完成性能标记,记录 Vue 初始化过程的时间消耗,并给实例设置名称。
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
// ⭐️ 如果实例的选项中包含 el(元素挂载点),则调用 $mount 方法挂载 Vue 实例到指定 DOM 元素。
vm.$mount(vm.$options.el)
}
}
// ...
mergeOptions 方法
export function mergeOptions( // 合并[构造函数上的 options]与[实例的 options]
parent: Record<string, any>, // Vue实例构造函数上的 options
child: Record<string, any>, // new Vue(...) 传入的 options
vm?: Component | null // 实例本身
): ComponentOptions {
// parent = {
// components:{},
// directives: {},
// filters: {},
// _base: Vue
// }
// child = { el: '#app', data: { count: 1 } }
if (__DEV__) {
checkComponents(child) // 检测组件名称是否合法
}
if (isFunction(child)) {
// @ts-expect-error
child = child.options // 如果 child 是函数,则取 child.options 作为 child
}
// 把 props 属性转为对象形式(标准结构)
// props: ["count", "a-name"] => props: { count: { type: null }, aName: { type: null } }
// props: { count: "number" } => props: { count: { type: "number" } }
normalizeProps(child, vm)
// 把 inject 属性转为对象形式(标准结构)
normalizeInject(child, vm)
// 把 directives 属性转为对象形式(标准结构)
normalizeDirectives(child)
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
if (child.extends) {
// 当存在 child.extends 属性时,则调用 mergeOptions 实现合并
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
// 当存在 child.mixins 属性时,则循环调用 mergeOptions 实现合并
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options: ComponentOptions = {} as any
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField(key: any) {
// const defaultStrat = function (parentVal: any, childVal: any): any {
// return childVal === undefined ? parentVal : childVal
// }
// defaultStrat 逻辑为:优先取 child 的值,没有再取 parent 的值
// const strats = config.optionMergeStrategies
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
_init 的核心逻辑
- 传入的配置项与默认配置项合并
- 初始化该实例的生命周期、事件系统、渲染逻辑
- 调用该实例的
beforeCreated钩子 - 初始化该实例的状态:data、props 等
- 调用该实例的
created钩子 el存在则执行该实例的挂载$mount逻辑
数据观测
vue 是数据驱动的,数据改变则视图也跟着变化。核心方式是Object.defineProperty()实现数据的劫持
vue 的数据观测核心机制是观察者模式
数据是被观察的一方,当数据发生变化时通知所有观察者,这样观察者就能做出响应(比如:重新渲染视图)
我们将观察者称为watcher,关系为data -> watcher
一个数据可以有多个观察者,通过中间对象dep来记录这种依赖关系data -> dep -> watcher
dep的数据结构为{ id: uuid, subs: []},其中的subs用于存放所有观察者
源码分析:
代码为 init.ts 里面的initState方法里面的initData方法
核心代码的展示与组合:
// initData 方法:
// vm:当前实例
function initData(vm: Component) {
// 获取实例上的 data 值
let data: any = vm.$options.data
// 当 data 为函数时,则执行它并获取它的返回值,否则直接用 data
data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
if (!isPlainObject(data)) {
// 当 data 不是对象时,报警告
data = {}
__DEV__ &&
warn(
'data functions should return an object:\n' +
'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data) // 获取 data 对象的 key 数组
const props = vm.$options.props // 获取实例的 props 值
const methods = vm.$options.methods // 获取实例的 methods 值
let i = keys.length // data 的 key 数组长度
while (i--) { // 递减循环,即从数组的后往前循环
const key = keys[i] // 获取具体的 key
if (__DEV__) {
if (methods && hasOwn(methods, key)) {
// data 的 key 与 methods 的 key重复时,报警告
warn(`Method "${key}" has already been defined as a data property.`, vm)
}
}
if (props && hasOwn(props, key)) {
// data 的 key 与 props 的 key重复时,报警告
__DEV__ &&
warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
/**
// * Check if a string starts with $ or _
// */
// export function isReserved(str: string): boolean {
// const c = (str + '').charCodeAt(0)
// return c === 0x24 || c === 0x5f
// }
// ⭐️ 调用 proxy 函数,传参为:
// vm-实例
// '_data'-固定属性键,等同于 data = vm._data
// key-当前 data 的属性键
// 将 this._data.xx 代理为 this.xx
proxy(vm, `_data`, key)
}
}
// ⭐️ 进行 data 数据的观察逻辑,响应式的核心代码
const ob = observe(data)
ob && ob.vmCount++
}
// target:vm 实例
// sourceKey:固定值 _data
// key:当前 data 的属性键
export function proxy(target: Object, sourceKey: string, key: string) {
function noop(a?: any, b?: any, c?: any) {}
const sharedPropertyDefinition = {
enumerable: true, // 是否可枚举
configurable: true, // 是否可更改与删除
get: noop,
set: noop
}
// 定义具体的 get 函数
sharedPropertyDefinition.get = function proxyGetter() {
// this 等价于 target
return this[sourceKey][key] // 等价于 target._data[key]
}
// 定义具体的 set 函数
sharedPropertyDefinition.set = function proxySetter(val) {
// this 等价于 target
this[sourceKey][key] = val // 等价于 target._data[key] = val
}
// 属性劫持
Object.defineProperty(target, key, sharedPropertyDefinition)
}
// ⭐️ observe 函数:响应式的核心代码
// value:为实例的 data
// shallow:undefined
// ssrMockReactivity:undefined
// 返回值为:new Observe(...) 后的实例
export function observe(
value: any,
shallow?: boolean,
ssrMockReactivity?: boolean
): Observer | void {
// 当已经被观察后,则直接返回
if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
return value.__ob__
}
// 一系列的判断,可以忽略,直接看里面执行的逻辑
if (
shouldObserve &&
(ssrMockReactivity || !isServerRendering()) &&
(isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value.__v_skip /* ReactiveFlags.SKIP */ &&
!isRef(value) &&
!(value instanceof VNode)
) {
// ⭐️ new 调用 Observer 构造函数,参数为:
// value:为实例的 data
// shallow:undefined
// ssrMockReactivity:undefined
return new Observer(value, shallow, ssrMockReactivity)
}
}
export class Observer {
dep: Dep
vmCount: number // number of vms that have this object as root $data
// value:为实例的 data
// shallow:false
// mock:false
constructor(public value: any, public shallow = false, public mock = false) {
// dep = { id: uuid, subs: [] }
this.dep = mock ? mockDep : new Dep()
this.vmCount = 0
/**
* Define a property.
*/
function def(obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
// 在实例的 data 里面新增 __ob__ 属性,其值为 Observer 实例
// 暂存一份数据
def(value, '__ob__', this)
if (isArray(value)) {
// 当实例的 data 为数组时
if (!mock) {
if (hasProto) {
/* eslint-disable no-proto */
;(value as any).__proto__ = arrayMethods
/* eslint-enable no-proto */
} else {
for (let i = 0, l = arrayKeys.length; i < l; i++) {
const key = arrayKeys[i]
def(value, key, arrayMethods[key])
}
}
}
if (!shallow) {
this.observeArray(value)
}
} else {
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// ⭐️ 调用 defineReactive,传参为:
// value:实例的 data
// key:实例的 data 的每个 key
// NO_INITIAL_VALUE = {}
// undefined
// shallow:false
// mock:false
defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
}
}
}
/**
* Observe a list of Array items.
*/
observeArray(value: any[]) {
for (let i = 0, l = value.length; i < l; i++) {
observe(value[i], false, this.mock)
}
}
}
/**
* Define a reactive property on an Object.
*/
// ⭐️ 调用 defineReactive,接受到的参数为:
// obj:实例的 data
// key:实例的 data 的每个 key
// val:{}
// customSetter: undefined
// shallow:false
// mock:false
// observeEvenIfShallow:undefined
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean,
observeEvenIfShallow = false
) {
// dep = { id: uuid, subs: [] }
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if (
(!getter || setter) &&
(val === NO_INITIAL_VALUE || arguments.length === 2)
) {
val = obj[key] // 取值,等价于 data[key]
}
// 将取得的值,再次递归调用 observe
let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)
// ⭐️ 使用 Object.defineProperty 实现属性拦截
// obj:实例的 data
// key:实例的 data 的每个 key
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
// ⭐️ dep.depend() 实现依赖的采集
if (Dep.target) {
if (__DEV__) {
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key
})
} else {
dep.depend()
}
if (childOb) {
childOb.dep.depend() // 孩子的依赖采集
if (isArray(value)) {
dependArray(value)
}
}
}
return isRef(value) && !shallow ? value.value : value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (!hasChanged(value, newVal)) {
return
}
if (__DEV__ && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else if (getter) {
// #7981: for accessor properties without setter
return
} else if (!shallow && isRef(value) && !isRef(newVal)) {
value.value = newVal
return
} else {
val = newVal
}
childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock)
// ⭐️ dep.notify() 实现观察者的通知
if (__DEV__) {
dep.notify({
type: TriggerOpTypes.SET,
target: obj,
key,
newValue: newVal,
oldValue: value
})
} else {
dep.notify()
}
}
})
return dep
}
export default class Dep {
static target?: DepTarget | null
id: number
subs: Array<DepTarget | null>
// pending subs cleanup
_pending = false
constructor() {
this.id = uid++
this.subs = []
}
addSub(sub: DepTarget) {
this.subs.push(sub)
}
removeSub(sub: DepTarget) {
// #12696 deps with massive amount of subscribers are extremely slow to
// clean up in Chromium
// to workaround this, we unset the sub for now, and clear them on
// next scheduler flush.
this.subs[this.subs.indexOf(sub)] = null
if (!this._pending) {
this._pending = true
pendingCleanupDeps.push(this)
}
}
depend(info?: DebuggerEventExtraInfo) {
if (Dep.target) {
Dep.target.addDep(this) // ⭐️ 新增依赖的观察者
if (__DEV__ && info && Dep.target.onTrack) {
Dep.target.onTrack({
effect: Dep.target,
...info
})
}
}
}
notify(info?: DebuggerEventExtraInfo) {
// stabilize the subscriber list first
const subs = this.subs.filter(s => s) as DepTarget[]
if (__DEV__ && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
const sub = subs[i]
if (__DEV__ && info) {
sub.onTrigger &&
sub.onTrigger({
effect: subs[i],
...info
})
}
sub.update() // ⭐️ 通知观察者有更新
}
}
}
官方文档:v2.cn.vuejs.org/v2/guide/re…
watch 的一些属性
export default {
data() {
return {
user: {
name: 'xxx',
age: 30
}
}
},
created() {
// 当 immediate 为 false 时,无法触发对应的 watch
// 当 immediate 为 true 时,可以触发对应的 watch
this.changeUserName()
},
methods: {
changeUserName() {
// 当 deep 为 false 时,无法触发对应的 watch
// 当 deep 为 true 时,可以触发对应的 watch
this.user.name = 'yyy'
}
},
watch: {
user: {
handler(newVal, oldVal) {
console.log(`watched ${oldVal} -> ${newVal}`)
},
deep: true,
immediate: true
}
}
}
通过 watch 的源码,可以看到deep、immediate的具体实现
immediate核心源代码:
Vue.prototype.$watch = function (
expOrFn: string | (() => any),
cb: any,
options?: Record<string, any>
): Function {
const vm: Component = 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) { // ⭐️ immediate 为 true 时
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
// ⭐️ 执行对应的回调函数并且捕获错误
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn() {
watcher.teardown()
}
}
deep核心源代码:
function _traverse(val: any, seen: SimpleSet) {
let i, keys
const isA = isArray(val)
if (
(!isA && !isObject(val)) ||
val.__v_skip /* ReactiveFlags.SKIP */ ||
Object.isFrozen(val) ||
val instanceof VNode
) {
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 if (isRef(val)) {
_traverse(val.value, seen)
} else {
// ⭐️ 针对对象,获取所有的 keys,然后循环递归处理依赖的监听
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
Diff 算法
虚拟 DOM:用来描述真实 DOM 的一个 JS 对象
diff 算法:对比新、旧虚拟 DOM 差异的算法
patch(打补丁):把新旧节点的差异应用到真实 DOM 上的操作
当数据更新后,会重新生成 VDOM,通过 DIFF 算法找到新旧 VDOM 的差异,最后通过 patch 进行页面更新
虚拟 DOM 结构(伪代码):
// html 代码
<div id="app">
<h1>Hello, Virtual DOM!</h1>
<p class="paragraph">This is a paragraph.</p>
</div>
// 对应的虚拟 DOM 结构(简化的伪代码)
const vnode = {
tag: 'div', // 节点的标签名
props: { id: 'app' }, // 节点的属性
children: [ // 子节点数组
{
tag: 'h1',
children: ['Hello, Virtual DOM!']
},
{
tag: 'p',
props: { className: 'paragraph' },
children: ['This is a paragraph.']
}
]
};
虚拟 DOM 源码:github.com/vuejs/vue/b…
Diff 算法核心
总结:递归、同级、双端
先比较新旧 VDOM 的根节点,然后一层层往下进行同级比较,同级比较时会采用首尾双端来加快对比
diff 流程的源码解析:github.com/vuejs/vue/b…
// ...
export function createPatchFunction(backend) {
// ...
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
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (__DEV__) {
checkDuplicateKeys(newCh)
}
// ⭐️ 核心 diff 算法:双指针对比
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 函数,进行它俩的子节点对比
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)) {
// 旧-首 节点 与 新-尾 节点 相同时:表明该节点位置往左移了
// 调用 patchVnode 函数,进行它俩的子节点对比
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)) {
// Vnode moved left
// 旧-尾 节点 与 新-首 节点 相同时:表明该节点位置往右移了
// 调用 patchVnode 函数,进行它俩的子节点对比
patchVnode(
oldEndVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
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)) {
// 当 新-首 节点的 key 不在 旧节点的 keys 内,则表明要走新增逻辑
// New element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
)
} else {
// 当 新-首 节点的 key 在 旧节点的 keys 内,则表明是位置移动了
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// 如果相同 key 对应的新旧节点[相同]时
// 调用 patchVnode 函数,进行它俩的子节点对比
patchVnode(
vnodeToMove,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
// 旧节点该 key 的值充值为 undefined
oldCh[idxInOld] = undefined
canMove &&
nodeOps.insertBefore(
parentElm,
vnodeToMove.elm,
oldStartVnode.elm
)
} else {
// 如果相同 key 对应的新旧节点[不相同]时,则表明要走新增逻辑
// same key but different element. treat as new element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
)
}
}
// 新-首 节点则往右移一个
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(oldCh, oldStartIdx, oldEndIdx)
}
}
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly?: any
) {
// ....
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
// ....
}
function isUndef(v: any): v is undefined | null {
return v === undefined || v === null
}
function isDef<T>(v: T): v is NonNullable<T> {
return v !== undefined && v !== null
}
return function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) { // 新的 Vnode 没有时
// 旧的 Vnode 有时,则需要进行销毁操作
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue: any[] = []
if (isUndef(oldVnode)) {
// 如果新的 Vnode 存在,旧的 Vnode 不存在,则需要进行创建操作
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else { // 新旧 Vnode 都存在时则需要执行的核心逻辑 ⭐️
// isRealElement:是否为真正存在的元素
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// ⭐️ 不是真正存在的元素 && 新旧 Vnode 相同时
// ⭐️ patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 是真正存在的元素 || 新旧 Vnode 不相同时
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 (__DEV__) {
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.'
)
}
}
// 既不是服务端渲染,又不是混合渲染
// 则基于 oldVnode 创建一个空的 Vnode,并替换它
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
// clone insert hooks to avoid being mutated during iteration.
// e.g. for customed directives under transition group.
const cloned = insert.fns.slice(1)
for (let i = 0; i < cloned.length; i++) {
cloned[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
}
补充知识
Rollup
打包工具,将小的、松散耦合的模块(主要为 js 代码)组合成更大型的应用程序或库,支持 ES6 模块,适用于前端库和组件
与之相应的webpack则适用于应用程序,单应用、多应用等等,支持各种模块加载(css/js/图片等)、按需引入等等功能
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
export default {
input: 'src/index.js', // 指定入口文件
output: {
file: 'dist/my-lib.js', // 输出的 bundle 文件路径
format: 'umd', // 输出格式:UMD,可以在浏览器全局作用域下运行,也支持 CommonJS 和 AMD 模块系统
name: 'MyLib', // 全局变量名称,用于在非模块化环境中引用该库
},
plugins: [
resolve(), // 解析第三方模块的绝对路径
commonjs(), // 将 CommonJS 模块转换为 ES6 模块供 Rollup 处理
],
};
Vue2 源码调试
- 下载源码
- 安装依赖:
pnpm i - 在需要的地方加上打印
- 运行:
pnpm test:unit将跑所有的单测,这时候控制台就不停的打印,等运行完毕后,再看结果
- 如果觉得打印的实在太多了,则可以新增一个命令:
-
"test:unit:newvue": "vitest run test/unit/modules/vdom/create-component.spec.ts"- 然后命令行运行:
pnpm test:unit:newvue - 这也会执行
new Vue(...)的操作,并且打印的数据更少
Vue2 中直接通过下标修改数组元素不会触发视图原因是?
Vue2 使用 Object.defineProperty 来实现对对象属性的响应式追踪
Object.defineProperty 只能应用于对象属性,而无法直接应用于数组的索引(因为数组索引不是标准意义上的对象属性)
Vue2 对于数组的响应式处理是通过重写数组的几个可变方法(如 push()、pop() 、shift()等)来间接实现的
以下为数组部分方法重写的源代码:
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { TriggerOpTypes } from '../../v3'
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Define a property.
*/
export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
/**
* Intercept mutating methods and emit events
*/
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
if (__DEV__) {
ob.dep.notify({
type: TriggerOpTypes.ARRAY_MUTATION,
target: this,
key: method
})
} else {
ob.dep.notify()
}
return result
})
})