Vue0.11版本源码阅读系列七:补充

本文深入解析Vue.js中计算属性的更新机制,watch选项的工作原理,以及自定义组件的渲染流程。计算属性依赖变化时,会触发getter,更新Watcher实例,进而更新相关指令。watch选项最终调用$watch方法,创建Watcher实例监听数据变化。自定义组件渲染涉及模板编译、$addChild方法创建子实例,并插入到DOM中。文章详述了这一系列过程,帮助读者理解Vue.js内部机制。
摘要由CSDN通过智能技术生成

第一篇留了两个问题:

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的构造函数:

image-20210114201622204

然后到模板编译阶段遍历到这个自定义元素会给它添加一个v-component属性:

tag = el.tagName.toLowerCase()
component =
    tag.indexOf('-') > 0 &&
    options.components[tag]
if (component) {
    el.setAttribute(config.prefix + 'component', tag)
}

image-20210115100403284

所以后续也是通过指令来处理这个自定义组件,接下来会生成链接函数,component属于terminal指令的一种:

image-20210115100542982

接下来就回到了正常的指令编译过程了,_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的工作原理也很简单,就是返回之前的实例而不是创建新实例,这样所有的状态都还保存着。

总结

本系列到这里基本就结束了,我相信能看到这里的人不多,因为第一次写这种源码阅读的系列,总的来说有点乱,很多地方重点不是很突出,描述的可能也不是很详细,可能不是很让人看的下去,另外难免也会有错误,欢迎大家指出。

阅读源码是每个开发者都无法绕过去的必经之路,无论是为了提升自己还是为了面试,我们终归是要对自己每时每刻在用的东西有个更深的了解,这样对于使用来说也是有好处的,另外思考和学习别人优秀的编码思维,也能让自己变的更好。

不得不说阅读源码是挺枯燥和无聊的,也是有难度的,很容易让人心生退意,很多地方你不是非常的了解其作用的话是基本看不懂的,当然我们也不必执着于这些地方,也不用把所有地方都看完看懂,更好的方式还是带着问题去阅读,比如说我想搞懂某一个地方原理,那么你就去看这部分的代码就可以了,当你沉浸在里面也是别有一番意思的。

话不多说,白白~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值