从Vue2.x源码中看到的知识点

构造函数

我们使用vue时,会使用

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})
 

这里生成了一个Vue实例,显然new关键字后面的Vue是一个ES6的class或者是构造函数。查看源码看到是一个构造函数。

// new Vue会生产出一个对象,这个对象是真正在内存中工作的Vue实例对象。
function Vue (options) {
    // 在用户new Vue时,执行的_init就是下面initMixin添加的方法
    this._init(options) 
}

// 使用构造函数而不使用class语法的好处是:
// 通过 xxxMixin 的函数调用,将Vue的功能拆分到多个模块去实现。
// 它们的功能都是给 Vue 的 prototype 上扩展一些方法。
// 这种方式是用 Class 难以实现的。
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue
 

在Vue源码中,是使用了ES6语法的,但这里面没有用class来声明,是因为如果写成class声明的类,类会很大,而Vue包含内容多,需要做拆分,所以还是使用构造函数语法,采用了在不同的模块中给Vue构造函数添加功能的方式。

选项对象、Vue构造器、Vue实例对象

在Vue官方文档中,可以看到这样一句话。

image.png
在日常开发,写一个组件时,如下:

<template>
...
</template>

<script>
// 这里跟React定义组件不同,不是写继承Component的类,而是写一个简单对象,这只是个选项对象
export default {
    ...
}
</script>
 

首先要清晰的是,我们日常开发中编写的,只是选项对象。这点是区别于React的直接定义React类(或者说是React组件的构造函数)。

我们注意根组件传入的选项对象,data属性是一个对象。而除了根组件,我们编写的选项对象,data属性是一个方法。之所以有这样的区别,是因为根组件是new Vue直接创建Vue实例对象,其他我们编写的选项对象,在代码运行时,先被Vue.extend扩展成继承自Vue构造器的子Vue构造器,再用子Vue构造器实例化出组件实例(即真正工作的vue组件对象)。所以我们编写的选项对象的data属性是被所有组件实例公用的,所以data属性需要是一个方法,为不同的组件实例返回专属的data数据。

总之,区分和清晰选项对象、Vue构造器、Vue实例对象的概念,有助于我们更好的理解Vue运行原理。从上面的分析可以看出作者拥有着很好的面向对象编程思想。

闭包

Vue源码中也些闭包的应用。

缓存变量

// 在讲普通属性变成响应式属性时,dep变量存在于Object.defineProperty方法下的get/set函数的作用域链上,使get/set方法能共享dep对象
export function defineReactive (
    const dep = new Dep()
    Object.defineProperty(obj, key, {
        get: function reactiveGetter () {
            ...
            // 依赖收集
            dep.depend()
        }
        set: function reactiveSetter (newVal) {
            ...
            // 派发更新
            dep.notify()
        }
    }
}
 

因为有闭包,defineReactive方法执行结束后,dep对象并不会被垃圾回收,而是存在于get/set方法的作用域链上。

函数柯里化

用于把 VNode 最终转换成 DOM的patch方法,由于patch方法是平台相关的,在 Web 和 Weex 环境中的DOM和相关操作不同。所以将平台不同的地方抽象出来作为参数,传入一个叫createPatchFunction方法用来生成针对不同平台的patch方法。

// nodeOps, modules是跟平台相关的逻辑,传递给createPatchFunction方法,返回值是patch方法,patch方法内部可以使用nodeOps, modules来进行一些真实dom的操作。
export const patch: Function = createPatchFunction({ nodeOps, modules })
 

柯里化便是闭包的一个应用,这里利用柯里化,实现了patch方法的多态。

这样在VNode 转换成真正的 DOM 节点的过程中,会调用patch方法,其中的只需考虑diff新旧VNode树的逻辑,不需考虑具体操作真实dom的逻辑。

生命周期钩子函数的this指向

new Vue({
  data: {
    a: 1
  },
  created: function () {
    // `this` 指向 vm 实例
    console.log('a is: ' + this.a)
  }
})
// => "a is: 1"
 

this的指向也是我们初学Vue略感蹊跷的地方。这个this怎么就指向data中数据了呢?

因为我们编写的是选项对象,后被组装给了组件的构造函数。用构造函数实例化组件得到vm实例。在vm实例调用生命周期钩子函数时

// 执行时会用call方法,将this指向vm实例
handlers[i].call(vm)
 

接下来的问题是,vm实例是怎样访问到data的。

Vue先将data变成响应式对象放在vm的_data属性上。再通过代理把每一个值 vm._data.xxx 都代理到 vm.xxx 上。这里的代理是也是通过Object.defineProperty实现的

// 原理如下代码,比如是data里的a属性。
// 用户在写this.a 相当于访问vm.a 也就是调用了下面代码的get:this._data.a
Object.defineProperty(vm, 'a', {
    get: function proxyGetter () {
        return this._data.a
    },
    set: function proxySetter (val) {
        this._data.a = val
    }
})
 

生命周期

beforeCreate:

在此勾子中无法获得props、data 中定义的值,也不能调用 methods 中定义的函数。

在vue-router源码中有这样的代码:

// 我们之所以在写代码时可以this.$router。下面的代码逻辑起了作用。
Vue.mixin({
    beforeCreate () {
        this._routerRoot = this
        this._router = this.$options.router
    }
})

Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
})
 

