Vue2源码

为什么data是函数,而components是对象?

data函数如果没有返回值会报错吗?

答:会

vue源码位置:src\core\instance\state.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// // 看这里!这里有一个判断 isPlainObject(data)
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// ...
// ...
// ...
// observe data
observe(data, true /* asRootData */)
}

Vue源码位置:packages\weex-template-compiler\build.js

1
2
3
function isPlainObject (obj) {
return _toString.call(obj) === '[object Object]'
}

data函数执行之后将返回值重新赋值给data,如果data不是个objectname就会报出一个警告

为什么props定义的数据不能和data同名?

vue源码路径:vue\src\core\instance\state.js

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

// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}

可以看到先获取datakey,然后再去调用hasOwn方法去判断methodsprops里面是否有重复的key,有的话就会发出警告。最后都通过了之后就会调用proxy(vm, '_data', key),用封装的proxy方法对vm实例做了一层代理,使得我们可以直接通过this直接调用key

vue2代理的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
// 去掉_props,vm._props.xxx => vm.xxx,可以直接访问
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
// 代理,
Object.defineProperty(target, key, sharedPropertyDefinition)
}

使用的是Object.defineProperty来实现的

Vue的初始化

Vue源码位置:src\core\instance\index.js

1
2
3
4
5
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

最先执行的是initMixin(),我们看看里面有啥。

Vue源码位置:src\core\instance\init.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//init.js

// 初始化
vm._self = vm
// 生命周期
initLifecycle(vm)
// 事件
initEvents(vm)
// 渲染
initRender(vm)
// 面试题:beforeCreae和created之间做了什么?或者说二者有什么区别?
// 答:初始化injected,初始化state,初始化provide
callHook(vm, 'beforeCreate')
// 初始化inject
initInjections(vm) // resolve injections before data/props
// 初始化state,state里面有data,computed,method,props,watch
initState(vm)
// 初始化provide
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

initState()是从state.js文件里面导入的,我们再去看看里面有什么。

src\core\instance\state.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 初始化props
if (opts.props) initProps(vm, opts.props)
// 初始化方法
if (opts.methods) initMethods(vm, opts.methods)
// 初始化data
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 初始化computed
if (opts.computed) initComputed(vm, opts.computed)
// 初始化watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

可见在beforeCreatecreated之间初始化了inject,props,methods,data,computed,watch

基于Vue2的proxy,自己实现一个代理

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
const noop = {} 

const propertyDefinition = {
enumerable:true,
configurable:true,
get:noop,
set:noop
}

const Proxy = function(target,sourceKey,key){
propertyDefinition.get = function getter() {
return target[sourceKey][key]
}

propertyDefinition.set = function setter (val) {
target[sourceKey][key] = val
}

Object.defineProperty(target,key,propertyDefinition)
}

function Vue(data){
this._data = data
Object.keys(this._data).forEach(key => {
Proxy(this,'_data',key)
})
}

const vueIns = new Vue({a:1,b:2})

vueIns._data.b = 'hhh'
vueIns.a = 2222
console.log(vueIns._data.a)
console.log(vueIns.b)

Vue的实例为什么不能挂载在body或根节点上?挂载了会报错吗?

答:挂载节点会被虚拟dom生成的Dom替换,会报错

Vue源码位置:src\platforms\web\entry-runtime-with-compiler.js

1
2
3
4
5
6
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}

Vue.set方法是如何实现的

在 Vue2中由于Object.defineProperty的局限性,无法原生监听对象新增属性、删除属性或者数组索引操作,所以提供了 Vue.set 方法作为补偿 api来解决这个问题。

Vue.set的核心作用是:

  • 为响应式对象新增属性,并让该属性也具备响应式能力(即被 getter、setter 拦截)
  • 触发该对象的依赖更新(即通知所有依赖该属性的 watcher 执行更新)

