共计 4878 个字符,预计需要花费 13 分钟才能阅读完成。
第一篇留了两个问题:
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
的工作原理也很简略,就是返回之前的实例而不是创立新实例,这样所有的状态都还保留着。
总结
本系列到这里根本就完结了,我置信能看到这里的人不多,因为第一次写这种源码浏览的系列,总的来说有点乱,很多中央重点不是很突出,形容的可能也不是很具体,可能不是很让人看的上来,另外不免也会有谬误,欢送大家指出。
浏览源码是每个开发者都无奈绕过去的必经之路,无论是为了晋升本人还是为了面试,咱们终归是要对本人每时每刻在用的货色有个更深的理解,这样对于应用来说也是有益处的,另外思考和学习他人优良的编码思维,也能让本人变的更好。
不得不说浏览源码是挺干燥和无聊的,也是有难度的,很容易让人心生退意,很多中央你不是十分的理解其作用的话是根本看不懂的,当然咱们也不用执着于这些中央,也不必把所有中央都看完看懂,更好的形式还是带着问题去浏览,比如说我想搞懂某一个中央原理,那么你就去看这部分的代码就能够了,当你沉迷在外面也是别有一番意思的。
话不多说,白白~