前言
疯狂搬砖了几个星期,每次坐到电脑前都觉得好累,精力被消耗,热情被消磨...这时候才逐渐意识到了已经做了社畜几个月了,不是当年那个充满精力,在图书馆学啥就学啥的热血青年。
src/core/instance/lifecycle.js
还记得src/platforms/web/runtime/index.js中有这么一段代码吗
// 实现一个$mount方法,调用mountComponent
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
其中的 mountComponent 具体做了什么在上一篇中我并没有分析它,在本篇中会展开说
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el;
if (!vm.$options.render) {
// 如果没有render就赋值为一个创建空vdom的函数
vm.$options.render = createEmptyVNode;
}
// 调用beforeMount钩子
callHook(vm, "beforeMount");
// 组件更新函数声明
let updateComponent;
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && config.performance && mark) {
...
};
} else {
updateComponent = () => {
// 首先执行render -> vdom
// 然后_update将vdom转为dom
vm._update(vm._render(), hydrating);
};
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 新建一个watcher,立即调用一次updateComponent
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted && !vm._isDestroyed) {
// 调用beforeUpdate
callHook(vm, "beforeUpdate");
}
},
},
true /* isRenderWatcher */
);
hydrating = false;
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, "mounted");
}
return vm;
}
总结:
- 调用
beforeMount - 声明组件更新函数
updateComponent - 新建一个
watcher,并立即调用一次updateComponent - 如果
$vnode为null,说明已经将虚拟dom转换为真实dom,此时调用mounted函数
我们再来看一下vm._update的函数干了什么
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this;
const prevEl = vm.$el;
const prevVnode = vm._vnode;
const restoreActiveInstance = setActiveInstance(vm);
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
// 初始化是没有preVnode
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
// diff
vm.$el = vm.__patch__(prevVnode, vnode);
}
restoreActiveInstance();
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null;
}
if (vm.$el) {
vm.$el.__vue__ = vm;
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el;
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
};
总结:
- 判断当前处理的元素是否有
_vnode的选项- 没有的话,通过
_patch初始化渲染,将处理后的真实dom挂载到$el选项中 - 有的话,通过
_patch将之前的旧vnode和当前的新vnode进行diff操作,最后挂载到$el中
- 没有的话,通过
src/core/vdom/patch.js
接下来就是看这 __patch__ 函数做了什么
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
// 一开始初始化的时候没有vnode,进去的是真实元素,走的第一个if
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// oldElm指的是宿主元素
// replacing existing element
const oldElm = oldVnode.elm
// parentElm指的是宿主元素的父元素
const parentElm = nodeOps.parentNode(oldElm)
// create new node
// 在宿主元素的兄弟节点创建一个解析好的真实dom元素
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
// 删除掉旧的元素
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
这个过程可以debugger的时候看到
<body>
<div id="app">
<h2>初始化</h2>
<div>
<p>counter --- {{counter}}</p>
</div>
</div>
</body>
<script src="../../dist/vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
counter: 1,
},
});
window.app = app;
</script>
总结:
patch函数更新的方式是通过vnode在宿主元素的兄弟节点插入一个解析好的真实dom,然后把旧的dom移除
吐槽
关于 new Vue() 发生了什么大体上已经知道了流程,但是更具体的做了什么,这点还需要时间去研究,毕竟不知道开发团队当时的设计和思路啥的,盲人摸象式的阅读还是挺难受的。。希望有经验的大佬务必能给点建议
initState
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, "beforeCreate");
initInjections(vm); // resolve injections before data/props
// data/props/methods/computed/watch
initState(vm);
initProvide(vm);
还记得上节 initMixin 中看到的几个初始化函数吗,我们最常用的组件状态都是在 initState 中,而且可以推测到这个函数与响应式有关,所以我们就了解这个函数吧。
export function initState (vm: Component) {
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)
}
}
小结:
- 初始化状态的顺序
props>methods>data>computed>watch - 小提示:不知道大家有没有试过
props里和data里的属性重名,结果搞出bug的情况,这时候就可以大概推测到他们之间有个顺序关系,这个名字被占了就不会被后面的覆盖
再来看initData
function initData (vm: Component) {
let data = vm.$options.data
// 判断是否是函数,函数就执行获取里面的数
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// 判断是否是纯对象,警告
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
// 下面两个if是判断是否有和props和method中重名的属性,警告
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
// 响应式
observe(data, true /* asRootData */)
}
小结:
- 判断是否是
data选项是否是函数,函数就执行获取真实的对象 - 检测是否是纯对象,非纯对象进行警告
- 遍历属性,如果有
props和methods中重名的属性,进行一个警告 - 响应式处理对象
响应式处理
响应式处理嘛,大家都知道vue2用的 Object.defineProperty,自己写也能写出来,但是我还没见过源码是怎么处理的,多了哪些细节,现在就来看一下吧。
export function observe(value: any, asRootData: ?boolean): Observer | void {
// 属性是非对象或者是vnode的直接跳过
if (!isObject(value) || value instanceof VNode) {
return;
}
let ob: Observer | void;
// 如果经过响应式处理过了,直接取__ob__
if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 初始化传入需要响应式处理的对象
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob;
}
小结:
observe对非对象和vnode 不生效- 已经经过响应式处理的对象会有
__ob__标识,并直接返回__ob__ - 数组,纯对象会进行响应式的处理
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
// 此处dep的目的:
// 如果使用Vue.set或者delete添加或删除属性,负责通知更新
// 举个例子,你直接删除某个属性,defineProperty无法拦截到
constructor(value: any) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, "__ob__", this);
// 分辨传入对象的类型
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
// 针对普通对象,遍历每个key进行defineReactive
walk(obj: Object) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
/**
* Observe a list of Array items.
*/
// 针对数组
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
}
}
总结:
- 对于需要响应式处理的对象,会生成一个
dep与其对应。这个dep在对象添加或删除属性的时候负责通知。 - 在对象中添加了
__ob__属性,值为Observer实例 - 根据对象是普通对象还是数组进行分开的处理
普通对象的响应式处理
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 创建key和dep一一对应的关系
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) && arguments.length === 2) {
val = obj[key];
}
// 对于嵌套对象进行递归的响应式拦截
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
// 如果存在,说明此处调用触发者是一个Watcher实例
// dep 与 watcher 是 n对n 的关系
if (Dep.target) {
// 建立dep 和 Dep.target之间的依赖关系
dep.depend();
if (childOb) {
// 建立ob内部dep和Dep.target之间依赖关系
childOb.dep.depend();
// 如果是数组,数组内部所有项目都要做相同处理
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== "production" && customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) return;
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
},
});
}
针对Observer和Dep我们来看个例子,理清楚调用链是怎样的
const app = new Vue({
el: "#app",
data: {
obj: {
a: 'asd'
}
},
});
首先
initData() -> observe({obj}) -> ob = new Observer({obj}) -> Observer中this.dep = new Dep() 这时创建了一个ob, 一个dep
然后
walk({obj}) -> keys = ["obj"]; 对keys中每个key进行defineReactive -> defineReactive(obj,key)中 dep = new Dep() 这时又创建了一个dep
之后
var childOb = !shallow && observe(val); 其中val = {a: "asd"} 再重复执行observe
最后
我们可以知道这个例子中有 2个ob,4个dep
数组如何做的响应式
我们都知道,Vue2 中响应式拦截用的是 Object.defineProperty,这个方法拦截不到 push/pop/splice/... 这些方法,所以 Vue2 中是在原来这些方法的基础上做了些处理。接下来我们一起来看下他们是怎么做的。
// can we use __proto__?
export const hasProto = '__proto__' in {}
// 获取原型
const arrayProto = Array.prototype
// 复制一份
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
// 重写原来的方法,用一个mutator函数替代
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
}
// 通过push/unshift/splice添加的元素也需要进行响应式的处理
if (inserted) ob.observeArray(inserted)
// notify change
// 通知更新
ob.dep.notify()
return result
})
})
class Observer {
if (Array.isArray(value)) {
// 判断浏览器是否有__proto__,有的话可以重写原型
// 没有的话。。我也不知道哪些浏览器会没有这玩意,总之会直接把那几个方法定义到数组中去
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
}
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
*/
// 替换原型
function protoAugment(target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src;
/* eslint-enable no-proto */
}
/**
* Augment a target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
function copyAugment(target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i];
def(target, key, src[key]);
}
}
总结
本来还想写一下Watcher,Observer和 Dep 的,但是再写下去感觉太长了,而且不好整理...就把它移到下一篇中说吧。
问: 组件执行 $mount 的时候做了哪些事情
答:首先执行beforeMount,然后声明组件更新函数 updateComponent,然后新建一个watcher并调用这个updateComponent将解析好的真实dom挂载到宿主元素上,最后执行mounted
问:patch函数做完diff后,是怎么处理旧的dom
答:patch 函数更新的方式是通过 vnode 在宿主元素的兄弟节点插入一个解析好的真实dom,然后把旧的dom移除
问:如果props,data,methods中有同名的属性foo,那么访问 this.foo其实是访问这三个选项中的哪个
答:因为在 initState 中执行顺序是 props > methods > data,所以访问的是this.foo = this.props.foo
问:Vue2 中数组是如何做的响应式处理
答:先判断对象是否有__proto__属性,有的话先通过Object.create将其复制下来,然后针对七个改变数组的方法重写,分别是push/pop/unshift/shift/splice/sort/reserve。复写的方式是先调用数组本身对应的方法,然后对push/unshift/splice这三个添加的元素进行响应式处理,最后通知更新。如果没有__proto__的话,就直接往数组实例中塞入这七个改写后的方法。