一、响应式,对象以及数组的劫持原理
1. 对象劫持原理
Vue 2 的响应式原理主要依赖于 Object.defineProperty
,它能够监听对象属性的读取和修改操作。Vue 在初始化时,会遍历传入的 data
对象的所有属性。对每个属性使用 Object.defineProperty
重新定义其 getter
和 setter
。 如果对象的属性值仍然是对象,Vue 会递归地对嵌套对象进行劫持,确保整个对象树都是响应式的。
2. 数组劫持原理
由于 Object.defineProperty
无法直接监听数组的变化,Vue 2 对数组进行了特殊处理。 Vue 2 通过重写数组的 7 个方法(push
、pop
、shift
、unshift
、splice
、sort
、reverse
)来实现对数组的劫持。ps 这些方法会改变原数组所以需要重写,slice这个方法不会改变原数组所以不需要重写。Vue 2 在遍历data时判断如果是数组通过改变该值的__proto__实现重写。同时会对数组里的值进行遍历设置响应式
3. 核心实现类:
Observer : 它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新
Dep : 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个 Dep 实例(里面 subs 是 Watcher 实例数组),当数据有变更时,会通过 dep.notify()通知各个 watcher。
Watcher : 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种。vue把凡是需要被作为依赖的函数都交给watcher处理,这样就能在函数执行时,让该函数中用到的变量将其作为依赖收集起来,将来值改变的时候再通知该函数再次执行。
4. 依赖收集
- initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集
- initState 时,对侦听属性初始化时,触发 user watcher 依赖收集
- render()的过程,触发 render watcher 依赖收集
- re-render时,vm.render()再次执行,会移除所有 subs 中的 watcer 的订阅,重新赋值。
5. 派发更新
- 组件中对响应的数据进行了修改,触发 setter 的逻辑
- 调用 dep.notify()
- 遍历所有的 subs(Watcher 实例),调用每一个 watcher 的 update 方法。
6. 为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty
Vue 3.0 采用 Proxy
的主要原因可以归纳为以下几点:
- 性能更好:
Proxy
是直接代理整个对象,不需要递归遍历每个属性,初始化性能更好。Proxy
的拦截操作是惰性的,只有在访问或修改属性时才会触发,减少了不必要的开销。 - 功能更强大:
Proxy
提供了 13 种拦截操作,包括get
、set
、deleteProperty
、has
、ownKeys
等,可以全面监听对象的行为。支持动态属性、数组索引变化、删除属性等操作。
7. computed
惰性求值的原理
computed: {
fullName() {
return this.firstName + ' ' + this.lastName;
}
}
-
1、当你定义一个
computed
属性时,Vue 首先会为其创建一个Watcher
实例。将用户自定义的函数如上fullName()
这个函数作为watcher的getter
传入,并标记lazy
为true,同时watcher里会设置dirty
为true。 然后将computed 属性变成响应式的形式定义到 Vue 实例上。 -
2、computed 属性响应式的get函数就是通过watcher.dirty的值来判断返回value的计算方式,以及收集哪些值把它加入了依赖。
-
3、当用户第一次访问这个computed属性的时候执行watcher的
evaluate
方法(里面执行的就是上例fullName()
),get函数里涉及到的变量(firstName、lastName
)会将这个watcher放在自己的依赖里, -
4、若computed属性所依赖的变量有更新则会执行这个watcher的
update
方法将dirty
变成true。 -
5、下次再获取这个computed属性的值的时候会根据
dirty
字段判断是否需要重新计算获取相应的值。
function initComputed(vm: Component, computed: Object) {
const watchers = (vm._computedWatchers = Object.create(null));
for (const key in computed) {
const userDef = computed[key];
const getter = typeof userDef === 'function' ? userDef : userDef.get;
// 创建 Watcher 实例
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
{ lazy: true } // 标记为 computed Watcher
);
// 将 computed 属性定义到 Vue 实例上
if (!(key in vm)) {
defineComputed(vm, key, userDef);
}
}
}
defineComputed
函数负责将 computed
属性定义到 Vue 实例上,并设置其 getter 和 setter。
function defineComputed(target: any, key: string, userDef: Object | Function) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key);
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? createComputedGetter(key)
: noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
createComputedGetter
函数负责创建 computed
属性的 getter 函数。这个 getter 函数会在访问 computed
属性时被调用。
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate(); // 重新计算值
}
if (Dep.target) {
watcher.depend(); // 收集依赖
}
return watcher.value;
}
};
}
对于 computed
属性,Watcher
实例会被标记为 lazy
,表示它是惰性求值的。
class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm;
this.lazy = !!options.lazy; // 标记为 computed Watcher
this.dirty = this.lazy; // 初始时,computed Watcher 是脏的
this.getter = expOrFn;
this.value = this.lazy ? undefined : this.get();
}
//computed属性的value是调用evaluate()生成的
evaluate() {
this.value = this.get(); // 重新计算值
this.dirty = false; // 标记为干净
}
get() {
pushTarget(this); // 将当前 Watcher 设置为 Dep.target
const value = this.getter.call(this.vm, this.vm); // 执行 getter 函数
popTarget(); // 恢复之前的 Watcher
return value;
}
depend() {
let i = this.deps.length;
while (i--) {
this.deps[i].depend(); // 收集依赖
}
}
update() {
if (this.lazy) { // computed属性只做dirty标记
this.dirty = true; // 标记为脏数据
} else {
// 其他 Watcher 的逻辑
}
}
}
8. watch 原理
watch: {
'a.b.c': {
handler(newVal, oldVal) {
console.log('数据变化:', newVal);
},
deep: true, // 深度监听嵌套对象的变化
immediate: true // 初始化时立即执行一次回调
}
}
- 1、首先明确,watch的属性都已经在data里面定义过了,也就是watch的属性都具备响应式。watch 初始化是会为当前属性调用vm.$watch(expOrFn, cb, options)方法创建watcher。watcher创建后就会访问这个属性的get从而将这个watcer放到这个属性的dep数组里。
- 2、vm.$watch(expOrFn, handler, options)其中'a.b.c'为expOrFn参数,cb对应的handler,options为deep,immediate等参数
- 3、这里的expOrFn是一个表达式,在watcher里通过this.getter = parsePath(expOrFn);来执行表达式
// `$watch` 方法会创建一个 `Watcher` 实例。
// 如果 `immediate` 选项为 `true`,会立即执行回调函数。
// 返回一个 `unwatchFn` 函数,用于取消监听。
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options);
}
options = options || {};
options.user = true; // 标记为用户定义的 watcher
const watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
cb.call(vm, watcher.value); // 立即执行回调函数
}
return function unwatchFn() {
watcher.teardown();
};
};
二、Vue 的渲染过程
1. 综述
第一步: template -> ast -> render
第二步: beforeMount -> 定义updateComponent函数,创建Watcher实例去执行updateComponent函数 -> mounted
vm._render()生成虚拟dom, vm._update里的patch函数进行dom diff算法对比生成真实dom
export function mountComponent(vm, el) {
const options = vm.$options
callHook(vm, 'beforeMount')
// 渲染页面
// 无论渲染还是更新都会调用此方法
let updateComponent = () => {
// console.log('update')
vm._update(vm._render())
}
// 渲染watcher 每个组件都有一个watcher
new Watcher(vm, updateComponent, () => {}, true) // true表示他是一个渲染watcher
callHook(vm, 'mounted')
}
Vue.prototype._update = function (vnode) {
// 通过虚拟节点创建真实dom
const vm = this
const preVnode = vm._vnode
vm._vnode = vnode
if (!preVnode) {
vm.$el = patch(vm.$el, vnode) // 第一次渲染
} else {
vm.$el = patch(preVnode, vnode) // 更新
}
}
2. template -> ast
1.vue中使用大量正则表达式去解析template模版将其转换成ast语法树,当解析到开始标签的时候调用start函数,在这里创建ast对象createASTElement,维护节点层级栈stack。当解析到结束标签的时候调用end函数,通过stack 在此处作父子关系绑定。每匹配解析一点内容就会调用advance函数将指针往前移
// 代码位置:/src/complier/parser/index.js
/**
* Convert HTML string to AST.
* 将HTML模板字符串转化为AST
*/
export function parse(template, options) {
// ...
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
// 当解析到开始标签时,调用该函数
start(tag, attrs, unary) {
let element = createASTElement(tagName, attrs)
if (!root) {
root = element
}
currentParent = element
stack.push(element) // 标签入栈
// 在此处调用用createASTElement生成如下element对象
// {
// type: 1,
// tag,
// attrsList: attrs,
// attrsMap: makeAttrsMap(attrs),
// parent,
// children: []
// }
},
// 当解析到结束标签时,调用该函数
end() {
// 在此处作父子关系绑定
// <div><p><span></span></p></div
// stack ['div', 'p', 'span']
// 解析到</span结束标签时,会从栈中弹出栈顶元素 span,
// 比较是否一致可校验字符串中是否有未正确闭合的标签。并将当前栈顶元素 p 标记为parent
let element = stack.pop() // 弹出栈顶元素
currentParent = stack.length > 0 ? stack[stack.length - 1] : null
if (currentParent) {
element.parent = currentParent
currentParent.children.push(element) //实现了树的父子关系
}
},
// 当解析到文本时,调用该函数
chars(text) {},
// 当解析到注释时,调用该函数
comment(text) {},
})
return root
}
- ast -> render
根据不同的
AST
节点类型创建不同的VNode
类型。
ast = {
'type': 1,
'tag': 'div',
'attrsList': [
{
'name':'id',
'value':'NLRX',
}
],
'attrsMap': {
'id': 'NLRX',
},
'static':false,
'parent': undefined,
'plain': false,
'children': [{
'type': 1,
'tag': 'p',
'plain': false,
'static':false,
'children': [
{
'type': 2,
'expression': '"Hello "+_s(name)',
'text': 'Hello {{name}}',
'static':false,
}
]
}]
}
-
首先,根节点
div
是一个元素型AST
节点,那么我们就要创建一个元素型VNode
,我们把创建元素型VNode
的方法叫做_c(tagName,data,children)
。我们暂且不管_c()
是什么,只需知道调用_c()
就可以创建一个元素型VNode
。那么就可以生成如下代码:_c('div',{attrs:{"id":"NLRX"}},[/*子节点列表*/])
-
根节点
div
有子节点,那么我们进入子节点列表children
里遍历子节点,发现子节点p
也是元素型的,那就继续创建元素型VNode
并将其放入上述代码中根节点的子节点列表中,如下:_c('div',{attrs:{"id":"NLRX"}},[_c('p'),[/*子节点列表*/]])
-
同理,继续遍历
p
节点的子节点,发现是一个文本型节点,那就创建一个文本型VNode
并将其插入到p
节点的子节点列表中,同理,创建文本型VNode
我们调用_v()
方法,如下:_c('div',{attrs:{"id":"NLRX"}},[_c('p'),[_v("Hello "+_s(name))]])
-
到此,整个
AST
就遍历完毕了,我们将得到的代码再包装一下,如下:` with(this){ reurn _c( 'div', { attrs:{"id":"NLRX"}, } [ _c('p'), [ _v("Hello "+_s(name)) ] ]) } `
-
最后,我们将上面得到的这个函数字符串传递给
createFunction
函数(关于这个函数在后面会介绍到),createFunction
函数会帮我们把得到的函数字符串转换成真正的函数,赋给组件中的render
选项,从而就是render
函数了。如下:res.render = createFunction(compiled.render, fnGenErrors) function createFunction (code, errors) { try { return new Function(code) } catch (err) { errors.push({ err, code }) return noop } }
以上就是根据一个简单的模板所对应的AST
生成render
函数的过程
3. dom diff
- render函数执行后的结果就是虚拟dom,虚拟dom时js对象包含dom的一些基本信息,ast语法树也是js对象,他与vnode的区别是ast这个对象一个模版只会生成一个后期不管数据怎么变动他都不会变。但是vnode是会随着数据的改变而变化的。vnode 最终要变成真实dom是在patch这个函数里,
patch
过程中基本会干三件事,分别是:创建节点,删除节点和更新节点。通过调用document.createElement创建节点,setAttribute、removeAttribute等方法更新属性实现 虚拟dom-> 真实dom
Vue.prototype._update = function (vnode) {
// 通过虚拟节点创建真实dom
const vm = this
const preVnode = vm._vnode
vm._vnode = vnode
if (!preVnode) {
vm.$el = patch(vm.$el, vnode) // 第一次渲染
} else {
vm.$el = patch(preVnode, vnode) // 更新
}
// console.log('vnode', vnode)
}
export function patch(oldVnode, vnode) {
if (!oldVnode) {
// 这里是组件的挂载
let el = createElm(vnode)
return el
} else {
// 1、判断是更新还是渲染,第一次渲染oldVnode是真实节点
const isRealElement = oldVnode.nodeType
if (isRealElement) { // 第一次渲染直接创建新节点插入就行
const oldElm = oldVnode // div app
const parentElm = oldVnode.parentNode // body
let el = createElm(vnode)
parentElm.insertBefore(el, oldElm.nextSibling) // 插入入到dom中
parentElm.removeChild(oldElm)
return el
} else { //更新操作需要进行dom diff
// dom diff 平级比对,应为正常业务很少父变子,子变父亲
if (oldVnode.tag !== vnode.tag) {
// 1、标签不一致直接替换
oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el)
}
// 2、如果文本呢?文本都没有tag
if (!oldVnode.tag) {
if (oldVnode.text !== vnode.text) {
oldVnode.el.textContent = vnode.text
}
}
// 3、标签一致而且不是文本(比对属性是否一致)
let el = (vnode.el = oldVnode.el)
updateProperties(vnode, oldVnode.data)
// 比对子节点
let oldChildren = oldVnode.children || []
let newChildren = vnode.children || []
if (oldChildren.length > 0 && newChildren.length > 0) {
// 新老都有子节点,需要比对子节点
updateChildren(el, oldChildren, newChildren)
} else if (newChildren.length > 0) {
// 新的有子节点,老的没有
for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i]
el.appendChild(createElm(child))
}
} else if (oldChildren.length > 0) {
// 老的有子节点,新的没有
el.innerHTML = ''
}
return el
}
}
}
- dom diff 原理 同级比较,采用双指针,分别进行 头和头比,尾和尾比,头和尾比,尾和头比,暴力对比的方式实现更新
function updateChildren(parent, oldChildren, newChildren) {
// vue 采用的是双指针
let oldStartIdx = 0
let oldStartVnode = oldChildren[0]
let oldEndIdx = oldChildren.length - 1
let oldEndVnode = oldChildren[oldEndIdx]
let newStartIdx = 0
let newStartVnode = newChildren[0]
let newEndIdx = newChildren.length - 1
let newEndVnode = newChildren[newEndIdx]
const makeIndexByKey = (children) => {
let map = {}
children.forEach((child, index) => {
if (child.key) {
map[child.key] = index // 根据key创建一个映射表
}
})
return map
}
let map = makeIndexByKey(oldChildren)
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (!oldStartVnode) {
oldStartVnode = oldChildren[++oldStartIdx]
}
if (!oldEndVnode) {
oldEndVnode = oldChildren[--oldEndIdx]
}
// 优化向后插入的情况 头和头比
if (isSameVnode(oldStartVnode, newStartVnode, 1)) {
patch(oldStartVnode, newStartVnode)
oldStartVnode = oldChildren[++oldStartIdx]
newStartVnode = newChildren[++newStartIdx]
}
// 优化向前插入的情况 尾和尾比
else if (isSameVnode(oldEndVnode, newEndVnode, 2)) {
patch(oldEndVnode, newEndVnode)
oldEndVnode = oldChildren[--oldEndIdx]
newEndVnode = newChildren[--newEndIdx]
}
// 头移尾 A B C D 变成 B C D A (头和尾比)
else if (isSameVnode(oldStartVnode, newEndVnode, 3)) {
patch(oldStartVnode, newEndVnode)
parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling)
oldStartVnode = oldChildren[++oldStartIdx]
newEndVnode = newChildren[--newEndIdx]
}
// 尾移头 A B C D 变成 D A B C (尾和头比)
else if (isSameVnode(oldEndVnode, newStartVnode, 4)) {
patch(oldEndVnode, newStartVnode)
parent.insertBefore(oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldChildren[--oldEndIdx]
newStartVnode = newChildren[++newStartIdx]
} else {
// A B C 变成 Q A F C N
// 暴力比对 乱序
// 先根据老节点的key 做一个映射表拿新的虚拟节点去映射表里找。如果可以查到就进行移动操作,找不到直接插入元素即可
let moveIndex = map[newStartVnode.key]
if (!moveIndex) {
parent.insertBefore(createElm(newStartVnode), oldStartVnode.el)
} else {
let moveVnode = oldChildren[moveIndex]
oldChildren[moveIndex] = undefined // 占位防止塌陷
parent.insertBefore(moveVnode.el, oldStartVnode.el)
patch(moveVnode, newStartVnode)
}
newStartVnode = newChildren[++newStartIdx]
}
}
if (newStartIdx <= newEndIdx) {
for (let i = newStartIdx; i <= newEndIdx; i++) {
// 将新增元素直接插入(可能是向后插入或向前插入)
let el = !newChildren[newEndIdx + 1]
? null
: newChildren[newEndIdx + 1].el
parent.insertBefore(createElm(newChildren[i]), el)
}
}
if (oldStartIdx <= oldEndIdx) {
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
// 删除多余的老节点
let child = oldChildren[i]
if (child) {
parent.removeChild(child.el)
}
}
}
}
4、key的作用
key
是虚拟节点(VNode)的唯一标识符,帮助 Vue 精准追踪每个节点的变化。
- 在
v-for
列表渲染中,若列表项顺序变化(如插入、删除),Vue 通过key
区分新旧节点,避免错误复用。 - 优化 Diff 算法性能。列表插入新元素时有
key
:仅插入新元素,其他节点复用,无key
:列表插入新元素时,可能导致后续元素全部更新 - 通过改变
key
强制销毁并重新渲染组件,解决状态残留问题。例如:动态组件切换
三、vue组件实现原理
1. 组件编译过程
在template —> ast -> render的过程中_c函数创建vnode,在此时判断tag是否是(div、span、p)等html的真实标签,不是的话按照组件来解析在这里创建组件节点。然后在patch的createElm函数里判断是组件的话调用createComponent生成真实dom
export function createElement(vm, tag, data = {}, ...children) {
let key = data && data.key
if (key) {
delete data.key
}
if (isReservedTag(tag)) {
// 原始标签的处理 div span ...
return vnode(tag, data, key, children, undefined)
} else {
// 组件的处理
let Ctor = vm.$options.components[tag]
return createComponent(vm, tag, data, key, children, Ctor)
}
}
function createComponent(vm, tag, data, key, children, Ctor) {
if (isObject(Ctor)) {
Ctor = vm.$options._base.extend(Ctor)
}
data.hooks = {
init(vnode) {
// 当前组件的实例就是componentInstance
let child = new Ctor({ _isComponent: true })
vnode.componentInstance = child
// 组件的挂载
child.$mount()
},
}
return vnode(
`vue-component${Ctor.cid}-${tag}`,
data,
key,
undefined,
undefined,
{
Ctor,
children,
}
)
}
patch 函数中 判断组件逻辑
function createComponent(vnode) {
// 判断是不是组件
let i = vnode.data
if ((i = i.hooks) && (i = i.init)) {
i(vnode)
}
if (vnode.componentInstance) {
return true
}
}
// 根据虚拟节点创建真实节点
export function createElm(vnode) {
const { tag, data, children, key, text } = vnode
if (typeof tag === 'string') {
// 有可能是组件
if (createComponent(vnode)) {
return vnode.componentInstance.$el
}
vnode.el = document.createElement(tag)
updateProperties(vnode)
children.forEach((child) => {
vnode.el.appendChild(createElm(child))
})
} else {
vnode.el = document.createTextNode(text)
}
return vnode.el
}
2、Vue 组件 data 为什么必须是函数
因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。
四、new Vue()干了什么
new Vue() 执行this._init函数进行初始化。init函数中执行以下流程 1、mergeOptions -> 2、initLifecycle(vm) -> 3、initEvents(vm)-> 4、initRender(vm) ->5、callHook(vm, 'beforeCreate') -> 6、initInjections(vm) -> 7、initState(vm) -> 8、initProvide(vm) -> 9、callHook(vm, 'created') -> 10、vm.$mount
1、mergeOptions
export function mergeOptions(
parent: Record<string, any>,
child: Record<string, any>,
vm?: Component | null
): ComponentOptions {
if (__DEV__) {
checkComponents(child)
}
if (isFunction(child)) {
// @ts-expect-error
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
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) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
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 strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
2、initLifecycle
export function initLifecycle(vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._provided = parent ? parent._provided : Object.create(null)
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
3、initEvents
export function initEvents(vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
export function updateComponentListeners(
vm: Component,
listeners: Object,
oldListeners?: Object | null
) {
target = vm
updateListeners(
listeners,
oldListeners || {},
add,
remove,
createOnceHandler,
vm
)
target = undefined
}
export function updateListeners(
on: Object,
oldOn: Object,
add: Function,
remove: Function,
createOnceHandler: Function,
vm: Component
) {
let name, cur, old, event
for (name in on) {
cur = on[name]
old = oldOn[name]
event = normalizeEvent(name)
if (isUndef(cur)) {
__DEV__ &&
warn(
`Invalid handler for event "${event.name}": got ` + String(cur),
vm
)
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm)
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture)
}
add(event.name, cur, event.capture, event.passive, event.params)
} else if (cur !== old) {
old.fns = cur
on[name] = old
}
}
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
remove(event.name, oldOn[name], event.capture)
}
}
}
4、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 as Component)
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = parentVnode
? normalizeScopedSlots(
vm.$parent!,
parentVnode.data!.scopedSlots,
vm.$slots
)
: 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
// @ts-expect-error
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
// @ts-expect-error
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// $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 (__DEV__) {
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
)
}
}
5、initInjections
export function initInjections(vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (__DEV__) {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
export function resolveInject(
inject: any,
vm: Component
): Record<string, any> | undefined | null {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
const result = Object.create(null)
const keys = hasSymbol ? Reflect.ownKeys(inject) : Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// #6574 in case the inject object is observed...
if (key === '__ob__') continue
const provideKey = inject[key].from
if (provideKey in vm._provided) {
result[key] = vm._provided[provideKey]
} else if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = isFunction(provideDefault)
? provideDefault.call(vm)
: provideDefault
} else if (__DEV__) {
warn(`Injection "${key as string}" not found`, vm)
}
}
return result
}
}
6、initState
export function initState(vm: Component) {
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
// Composition API
initSetup(vm)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
const ob = observe((vm._data = {}))
ob && ob.vmCount++
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
7、initProvide
export function initProvide(vm: Component) {
const provideOption = vm.$options.provide
if (provideOption) {
const provided = isFunction(provideOption)
? provideOption.call(vm)
: provideOption
if (!isObject(provided)) {
return
}
const source = resolveProvided(vm)
// IE9 doesn't support Object.getOwnPropertyDescriptors so we have to
// iterate the keys ourselves.
const keys = hasSymbol ? Reflect.ownKeys(provided) : Object.keys(provided)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
Object.defineProperty(
source,
key,
Object.getOwnPropertyDescriptor(provided, key)!
)
}
}
}
xport function resolveProvided(vm: Component): Record<string, any> {
// by default an instance inherits its parent's provides object
// but when it needs to provide values of its own, it creates its
// own provides object using parent provides object as prototype.
// this way in `inject` we can simply look up injections from direct
// parent and let the prototype chain do the work.
const existing = vm._provided
const parentProvides = vm.$parent && vm.$parent._provided
if (parentProvides === existing) {
return (vm._provided = Object.create(parentProvides))
} else {
return existing
}
}
8、初始化时为什么是这样的顺序initInjections → initState → initProvide
Vue2 初始化顺序 initInjections → initState → initProvide
的设计,本质是为了:
- 依赖方向性:父组件的
provide
在子组件初始化前就绪,子组件的inject
能及时获取父级数据 。 - 数据完整性:
initState
中的数据可被provide
动态引用,同时inject
的值能支持data
的初始化逻辑。 - 生命周期隔离:通过钩子函数分隔不同阶段的数据操作,避免状态混乱 这一顺序设计体现了 Vue2 对组件化开发中依赖管理和数据流的深度考量。
五、 nextTick的原理
在 Vue.js 2.x 中,nextTick
是一个核心机制,用于延迟回调函数的执行直到下一次 DOM 更新周期之后。它的核心原理基于 JavaScript 的事件循环(Event Loop)和异步任务队列机制。
Vue 2.7 的 nextTick
实现位于 src/core/util/next-tick.js
。其核心逻辑包含三个部分:
- 回调队列管理:用
callbacks
数组存储待执行的回调。 - 异步触发机制:通过
timerFunc
函数选择合适的异步策略(微任务/宏任务)。 - 状态管理:用
pending
标志位避免重复触发。
思考
:nextTick里嵌套nextTick执行时机
核心代码简版
const callbacks = []; // 存储回调的队列
let pending = false; // 标记是否已触发异步任务
// 执行队列中的所有回调
function flushCallbacks() {
pending = false; // 这里看起来是立马设置为false,实际却是等同步任务执行完毕后才执行的。
// 所以在一个同步任务里的多个回调函数都进入callbacks里了再依次执行。
// 并且由于之前pending一直为true 所以timerFunc只会执行一次
const copies = callbacks.slice(0);
// callbacks里的回调函数中有可能还使用nextTick所以这里需要执行备份并把之前的callbacks清空
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
// 定义异步触发策略(优先级:Promise > MutationObserver > setImmediate > setTimeout)
let timerFunc;
if (typeof Promise !== 'undefined') {
// 优先使用 Promise(微任务)
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== 'undefined') {
// 降级为 MutationObserver(微任务)
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);
};
} else if (typeof setImmediate !== 'undefined') {
// 降级为 setImmediate(宏任务)
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 最后使用 setTimeout(宏任务)
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
// 暴露给外部的 nextTick 函数
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve;
// 将回调包装后推入队列
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx);
} catch (e) { /* 错误处理 */ }
} else if (_resolve) {
_resolve(ctx);
}
});
// 如果未触发异步任务,则启动
if (!pending) {
pending = true;
timerFunc();
}
// 支持 Promise 链式调用(当没有传入 cb 时)
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve;
});
}
}
六. Vue 事件机制,手写off,once
Vue 的事件机制基于发布-订阅模式,以下是手写实现 $on
、$off
、$emit
和 $once
的核心代码,vue源码中_events是在initEvents进行初始化的
class Vue {
constructor() {
this._events = Object.create(null); // 存储事件回调
}
// 注册事件
$on(event, callback) {
if (Array.isArray(event)) {
event.forEach(e => this.$on(e, callback));
return this;
}
(this._events[event] || (this._events[event] = [])).push(callback);
return this;
}
// 注册一次性事件
$once(event, callback) {
// 这里定义一个包装函数当作回调,函数执行时先移除事件仔执行callback达到只执行一次的目的
const wrapper = (...args) => {
this.$off(event, wrapper);
callback.apply(this, args);
};
wrapper.fn = callback; // 保存原回调用于识别
this.$on(event, wrapper);
return this;
}
// 触发事件
$emit(event, ...args) {
const cbs = this._events[event]?.slice(); // 复制数组避免执行时被修改
if (cbs) {
cbs.forEach(cb => cb.apply(this, args));
}
return this;
}
// 移除事件
$off(event, callback) {
// 无参数移除所有事件
if (!arguments.length) {
this._events = Object.create(null);
return this;
}
// 数组事件递归处理
if (Array.isArray(event)) {
event.forEach(e => this.$off(e, callback));
return this;
}
// 无对应事件直接返回
const cbs = this._events[event];
if (!cbs) return this;
// 无回调移除整个事件队列
if (!callback) {
this._events[event] = null;
return this;
}
// 移除特定回调
let i = cbs.length;
while (i--) {
const cb = cbs[i];
// 这里的cb.fn是在$once里设置的包装函数的属性,也就是原回掉函数
if (cb === callback || cb.fn === callback) {
cbs.splice(i, 1);
}
}
return this;
}
}
待补充 七、keep-alive 的实现原理和缓存策略 11.组件插槽原理 13.vue-router 14.vuex