第一篇留了两个问题:
1.计算属性依赖的属性变动了是如何触发计算属性更新的
2.watch
选项或$watch
办法的原理是怎么的
本篇来剖析一下这两个问题,另外简略看一下自定义元素是怎么渲染的。
计算属性
<p v-text="showMessage + '我是不重要的字符串'"></p>
{
data: {
message: 'Hello Vue.js!'
},
computed: {
showMessage() {
return this.message.toUpperCase()
}
}
}
以这个简略的例子来说,首先计算属性也是会挂载到vue
实例上成为实例的一个属性:
for (var key in computed) {
var userDef = computed[key]
var def = {
enumerable: true,
configurable: true
}
if (typeof userDef === 'function') {
def.get = _.bind(userDef, this)
def.set = noop
} else {
def.get = userDef.get
? _.bind(userDef.get, this)
: noop
def.set = userDef.set
? _.bind(userDef.set, this)
: noop
}
Object.defineProperty(this, key, def)
}
通过this.xxx
拜访计算属性时会调用咱们定义的computed
选项外面的函数。
其次在模板编译指令解析的阶段计算属性和一般属性并没有区别,这个v-text
指令会创立一个Directive
实例,这个Directive
实例初始化时会以showMessage + '我是不重要的字符串'
为惟一的标记创立一个Watcher
实例,v-text
指令的update
办法会被这个Watcher
实例所收集,增加到它的cbs
数组里,Watcher
实例化时会把本身赋值给Observer.target
,随后对showMessage + '我是不重要的字符串'
这个表达式求值,也就会调用到计算属性的函数showMessage()
,这个函数调用后会援用所依赖的所有属性,这里也就是message
,这会触发message
的getter
,这样这个Watcher
实例就被增加到message
的依赖收集对象dep
里了,后续当message
的值变动触发其setter
后会遍历其dep
里收集的Watcher
实例,触发Watcher
的update
办法,最初会遍历cbs
里增加的指令的update
办法,这样这个依赖计算属性的指令就失去了更新。
值得注意的是在这个版本里,计算属性是没有缓存的,即便所依赖的值没有变动,反复援用计算属性的值也会从新执行咱们定义的计算属性函数。
侦听器
watch
选项申明的侦听器最初调用的也是$watch
办法,在第一篇曾经晓得了$watch
办法里次要就是创立了一个Watcher
实例:
// exp就是咱们要侦听的数据,如:a、a.b
exports.$watch = function (exp, cb, deep, immediate) {
var vm = this
var key = deep ? exp + '**deep**' : exp
var watcher = vm._userWatchers[key]
var wrappedCb = function (val, oldVal) {
cb.call(vm, val, oldVal)
}
if (!watcher) {
watcher = vm._userWatchers[key] =
new Watcher(vm, exp, wrappedCb, {
deep: deep,
user: true
})
} else {
watcher.addCb(wrappedCb)
}
}
对于Watcher
咱们当初曾经很相熟了,实例化的时候会把本人赋值给Observer.target
,而后触发表达式的求值,也就是咱们要侦听的属性,触发其gettter
而后把该Watcher
收集到它的依赖收集对象dep
里,只有被收集就好办了,后续属性值变动后就会触发这个Watcher
的更新,也就会触发下面的回调。
自定义组件的渲染
<my-component></my-component>
new Vue({
el: '#app',
components: {
'my-component': {
template: '<div>{{msg}}</div>',
data() {
return {
msg: 'hello world!'
}
}
}
}
})
在第一篇里咱们提到了每个组件选项最初都会被创立成一个继承了vue
的构造函数:
而后到模板编译阶段遍历到这个自定义元素会给它增加一个v-component
属性:
tag = el.tagName.toLowerCase()
component =
tag.indexOf('-') > 0 &&
options.components[tag]
if (component) {
el.setAttribute(config.prefix + 'component', tag)
}
所以后续也是通过指令来解决这个自定义组件,接下来会生成链接函数,component
属于terminal
指令的一种:
接下来就回到了失常的指令编译过程了,_bindDir
办法会给v-component
指令创立一个Directive
实例,而后会调用component
指令的bind
办法:
{
bind: function () {
// el就是咱们的自定义元素my-component
if (!this.el.__vue__) {
// 创立一个正文元素替换掉该自定义元素
this.ref = document.createComment('v-component')
_.replace(this.el, this.ref)
// 查看是否存在keep-alive选项
this.keepAlive = this._checkParam('keep-alive') != null
// 查看是否存在ref来援用该组件
this.refID = _.attr(this.el, 'ref')
if (this.keepAlive) {
this.cache = {}
}
// 解析构造函数,也就是返回初始化时选项合并阶段生成的构造函数,expression这里是指令值my-component
this.resolveCtor(this.expression)
// 创立子实例
var child = this.build()
// 插入该子实例
child.$before(this.ref)
// 设置ref
this.setCurrent(child)
}
}
}
看build
办法:
{
build: function () {
// 如果有缓存间接返回
if (this.keepAlive) {
var cached = this.cache[this.ctorId]
if (cached) {
return cached
}
}
var vm = this.vm
if (this.Ctor) {
var child = vm.$addChild({
el: this.el,
_asComponent: true,
_host: this._host
}, this.Ctor)// Ctor就是该组件的构造函数
if (this.keepAlive) {
this.cache[this.ctorId] = child
}
return child
}
}
}
这个办法用来创立子实例,调用了$addChild
办法,简化后如下:
exports.$addChild = function (opts, BaseCtor) {
var parent = this
// 父实例就是上述咱们new Vue的实例
opts._parent = parent
// 根组件也就是父实例的根组件
opts._root = parent.$root
// 创立一个该自定义组件的实例
var child = new BaseCtor(opts)
return child
}
下面两个办法次要就是创立了一个该组件构造函数的实例,因为组件构造函数继承了vue
,所以之前的new Vue
时做的初始化工作同样也都会走一遍,什么察看数据、遍历该自定义组件及其所有子元素进行模板编译绑定指令等等,因为咱们传递了template
选项,所以在第一篇里一带而过的办法_compile
里在调用compile
办法之前会先对这个进行解决:
// 这里会把template模板字符串转成dom,原理很简略,创立一个文档片段,再创立一个div,之后再把模板字符串设为div的innserHTML,最初再把div里的元素都增加到文档片段里即可
el = transclude(el, options)
// 编译并链接其余的
compile(el, options)(this, el)
最初如果存在keep-alive
则把该实例缓存一下,回到bind
办法里的child.$before(this.ref)
:
exports.$before = function (target, cb, withTransition) {
return insert(
this, target, cb, withTransition,
before, transition.before
)
}
function insert (vm, target, cb, withTransition, op1, op2) {
// 获取指标元素,这里就是bind办法里创立的正文元素
target = query(target)
// 元素以后不在文档中
var targetIsDetached = !_.inDoc(target)
// 判断是否要应用过渡形式插入,如果元素不在文档中则会应用带过渡的形式插入
var op = withTransition === false || targetIsDetached
? op1
: op2
// 如果指标元素以后曾经插入文档以及该该组件没有挂载过就须要触发attached生命周期
var shouldCallHook =
!targetIsDetached &&
!vm._isAttached &&
!_.inDoc(vm.$el)
// 插入文档
op(vm.$el, target, vm, cb)
if (shouldCallHook) {
vm._callHook('attached')
}
return vm
}
op
办法会调用transition.before
办法把元素插入到文档中,对于过渡插入的详细分析请参考vue0.11版本源码浏览系列六:过渡原理。
到这里组件就曾经渲染实现了,bind
办法里最初调用了setCurrent
:
{
setCurrent: function (child) {
this.childVM = child
var refID = child._refID || this.refID
if (refID) {
this.vm.$[refID] = child
}
}
}
如果咱们设置了援用比方:<my-component v-ref="myComponent"></my-component>
,那么就能够通过this.$.myComponent
拜访到该子组件。
keep-alive
的工作原理也很简略,就是返回之前的实例而不是创立新实例,这样所有的状态都还保留着。
总结
本系列到这里根本就完结了,我置信能看到这里的人不多,因为第一次写这种源码浏览的系列,总的来说有点乱,很多中央重点不是很突出,形容的可能也不是很具体,可能不是很让人看的上来,另外不免也会有谬误,欢送大家指出。
浏览源码是每个开发者都无奈绕过去的必经之路,无论是为了晋升本人还是为了面试,咱们终归是要对本人每时每刻在用的货色有个更深的理解,这样对于应用来说也是有益处的,另外思考和学习他人优良的编码思维,也能让本人变的更好。
不得不说浏览源码是挺干燥和无聊的,也是有难度的,很容易让人心生退意,很多中央你不是十分的理解其作用的话是根本看不懂的,当然咱们也不用执着于这些中央,也不必把所有中央都看完看懂,更好的形式还是带着问题去浏览,比如说我想搞懂某一个中央原理,那么你就去看这部分的代码就能够了,当你沉迷在外面也是别有一番意思的。
话不多说,白白~
发表回复