关于javascript:Vue011版本源码阅读系列七补充

第一篇留了两个问题:

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,这会触发messagegetter,这样这个Watcher实例就被增加到message的依赖收集对象dep里了,后续当message的值变动触发其setter后会遍历其dep里收集的Watcher实例,触发Watcherupdate办法,最初会遍历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的工作原理也很简略,就是返回之前的实例而不是创立新实例,这样所有的状态都还保留着。

总结

本系列到这里根本就完结了,我置信能看到这里的人不多,因为第一次写这种源码浏览的系列,总的来说有点乱,很多中央重点不是很突出,形容的可能也不是很具体,可能不是很让人看的上来,另外不免也会有谬误,欢送大家指出。

浏览源码是每个开发者都无奈绕过去的必经之路,无论是为了晋升本人还是为了面试,咱们终归是要对本人每时每刻在用的货色有个更深的理解,这样对于应用来说也是有益处的,另外思考和学习他人优良的编码思维,也能让本人变的更好。

不得不说浏览源码是挺干燥和无聊的,也是有难度的,很容易让人心生退意,很多中央你不是十分的理解其作用的话是根本看不懂的,当然咱们也不用执着于这些中央,也不必把所有中央都看完看懂,更好的形式还是带着问题去浏览,比如说我想搞懂某一个中央原理,那么你就去看这部分的代码就能够了,当你沉迷在外面也是别有一番意思的。

话不多说,白白~

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理