vue2.x核心源码深入浅出,我还是去看源码了

平常的工作就是以vue2.x进行开发 , 因为我是个实用主义者,以前我就一直觉得,你既然选择了这个框架开发你首先就要先弄懂这玩意怎么用,也就是先熟悉vue语法和各种api,而不是去纠结实现它的原理是什么 。甚至我可以这么说,你没有看过源码,只通过官方文档也能用这个框架解决绝大部分业务需要,解决大部分bug,而且大部分情况下,别人是不会管你知不知道原理的 。但我不是说阅读源码不好,至少在解决另一小部分bug的时候会让你少走很多弯路,知道为什么会导致这样的bug,还有一点,至少在面试的时候还是很有用的,手动狗头 。
先放上vue2.x版本官方文档:https://v2.cn.vuejs.org/v2/guide/instance.html,然后gayhub上的vue2.x源码地址:https://github.com/vuejs/vue/tree/v2.7.10,由于vue2.x还在迭代更新中,目前最新tag是v2.7.10,所以我们这次分析此分支下的代码 。本次分析的代码主要在src/core下,建议谷歌浏览器安装Octo tree插件,一款在线以树形格式展示github项目代码结构的插件(如图左侧) , 效果真的很棒 。

vue2.x核心源码深入浅出,我还是去看源码了

文章插图
1. 实例挂载
大家都会在入口文件main.js写上
let app = new Vue({el: '#app',data: {message: 'Hello Vue!'}})其实这块代码套用官方的话呢,就是通过vue函数,给你创建一个vue实例 。关于vue这个对象,源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/instance/index.ts#L9
function Vue(options) {if (__DEV__ && !(this instanceof Vue)) {warn('Vue is a constructor and should be called with the `new` keyword')}this._init(options)}是不是感觉特别短 , 它只是说明了vue这个函数 , 必须要通过new关键字来进行初始化,而且,重头戏在this._init(options)这行代码里,这里调用的_init方法源码是定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/instance/init.ts#L16中的initMixin() , 但重点是下面这些代码(L38~L66):
// merge options 合并配置if (options && options._isComponent) {// optimize internal component instantiation// since dynamic options merging is pretty slow, and none of the// internal component options needs special treatment.initInternalComponent(vm, options as any)} else {vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor as any),options || {},vm)}/* istanbul ignore else */if (__DEV__) {initProxy(vm) // 初始化代理属性} else {vm._renderProxy = vm}// expose real selfvm._self = vminitLifecycle(vm) // 初始化生命周期initEvents(vm) // 初始化事件中心initRender(vm) // 初始化渲染callHook(vm, 'beforeCreate', undefined, false /* setContext */) // 初始化beforeCreate钩子initInjections(vm) // resolve injections before data/propsinitState(vm) // 初始化props、methods、data、computed、watchinitProvide(vm) // resolve provide after data/propscallHook(vm, 'created') // 初始化created钩子2.双向绑定 
实现vue双向绑定的3个核心类:observe类,dep类和watcher类,在src/core/observer文件夹下,分别对应index.ts文件、dep.ts文件、watcher.ts文件,首先,我们先看index.ts中对observe类的定义,源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/index.ts#L49
export class Observer {dep: DepvmCount: number // number of vms that have this object as root $dataconstructor(public value: any, public shallow = false, public mock = false) {// this.value = https://www.huyubaike.com/biancheng/valuethis.dep = mock ? mockDep : new Dep()this.vmCount = 0def(value,'__ob__', this)if (isArray(value)) {if (!mock) {if (hasProto) {/* eslint-disable no-proto */;(value as any).__proto__ = arrayMethods/* eslint-enable no-proto */} else {for (let i = 0, l = arrayKeys.length; i < l; i++) {const key = arrayKeys[i]def(value, key, arrayMethods[key])}}}if (!shallow) {this.observeArray(value)}} else {/*** Walk through all properties and convert them into* getter/setters. This method should only be called when* value type is Object.*/const keys = Object.keys(value)for (let i = 0; i < keys.length; i++) {const key = keys[i]defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock)}}}observe类会在vue实例被创建的时候,去遍历data里的每一个属性 , 先调用Array.isArray()判断是不是数组 。第一个考点就来了,vue是怎么监控数组的?可以看到,如果属性是数组,就会直接将arrayMethods直接赋值给监控数组的_proto_上以达到重写数组方法的目的,所以实际上我们调用的这几个数组方法已经是经过mutator()重写过了的(所以官方称这些为数组变更方法) , 在这里重写数组方法的好处是只对想要监控的数组生效,不用担心会污染到全局的Array方法 。还有一点,虽然现在的浏览器基本都支持这种非标准属性(_proto_)的写法,因为这种写法本身就是早期浏览器自身厂商对原型属性规范的实现,但是为了以防有些浏览器不支持,源码这里还是对浏览器做了兼容 , 如果不支持,就将这些变异方法一个个绑定到监控的数组上 。

推荐阅读