在看Vue的源码之前,先来了解一个概念 :虚拟节点。
前端发展很多年,直到出现了虚拟DOM,才可以从操作DOM解脱出来。
JQuery的出现,简化了操作DOM的过程,但是还是摆脱不了操作DOM。
而虚拟DOM的目的是,使用虚拟节点代替真实节点,所有操作都发生在虚拟节点,然后通过diff算法对比新旧两棵虚拟DOM,计算出更新真实DOM的最少操作,由框架代替用户执行这些操作,所以用户可以把大量的精力放在业务逻辑上
vue中的VNode
VueJS的虚拟DOM是基于开源Snabbdom
的。
Snabbdom
基本使用:
var snabbdom = require('snabbdom');
var patch = snabbdom.init([
require('snabbdom/modules/class').default,
require('snabbdom/modules/props').default,
require('snabbdom/modules/style').default,
require('snabbdom/modules/eventlisteners').default
]);
var h = require('snabbdom/h').default;
var container = document.getElementById('container');
var vnode = h('div#container.two.classes', { on: { click: onClick } }, [
h('span', { style: { fontWeight: 'bold' } }, 'This is bold'),
' and this is just normal text',
h('a', { props: { href: '/foo' } }, 'I\'ll take you places!')
]);
patch(container, vnode);
function onClick() {
console.log('点击');
}
更新节点:
var newVnode = h('div#container.two.classes', {on: {click: anotherEventHandler}}, [
h('span', {style: {fontWeight: 'normal', fontStyle: 'italic'}}, 'This is now italic type'),
' and this is still just normal text',
h('a', {props: {href: '/bar'}}, 'I\'ll take you places!')
]);
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
patch比较新旧的虚拟节点,直接修改dom树。
主要就是调用h函数创建虚拟节点vnode,创建好后调用patch函数生成真实的dom节点。
Vue初始化流程
Vue最基本的使用:
{{message}}
刷新页面,来到Vue的初始化方法。往下走,进入_init()
方法。如上图所示这个方法是在在initMix
函数中定义的,继续往下。
来到了_init()
方法,开始处理options。options是我们创建Vue的时候传进来的:
继续看这段代码:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor), // 取出构造函数中的Options 也就是Vue.options
options || {},
vm
)
resolveConstructorOptions
函数取出Vue构造函数的options,其实就是Vue.options,再和传进来的参数进行合并。而Vue.options在哪定义的呢?详细可以看这里:人人都能懂的Vue源码系列—03—resolveConstructorOptions函数-上,是在src/platforms/web/runtime/index.js
中定义的。
Vue.options的默认值:
来到mergeOptions
方法:
关键函数:
function mergeField (key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
strats定义了一系列方法来处理这些参数,我们先看看strats.data:
最后是返回了一个函数,这个函数在什么时候被调用呢?
可以在Call Stack里面看到是在初始化data的时候调用的。这个后面再详细看。
处理完options,开始初始化Proxy:initProxy
。Proxy是啥?ES6中的Proxy,可以把某个对象包起来,得到一个代理对象,调用这个代理对象的值会间接调用原始对象的值,因此可以在调用的过程中做一些处理,比如当没有这个值的时候返回一个默认值等。详细可看理解Javascript的Proxy。Proxy的基本使用:
let data = {}
let dataProxy = new Proxy(data, {
get(obj, prop) {
if (prop === 'name') {
if (!obj.name) {
return 'Anonymous'
}
}
return obj[prop]
}
})
console.log(dataProxy.name)
那Vue这里的initProxy要做什么呢?
initProxy里面会定义一个属性:_renderProxy
。如果当前环境不支持Proxy,那么_renderProxy直接就是vm本身。前面也提到,Proxy就是让直接调用变成间接调用,然后在中间做些额外处理。
hasProxy源码:
var hasProxy =
typeof Proxy !== 'undefined' && isNative(Proxy);
function isNative (Ctor) {
return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}
确保Proxy是原生代码,没有被修改。
hasHandler的源码:
var hasHandler = {
has: function has (target, key) {
var has = key in target;
var isAllowed = allowedGlobals(key) ||
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data));
if (!has && !isAllowed) {
if (key in target.$data) { warnReservedPrefix(target, key); }
else { warnNonPresent(target, key); }
}
return has || !isAllowed
}
};
has方法隐藏某些属性,不被in运算符发现。当查看vm是否存在某个属性时会来到这个方法。这里主要的作用就是发一些警告。
人人都能懂的Vue源码系列—07—initProxy
继续往下,来到initState()方法
function initState (vm) {
vm._watchers = [];
var 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);
}
}
先看initData()方法
这里就会调用前面提到的mergedInstanceDataFn()方法取出data。接着判断data、props、methods里面有没有重名的。最后就是重头戏,监听对象:
observe(data, true /* asRootData */);
...
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var 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
}
其他的先忽略,直接到创建Observer这里:
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
var Observer = function Observer (value) {
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);
}
};
这里首先把被观察的对象保存在Observer中,然后创建一个Dep,这个很重要,后面详细说。接着给对象定义一个变量__ob__
保存Observer,所以引用关系变成了:
Observer -> value -> Object -> __ob__
-> Observer
注意这个Object还保存在vm的_data中。
总之经过这一层vm的data多了一个属性ob,它就是Observer。
接下来看看this.walk(value)
:
遍历obj的key然后调用defineReactive$$1
方法。
/**
* Define a reactive property on an Object.
*/
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var 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) {
var 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 (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();
}
});
}
这段代码还挺长的,但主要工作就是给obj定义key属性,并重写它的get和set方法。开头还定义了一个Dep对象,它主要用来做依赖收集以及发通知的。当get方法被调用的时候,如果Dep.target不为空,就会调用dep.depend()
收集起来,这里的Dep.target通常就是Watcher。当组件第一次渲染的时候,会把Dep.target设置为被渲染组件的Watcher,在渲染的过程中调用到get方法时,Watcher就会被收集到该变量的dep中。当setter方法被调用时再通知给dep下面的所有watcher,watcher就会调用update方法。
到这里data初始化结束。
接下来调用$mount
方法。
var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && query(el);
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
warn(
"Do not mount Vue to or - mount to normal elements instead."
);
return this
}
var options = this.$options;
// resolve template/el and convert to render function
if (!options.render) {
var template = options.template;
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template);
/* istanbul ignore if */
if (!template) {
warn(
("Template element not found or is empty: " + (options.template)),
this
);
}
}
} else if (template.nodeType) {
template = template.innerHTML;
} else {
{
warn('invalid template option:' + template, this);
}
return this
}
} else if (el) {
template = getOuterHTML(el);
}
if (template) {
/* istanbul ignore if */
if (config.performance && mark) {
mark('compile');
}
var ref = compileToFunctions(template, {
outputSourceRange: "development" !== 'production',
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this);
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
options.render = render;
options.staticRenderFns = staticRenderFns;
/* istanbul ignore if */
if (config.performance && mark) {
mark('compile end');
measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
}
}
}
return mount.call(this, el, hydrating)
};
这里先取出被挂载到的元素。往下走,options.render和options.template都为空,所以通过getOuterHTML(el)取出Html字符串,它会通过compileToFunctions
被转换成render方法:
(function anonymous(
) {
with (this) { return _c('div', { attrs: { "id": "app" } }, [_c('p', [_v(_s(message))])]) }
})
是不是很像Snabbdom创建虚拟节点的方法。其实render方法就是创建虚拟节点的。我们写vue文件的template其实也会被转成render函数,不过是在本地编译的时候就转好了。
接下来调用mount函数。
接下来是关键的代码。updateComponent调用vm的_render。接下来创建Watcher。
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
...
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid$2; // 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 = expOrFn.toString();
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
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();
};
expOrFn即updateComponent,this.getter = expOrFn = updateComponent。 在Watcher的构造函数最后调用了get方法。get方法的定义:
/**
* Evaluate the getter, and re-collect dependencies.
*/
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var 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 {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
pushTarget(this)把Dep.target设置为当前Watcher。接着调用this.getter,也就是updateComponent,也就是vm._update。
vm._update(vm._render(), hydrating);
_render函数会创建虚拟节点。
这里的render就是前面把Html编译成的函数。拿到虚拟节点后传给_update。
__patch__
会比对前后虚拟节点的最小差异创建新的dom节点并进行替换。
彻底理解Vue中的Watcher、Observer、Dep