初始化Vue
我们简单实例化一个Vue的实例, 下面的我们针对这个简单的实例进行深入的去思考:
// app Vue instance
var app = new Vue({
data: {
newTodo: '',
},
// watch todos change for localStorage persistence
watch: {
newTodo: {
handler: function (newTodo) {
console.log(newTodo);
},
sync: false,
before: function () {
}
}
}
})
// mount
app.$mount('.todoapp')
initState
在上面我们有添加一个watch
的属性配置:
从上面的代码我们可知,我们配置了一个key为newTodo
的配置项, 我们从上面的代码可以理解为:
当newTodo
的值发生变化了,我们需要执行hander
方法,所以我们来分析下具体是怎么实现的。
我们还是先从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);
}
}
//在此我向大家推荐一个前端全栈开发交流圈:582735936 突破技术瓶颈,提升思维能力
我们来具体分析下initWatch
方法:
function initWatch (vm, watch) {
for (var key in watch) {
var handler = watch[key];
if (Array.isArray(handler)) {
for (var i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
从上面的代码分析,我们可以发现watch
可以有多个hander
,写法如下:
watch: {
todos:
[
{
handler: function (todos) {
todoStorage.save(todos)
},
deep: true
},
{
handler: function (todos) {
console.log(todos)
},
deep: true
}
]
},
我们接下来分析createWatcher
方法:
function createWatcher (
vm,
expOrFn,
handler,
options
) {
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options)
}
//在此我向大家推荐一个前端全栈开发交流圈:582735936 突破技术瓶颈,提升思维能力
总结:
- 从这个方法可知,其实我们的
hanlder
还可以是一个string
- 并且这个
hander
是vm
对象上的一个方法,我们之前已经分析methods
里面的方法都最终挂载在vm
实例对象上,可以直接通过vm["method"]
访问,所以我们又发现watch
的另外一种写法, 直接给watch
的key
直接赋值一个字符串名称, 这个名称可以是methods
里面定一个的一个方法:
watch: {
todos: 'newTodo'
},
methods: {
handlerTodos: function (todos) {
todoStorage.save(todos)
}
}
接下来调用$watch
方法
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
var watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
}
}
return function unwatchFn () {
watcher.teardown();
}
};
在这个方法,我们看到有一个immediate
的属性,中文意思就是立即
, 如果我们配置了这个属性为true
, 就会立即执行watch
的hander
,也就是同步 执行, 如果没有设置, 则会这个watcher
是异步执行,下面会具体分析怎么去异步执行的。 所以这个属性可能在某些业务场景应该用的着。
在这个方法中new
了一个Watcher
对象, 这个对象是一个重头戏,我们下面需要好好的分析下这个对象。 其代码如下(删除只保留了核心的代码):
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm;
vm._watchers.push(this);
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
}
}
this.value = this.lazy
? undefined
: this.get();
};
主要做了如下几件事:
- 将
watcher
对象保存在vm._watchers
中 - 获取
getter
,this.getter = parsePath(expOrFn);
- 执行
this.get()
去获取value
其中parsePath
方法代码如下,返回的是一个函数:
var bailRE = /[^\w.$]/;
function parsePath (path) {
if (bailRE.test(path)) {
return
}
var segments = path.split('.');
return function (obj) {
for (var i = 0; i < segments.length; i++) {
if (!obj) { return }
obj = obj[segments[i]];
}
return obj
}
}
//在此我向大家推荐一个前端全栈开发交流圈:582735936 突破技术瓶颈,提升思维能力
在调用this.get()
方法中去调用value = this.getter.call(vm, vm);
然后会调用上面通过obj = obj[segments[i]];
去取值,如vm.newTodo
, 我们从 深入了解 Vue 响应式原理(数据拦截),已经知道,Vue 会将data
里面的所有的数据进行拦截,如下:
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();
}
});
所以我们在调用vm.newTodo
时,会触发getter
,所以我们来深入的分析下getter
的方法
getter
getter 的代码如下:
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
}
- 首先取到值
var value = getter ? getter.call(obj) : val;
- 调用
Dep
对象的depend
方法, 将dep
对象保存在target
属性中Dep.target.addDep(this);
而target
是一个Watcher
对象 其代码如下:
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};
//在此我向大家推荐一个前端全栈开发交流圈:582735936 突破技术瓶颈,提升思维能力
生成的Dep
对象如下图:
3. 判断是否有自属性,如果有自属性,递归调用。
现在我们已经完成了依赖收集, 下面我们来分析当数据改变是,怎么去准确地追踪所有修改。
准确地追踪所有修改
我们可以尝试去修改data
里面的一个属性值,如newTodo
, 首先会进入set
方法,其代码如下:
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();
}
下面我来分析这个方法。
- 首先判断新的value 和旧的value ,如果相等,则就直接return
- 调用
dep.notify();
去通知所有的subs
,subs
是一个类型是Watcher
对象的数组 而subs
里面的数据,是我们上面分析的getter
逻辑维护的watcher
对象.
而notify
方法,就是去遍历整个subs
数组里面的对象,然后去执行update()
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if (!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(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
//在此我向大家推荐一个前端全栈开发交流圈:582735936 突破技术瓶颈,提升思维能力
上面有一个判断config.async
,是否是异步,如果是异步,需要排序,先进先出, 然后去遍历执行update()
方法,下面我们来看下update()
方法。
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
上面的方法,分成三种情况:
- 如果
watch
配置了lazy
(懒惰的),不会立即执行(后面会分析会什么时候执行) - 如果配置了
sync
(同步)为true
则会立即执行hander
方法 - 第三种情况就是会将其添加到
watcher
队列(queue
)中
我们会重点分析下第三种情况, 下面是queueWatcher
源码
function queueWatcher (watcher) {
var id = watcher.id;
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// queue the flush
if (!waiting) {
waiting = true;
if (!config.async) {
flushSchedulerQueue();
return
}
nextTick(flushSchedulerQueue);
}
}
}
//在此我向大家推荐一个前端全栈开发交流圈:582735936 突破技术瓶颈,提升思维能力
- 首先
flushing
默认是false
, 所以将watcher
保存在queue
的数组中。 - 然后
waiting
默认是false
, 所以会走if(waiting)
分支 -
config
是Vue
的全局配置, 其async
(异步)值默认是true
, 所以会执行nextTick
函数。
下面我们来分析下nextTick
函数
nextTick
nextTick
代码如下:
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
if (useMacroTask) {
macroTimerFunc();
} else {
microTimerFunc();
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
//在此我向大家推荐一个前端全栈开发交流圈:582735936 突破技术瓶颈,提升思维能力
nextTick
主要做如下事情:
- 将传递的参数
cb
的执行放在一个匿名函数中,然后保存在一个callbacks
的数组中 -
pending
和useMacroTask
的默认值都是false
, 所以会执行microTimerFunc()
(微Task)microTimerFunc()
的定义如下:
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}
其实就是用Promise
函数(只分析Promise
兼容的情况), 而Promise
是一个i额微Task 必须等所有的宏Task 执行完成后才会执行, 也就是主线程空闲的时候才会去执行微Task;
现在我们查看下flushCallbacks
函数:
function flushCallbacks () {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
这个方法很简单,
- 第一个是变更
pending
的状态为false
- 遍历执行
callbacks
数组里面的函数,我们还记得在nextTick
函数中,将cb
保存在callbacks
中。
我们下面来看下cb
的定义,我们调用nextTick(flushSchedulerQueue);
, 所以cb
指的就是flushSchedulerQueue
函数, 其代码如下:
function flushSchedulerQueue () {
flushing = true;
var watcher, id;
queue.sort(function (a, b) { return a.id - b.id; });
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run();
// in dev build, check and stop circular updates.
if (has[id] != null) {
circular[id] = (circular[id] || 0) + 1;
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? ("in watcher with expression \"" + (watcher.expression) + "\"")
: "in a component render function."
),
watcher.vm
);
break
}
}
}
// keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
//在此我向大家推荐一个前端全栈开发交流圈:582735936 突破技术瓶颈,提升思维能力
resetSchedulerState();
// call component updated and activated hooks
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush');
}
}
- 首先将
flushing
状态开关变成true
- 将
queue
进行按照ID
升序排序,queue
是在queueWatcher
方法中,将对应的Watcher
保存在其中的。 - 遍历
queue
去执行对应的watcher
的run
方法。 - 执行
resetSchedulerState()
是去重置状态值,如waiting = flushing = false
- 执行
callActivatedHooks(activatedQueue);
更新组件 ToDO: - 执行
callUpdatedHooks(updatedQueue);
调用生命周期函数updated
- 执行
devtools.emit('flush');
刷新调试工具。
我们在3. 遍历queue去执行对应的watcher的run 方法。, 发现queue
中有两个watcher
, 但是我们在我们的app.js
中初始化Vue
的 时候watch
的代码如下:
watch: {
newTodo: {
handler: function (newTodo) {
console.log(newTodo);
},
sync: false
}
}
从上面的代码上,我们只Watch
了一个newTodo
属性,按照上面的分析,我们应该只生成了一个watcher
, 但是我们却生成了两个watcher
了, 另外一个watcher
到底是怎么来的呢?
总结:
- 在我们配置的
watch
属性中,生成的Watcher
对象,只负责调用hanlder
方法。不会负责UI的渲染 - 另外一个
watch
其实算是Vue
内置的一个Watch
(个人理解),而是在我们调用Vue
的$mount
方法时生成的, 如我们在我们的app.js
中直接调用了这个方法:app.$mount('.todoapp')
. 另外一种方法不直接调用这个方法,而是在初始化Vue
的配置中,添加了一个el: '.todoapp'
属性就可以。这个Watcher
负责了UI的最终渲染,很重要,我们后面会深入分析这个Watcher
-
$mount
方法是最后执行的一个方法,所以他生成的Watcher
对象的Id
是最大的,所以我们在遍历queue
之前,我们会进行一个升序 排序, 限制性所有的Watch
配置中生成的Watcher
对象,最后才执行$mount
中生成的Watcher
对象,去进行UI渲染。
$mount
我们现在来分析$mount
方法中是怎么生成Watcher
对象的,以及他的cb
是什么。其代码如下:
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
- 从上面的代码,我们可以看到最后一个参数
isRenderWatcher
设置的值是true
, 表示是一个Render Watcher, 在watch
中配置的,生成的Watcher
这个值都是false
, 我们在Watcher
的构造函数中可以看到:
if (isRenderWatcher) {
vm._watcher = this;
}
如果isRenderWatcher
是true
直接将这个特殊的Watcher
挂载在Vue
实例的_watcher
属性上, 所以我们在flushSchedulerQueue
方法中调用callUpdatedHooks
函数中,只有这个watcher
才会执行生命周期函数updated
function callUpdatedHooks (queue) {
var i = queue.length;
while (i--) {
var watcher = queue[i];
var vm = watcher.vm;
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated');
}
}
}
- 第二个参数
expOrFn
, 也就是Watcher
的getter
, 会在实例化Watcher
的时候调用get
方法,然后执行value = this.getter.call(vm, vm);
, 在这里就是会执行updateComponent
方法,这个方法是UI 渲染的一个关键方法,我们在这里暂时不深入分析。 - 第三个参数是
cb
, 传入的是一个空的方法 - 第四个参数传递的是一个
options
对象,在这里传入一个before
的function, 也就是,在UI重新渲染前会执行的一个生命中期函数beforeUpdate
上面我们已经分析了watch
的一个工作过程,下面我们来分析下computed
的工作过程,看其与watch
有什么不一样的地方。
computed
首先在实例化Vue
对象时,也是在initState
方法中,对computed
进行了处理,执行了initComputed
方法, 其代码如下:
function initComputed (vm, computed) {
// $flow-disable-line
var watchers = vm._computedWatchers = Object.create(null);
// computed properties are just getters during SSR
var isSSR = isServerRendering();
for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
if (getter == null) {
warn(
("Getter is missing for computed property \"" + key + "\"."),
vm
);
}
//在此我向大家推荐一个前端全栈开发交流圈:582735936 突破技术瓶颈,提升思维能力
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef);
} else {
if (key in vm.$data) {
warn(("The computed property \"" + key + "\" is already defined in data."), vm);
} else if (vm.$options.props && key in vm.$options.props) {
warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
}
}
}
}
上面代码比较长,但是我们可以总结如下几点:
-
var watchers = vm._computedWatchers = Object.create(null);
在vm
实例对象上面挂载了一个_computedWatchers
的属性,保存了由computed
生成的所有的watcher
- 然后遍历所有的
key
, 每一个key 都生成一个watcher
-
var getter = typeof userDef === 'function' ? userDef : userDef.get;
从这个代码可以延伸computed
的两种写法,如下:
computed: {
// 写法1:直接是一个function
// strLen: function () {
// console.log(this.newTodo.length)
// return this.newTodo.length
// },
// 写法2: 可以是一个对象,但是必须要有get 方法,
// 不过写成对象没有什么意义, 因为其他的属性,都不会使用。
strLen: {
get: function () {
console.log(this.newTodo.length)
return this.newTodo.length
}
}
}
- 如果不是服务端渲染,就生成一个
watcher
对象,并且保存在vm._computedWatchers
属性中,但是这个与watch
生成的watcher
有一个重要的区别就是, 传递了一个属性computedWatcherOptions
对象,这个对象就配置了一个lazy: ture
我们在Watcher
的构造函数中,有如下逻辑:
this.value = this.lazy
? undefined
: this.get();
因为this.lazy
是true
所以不会执行this.get();, 也就不会立即执行computed
里面配置的对应的方法。
-
defineComputed(vm, key, userDef);
就是将computed
的属性,直接挂载在vm
上,可以直接通过vm.strLen
去访问,不过在这个方法中,有针对是不是服务器渲染做了区别,服务器渲染会立即执行computed
的函数,获取值,但是在Web 则不会立即执行,而是给get
赋值一个函数:
function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}
如果我们在我们的template
中引用了computed
的属性,如:<div>{{strLen}}</div>
, 会执行$mount
去渲染模版的时候,会去调用strLen
,然后就会执行上面的computedGetter
的方法去获取值, 执行的就是:
Watcher.prototype.evaluate = function evaluate () {
this.value = this.get();
this.dirty = false;
};
执行了this.get()
就是上面分析watch
中的this.get()
.
思考:
我们上面基本已经分析了computed
逻辑的基本过程,但是我们好像还是没有关联上, 当我们的data
里面的值变了,怎么去通知computed
更新的呢?我们的computed
如下:
computed: {
strLen: function () {
return this.newTodo.length
},
}
当我们改变this.newTodo
的时候,会执行strLen
的方法呢?
答案:
- 在上面我们已经分析了我们在我们的
template
中有引用strLen
,如<div>{{strLen}}</div>
,在执行$mount
去渲染模版的时候,会去调用strLen
,然后就会执行的computedGetter
的方法去获取值,然后调用get
方法,也就是我们computed
配置的函数:
computed: {
strLen: function () {
return this.newTodo.length
}
},
- 在执行上面方法的时候,会引用
this.newTodo
, 就会进入reactiveGetter
方法(深入了解 Vue 响应式原理(数据拦截))
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
}
会将当前的Watcher
对象添加到dep.subs
队列中。
- 当
this.newTodo
值改变时,就会执行reactiveSetter
方法,当执行dep.notify();
时,也就会执行computed
里面的方法,从而达到当data
里面的值改变时,其有引用这个data
属性的computed
也就会立即执行。 - 如果我们定义了
computed
但是没有任何地方去引用这个computed
, 即使对应的data
属性变更了,也不会执行computed
方法的, 即使手动执行computed
方法, 如:app.strLen
也不会生效,因为在Watcher
的addDep
方法,已经判断当前的watcher
不是一个新加入的watcher
了
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};
结语
感谢您的观看,如有不足之处,欢迎批评指正。
获取资料👈👈👈
本次给大家推荐一个免费的学习群,里面概括移动应用网站开发,css,html,webpack,vue node angular以及面试资源等。
对web开发技术感兴趣的同学,欢迎加入Q群:👉👉👉582735936 👈👈👈,不管你是小白还是大牛我都欢迎,还有大牛整理的一套高效率学习路线和教程与您免费分享,同时每天更新视频资料。
最后,祝大家早日学有所成,拿到满意offer,快速升职加薪,走上人生巅峰。
网友评论