前言
这段时间我司需求没那么急了,开始闲下来了。之前也一直想学习Vue2源码,打开vue2源码后,摸不着头脑,不知从何开始看起。后面看到鲨鱼大佬文章,便开始跟着敲跟着学习,同时感谢这些大佬手写源码,并对其一步步的分析。总的来说跟着敲完这些代码,获益匪浅。
1.Vue实例数据初始化
Vue 其实就是一个构造函数,通过initMixin把_init方法挂载在Vue原型上,供Vue实例调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const { initMixin } = require('./init')
function Vue(options) { this._init(options) }
initMixin(Vue)
let vue = new Vue({ props: {}, data() { return { a: 1, b: [1], c: { d: 1 } } }, watch: {}, render: () => {} })
|
其中
在Vue中,initMixin
函数是一个用于混入初始化逻辑的函数。它主要的作用是给Vue构造函数添加实例初始化的方法,以便在创建Vue实例时执行一些初始化操作。
具体来说,initMixin
函数会向Vue构造函数的原型对象上混入一个名为_init
的方法。这个_init
方法是Vue实例初始化的入口,它会在创建Vue实例时被调用。
_init
方法的主要职责是:
- 合并配置:将用户传入的配置选项与Vue构造函数的默认选项进行合并,得到最终的配置对象。
- 初始化生命周期:创建Vue实例的生命周期钩子,并为
$options
添加一些属性用于存储生命周期状态。 - 初始化事件:初始化Vue实例的事件系统,包括事件的订阅与派发。
- 初始化渲染:创建Vue实例的渲染函数和虚拟DOM的相关属性。
- 初始化状态:设置Vue实例的响应式数据,并创建数据观测的实例。
- 调用
$mount
:如果配置中存在el
选项,会自动调用$mount
方法进行挂载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const { initState } = require('./state')
function initMixin(Vue) { Vue.prototype._init = function (options) { const vm = this
vm.$options = options
initState(vm) } } module.exports = { initMixin: initMixin }
|
initState的具体处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
export function initState(vm) { const opts = vm.$options; if (opts.props) { initProps(vm); } if (opts.methods) { initMethod(vm); } if (opts.data) { initData(vm); } if (opts.computed) { initComputed(vm); } if (opts.watch) { initWatch(vm); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import { observe } from "./observer/index.js";
function initData(vm) { let data = vm.$options.data; data = vm._data = typeof data === "function" ? data.call(vm) : data || {};
for (let key in data) { proxy(vm, `_data`, key); } observe(data); }
function proxy(object, sourceKey, key) { Object.defineProperty(object, key, { get() { return object[sourceKey][key]; }, set(newValue) { object[sourceKey][key] = newValue; }, }); }
|
2.响应式处理
可以看到在初始化过程中涉及一个方法 observe(data);这个就是响应式数据核心,也就是大名鼎鼎的数据劫持结合观察者的模式实现vue响应式数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| import { arrayMethods } from "./array";
class Observer { constructor(value) { Object.defineProperty(value, '__ob__', { value: this, enumerable: false, writable: true, configurable: true })
if(Array.isArray(value)) { value.__proto__ = arrayMethods this.observeArray(value) } else { this.walk(value) } }
walk(data) { let keys = Object.keys(data) for(let i = 0; i < keys.length; i++) { const key = keys[i] const value = data[key] defineReactive(data, key, value) } }
observeArray(items) { for(let i = 0; i < items.length; i++) { observe(items[i]) } } }
function defineReactive(data, key, value) { observe(value)
Object.defineProperty(data, key, { get() { console.log('获取值') return value }, set(newVal) { if (newVal === value) return console.log('设置值') value = newVal } }) }
function observe(value) { if (Object.prototype.toString.call(value) === '[object Object]' || Array.isArray(value)) { return new Observer(value) } }
module.exports = { observe: observe }
|
数据劫持核心是 defineReactive 函数,主要使用 Object.defineProperty 来对数据 get 和 set 进行劫持 这里就解决了之前的问题为啥数据变动了会自动更新视图,因为我们可以在 set 里面去通知视图更新
3.响应式处理的注意点
可以看到vue源码中数组和对象是分开处理的,数组并没有和对象一样层层递归,给每一个属性都劫持set和get
对象
的属性通常比较少,对每一个属性都劫持set和get
,并不会消耗很多性能
数组
有可能有成千上万个元素,如果每一个元素都劫持set和get
,无疑消耗太多性能了
所以对象
通过defineProperty
进行正常的劫持set和get
4.对数组的观测
上面的arrayMethods就是响应式对数组的处理, 数组
是通过修改数组原型上的部分方法
,来实现修改数组触发响应式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto); let methodsToPatch = [ "push", "pop", "shift", "unshift", "splice", "reverse", "sort", ]; methodsToPatch.forEach((method) => { arrayMethods[method] = function (...args) { const result = arrayProto[method].apply(this, args); const ob = this.__ob__;
let inserted; switch (method) { case "push": case "unshift": inserted = args; break; case "splice": inserted = args.slice(2); default: break; } if (inserted) ob.observeArray(inserted); return result; }; });
|
4.思维导图
参考文章
鲨鱼哥-手写 Vue2.0 源码(一)-响应式数据原理