实现的核心逻辑

  1. 参数校验与边界处理
    • 接收三个参数:target(目标对象/数组)、key(新增属性名/索引)、value(新增属性值)
    • 如果 target 是 vue 实例或者 $data(根数据对象),直接报错。vue 不允许给实例添加根级响应式属性
    • 如果 target 不是响应式对象(未被 Observer 处理过),直接赋值target[key] = value,然后返回。非响应式数据无需劫持。
  2. 处理数组的情况
    如果 target 是数组且 key 是有效的索引:
    • 计算实际索引(处理负数索引,如 key=-1 对应 target.length-1
    • 使用 splice 方法在指定索引位置新增元素

核心操作: 调用数组的 splice 方法替换元素(target.splice(key,1,val))。因为 Vue 已经重写了数组的 splice 方法,使其执行的时候会触发依赖更新(dep.notify()),同时新插入的 val 会被转为响应式。

  1. 处理对象的情况
    如果 target 是对象:
    • 如果 key 已存在与 target,那么旧直接赋值target[key] = value,会触发 setter,自动更新依赖。
    • 如果 key 是新属性:
      • 获取 target 对象的 Observer 实例(Vue 内部用于管理响应式的对象,每个响应式数据都有一个 Observer 实例)
      • 调用 Observer 实例的 defineReactive 方法,为新属性key 添加 getter/setter,使其具备响应式属性。
      • 触发 target 对象的依赖更新,通过 Observer 实例的 dep.notify() 方法通知所有依赖该对象的 watcher 执行更新。

computed 和 watch 的区别

computed

从现有响应式数据中动态派生新值。

  • 显式依赖响应式数据(data、props),依赖关系是“多对一”,可从多个数据派生出一个新值。
  • 只有当依赖的数据发生变化时,才会重新计算,若依赖未变,直接返回缓存的结果。
  • 触发时机是惰性的,只有当计算结果被模板或者其他响应式数据使用时才会触发计算。

watch

  • 监听响应式数据变化并执行副作用。
  • 可以监听单个或多个响应式数据(data、props、computed),依赖关系是“一对多”或者“多对多”(一个数据变化可以触发多个操作)
  • 触发时机是主动的,当监听的响应式数据发生变化时,会立即执行回调函数。
  • 当监听的数据变化时,都会执行回调函数,不会缓存执行结果。
  • 配置灵活,支持deep: true深度监听,以及immediate: true立即执行回调,handler(回调函数)等选项。

new Vue()过程中做了些什么

new Vue(options)的过程是实例初始化的核心流程,本质是通过一系列初始化步骤将传入的配置(options)转化为一个可响应、可渲染的 Vue 实例。

整个过程可分为选项合并初始化核心模块模板编译与挂载三大阶段。

选项合并

Vue 实例接收的 options(如 data、methods、components 等)并非直接使用,而是需要与全局配置(Vue.config)、组件继承的配置(若为组件实例)进行合并,最终生成一个统一的配置对象。

初始化核心模块

  1. 初始化生命周期
  2. 初始化事件系统($on、$emit、$off等)
  3. 初始化注入(inject),从父组件的 provide 中获取对应的值
  4. 初始化状态,处理 data、props、computed、watch 等核心数据
    1. 初始化 props
    2. 初始化 methods
    3. 初始化 data
    4. 初始化 computed
    5. 初始化 watch
  5. 初始化 provide

模板编译与挂载

  1. 模板编译
    将模板转换为 render 渲染函数
    • 解析:将模板字符串解析为 AST(抽象语法数)
    • 优化:优化 AST,标记 AST 中的静态节点,避免每次更新时重新渲染
    • 生成:将 AST 转换为 render 函数
  2. 挂载
    通过 render 函数生成虚拟 DOM,再将虚拟 DOM 渲染为真实 DOM 并插入到 el 对应的容器中

Vue.observable的了解

在 Vue 2 中,Vue.observable 是一个全局 API,定义在 Vue 构造函数上,源码基于响应式系统的核心逻辑实现。

核心作用

将一个普通对象转换为响应式对象,使其具备 “数据变化时触发依赖更新” 的能力。转换后的对象可以被组件引用,当对象属性变化时,引用它的组件会自动重新渲染。

实现原理

与组件内 data 的响应式处理逻辑一致:通过 Object.defineProperty 对对象的属性递归添加 getter/setter,实现依赖收集(getter 时)和更新触发(setter 时),本质是创建了一个 Observer 实例来管理该对象的响应式。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// store.js
import Vue from 'vue'

// 创建响应式状态对象
export const store = Vue.observable({
count: 0,
user: { name: 'Alice' }
})

// 定义修改状态的方法
export const mutations = {
increment() {
store.count++
},
setName(name) {
store.user.name = name
}
}

Vue2源码
https://zouhualu.github.io/20220222/vue2源码/
作者
花鹿
发布于
2022年2月22日
许可协议