为vm实例挂一些编写代码时要用的的变量对象,beforeCreate是很合适的。

同理,在Vuex源码中,利用beforeCreate为vm实例添加了$store变量。

created:

如果是需要访问 props、data 等数据的话,就需要使用 created 钩子函数。

在我们写的应用的组件树中,created的执行顺序是从父到子,顺次执行。

mounted:

在真实dom挂载之后,执行mounted。

在我们写的应用的组件树中,不同于created,mounted的执行顺序是从子到父,顺次执行。

destroy:

组件的销毁钩子函数。

在我们写的应用的组件树中,同于mounted,destroy的执行顺序是从子到父,顺次执行。

activated & deactivated

activated 和 deactivated 钩子函数是专门为 keep-alive 组件定制的钩子。

nextTick的细节

nextTick在Web中默认使用的是微任务Promise实现,如果不支持菜使用宏任务(JS事件循环知识点)

研究diff算法时的困惑

之前学习vue diff算法的时候,让我困惑的点在于在比较新旧两个dom树,如果节点是一个组件类型的VNode,会怎么样?

首先要清晰的点是,大部分介绍diff算法的文章,是不考虑组件类型的VNode节点的。所以在看这些文章的时候,只需要将所有节点看成是可以映射成真实dom节点的VNode节点,去考虑其讲解的算法。

这样有利于更专注的研究diff算法本身的逻辑,Vue的diff算法是基于Snabbdom库的,也就是一个没有组件节点的diff算法。

有组件类型Vnode节点时的diff逻辑

在新旧两个VNode树中没有组件类型的VNode节点时,patch是只负责比对两树的不同进而操作真实DOM。但VNode树中有组件类型的的VNode节点时,patch方法的职责变多,如生产组件类型的的VNode节点的子树。

假设一种场景,diff出新树比旧树多了一个Vnode节点,只需在真实dom树上,新增一个有此Vnode映射出的真实dom即可。但当新旧两个VNode树中包含组件类型的VNode节点时,diff出新树比旧树多了一个组件类型的Vnode节点。组件类型的Vnode不能直接映射成真实dom,而是会形成一个子VNode树。这个形成子树的过程跟形成父树一样,产生了一个递归的效果。

所以一个页面的Vnode树,应该是一个大树,里面还嵌套了许多小树,它们的diff逻辑有些情况是互不干扰的,除了在大树中要对小树所在的那个Vnode节点进行新增或删除。

由上面假设的场景可以看出,要捋清整体的vue diff逻辑,还是要花些功夫的,具体的逻辑还是要研究下源码。

算法

在研究diff算法的逻辑时,也能看到到作者的一些算法思想。

首先就是虚拟dom树使用的是树结构。diff的时候对树进行的遍历,虽然对比是逐层对比,但是遍历是用的DFS(深度优先遍历)。

在Vue3.x中,在比对两个Vnode节点的Children变化时,还使用了最长递归子序列算法(leecode上的题目,可以使用贪心+二分法解题),用以保证最少的移动真实dom。

(0)
上一篇 2021年5月26日 下午6:07
下一篇 2021年5月26日 下午6:23

相关推荐

发表评论

登录后才能评论