目录结构
├── benchmarks 性能、基准测试
├── dist 构建打包的输出目录
├── examples 案例目录
├── flow flow 语法的类型声明
├── packages 一些额外的包,比如:负责服务端渲染的包 vue-server-renderer、配合 vue-loader 使用的 vue-template-compiler,还有 weex 相关的
│ ├── vue-server-renderer
│ ├── vue-template-compiler
│ ├── weex-template-compiler
│ └── weex-vue-framework
├── scripts 所有的配置文件的存放位置,比如 rollup 的配置文件
├── src vue 源码目录
│ ├── compiler 编译器
│ ├── core 运行时的核心包
│ │ ├── components 全局组件,比如 keep-alive
│ │ ├── config.js 一些默认配置项
│ │ ├── global-api 全局 API,比如熟悉的:Vue.use()、Vue.component() 等
│ │ ├── instance Vue 实例相关的,比如 Vue 构造函数就在这个目录下
│ │ ├── observer 响应式原理
│ │ ├── util 工具方法
│ │ └── vdom 虚拟 DOM 相关,比如熟悉的 patch 算法就在这儿
│ ├── platforms 平台相关的编译器代码
│ │ ├── web
│ │ └── weex
│ ├── server 服务端渲染相关
├── test 测试目录
├── types TS 类型声明
Vue 的初始化过程(new Vue(options))都做了什么?
1.处理组件配置项
初始化根组件时进行了选项合并操作,将全局配置合并到根组件的局部配置上
初始化每个子组件时做了一些性能优化,将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率
2.初始化组件实例的关系属性,比如 $parent、$children、$root、$refs 等
3.处理自定义事件
4.调用 beforeCreate 钩子函数
5.初始化组件的 inject 配置项,得到 ret[key] = val 形式的配置对象,然后对该配置对象进行浅层的响应式处理(只处理了对象第一层数据),并代理每个 key 到 vm 实例上
6.数据响应式,处理 props、methods、data、computed、watch 等选项
7.解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
8.调用 created 钩子函数
9.如果发现配置项上有 el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,就不需要再手动调用 $mount 方法,反之,没提供 el 选项则必须调用 $mount
10.接下来则进入挂载阶段
总结:Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。
Vue 实例挂载的实现
Vue 中我们是通过 $mount 实例方法去挂载 vm 的
$mount 方法实际上会去调用 mountComponent 方法
mountComponent 核心就是先实例化一个渲染 Watcher,在它的回调函数中会调用 updateComponent 方法,在此方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM
Watcher 起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数
最后判断为根节点的时候设置 vm._isMounted 为 true, 表示这个实例已经挂载了,同时执行 mounted 钩子函数
render
Vue 的 _render 方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node
写的比较多的是 template 模板,在 mounted 方法的实现中,会把 template 编译成 render 方法
Virtual DOM
真正的 DOM 元素是非常庞大的,因为浏览器的标准就把 DOM 设计的非常复杂。当我们频繁的去做 DOM 更新,会产生一定的性能问题
而 Virtual DOM 就是用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多
在 Vue.js 中,Virtual DOM 是用 VNode 这么一个 Class 去描述
VNode 是对真实 DOM 的一种抽象描述,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其它属性都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。
由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的
Virtual DOM 除了它的数据结构的定义,映射到真实的 DOM 实际上要经历 VNode 的 create、diff、patch 等过程
Vue.js 利用 createElement 方法创建 VNode
createElement 创建 VNode 的过程,每个 VNode 有 children,children 每个元素也是一个 VNode,这样就形成了一个 VNode Tree,它很好的描述了我们的 DOM Tree。
update
Vue 的 _update 是实例的一个私有方法,它被调用的时机有 2 个,一个是首次渲染,一个是数据更新的时候
从初始化 Vue 到最终渲染的整个过程(配图)
生命周期
beforeCreate & created
beforeCreate 和 created 函数都是在实例化 Vue 的阶段,在 _init 方法中执行的
在这两个钩子函数执行的时候,并没有渲染 DOM,所以我们也不能够访问 DOM,一般来说,如果组件在加载的时候需要和后端有交互,放在这俩个钩子函数执行都可以,如果是需要访问 props、data 等数据的话,就需要使用 created 钩子函数
beforeMount & mounted
beforeMount 钩子函数发生在 mount,也就是 DOM 挂载之前,它的调用时机是在 mountComponent 函数中
在执行 vm._render() 函数渲染 VNode 之前,执行了 beforeMount 钩子函数,在执行完 vm._update() 把 VNode patch 到真实 DOM 后,执行 mounted 钩子
beforeUpdate & updated
beforeUpdate 和 updated 的钩子函数执行时机都应该是在数据更新的时候
beforeUpdate 的执行时机是在渲染 Watcher 的 before 函数中
注意,在组件已经 mounted 之后,才会去调用 beforeUpdate 这个钩子函数
beforeDestroy & destroyed
beforeDestroy 和 destroyed 钩子函数的执行时机在组件销毁的阶段,最终会调用 $destroy 方法
在 $destroy 的执行过程中,它又会执行 vm.__patch__(vm._vnode, null) 触发它子组件的销毁钩子函数,这样一层层的递归调用,所以 destroy 钩子函数执行顺序是先子后父,和 mounted 过程一样
activated & deactivated
activated 和 deactivated 钩子函数是专门为 keep-alive 组件定制的钩子
keep-alive 参考:https://www.jianshu.com/p/952...
响应式
初始化的过程,把原始的数据最终映射到 DOM 中,数据的变更会触发 DOM 的变化
1.数据渲染到页面
2.处理用户交互
原生js做法
监听点击事件,修改数据,手动操作 DOM 重新渲染
vue做法
利用 Object.defineProperty 原理,具体详见:https://developer.mozilla.org...
Object.defineProperty(obj, prop, descriptor)
对于 descriptor 中的 get 和 set,get 是一个给属性提供的 getter 方法,当我们访问了该属性的时候会触发 getter 方法;set 是一个给属性提供的 setter 方法,当我们对该属性做修改的时候会触发 setter 方法
一旦对象拥有了 getter 和 setter,我们可以简单地把这个对象称为响应式对象
proxy
代理的作用是把 props 和 data 上的属性代理到 vm 实例上。定义了 props,可以通过 vm 实例访问到它
let comP = {
props: {
msg: 'hello'
},
methods: {
say() {
console.log(this.msg)
}
}
}
在 say 函数中通过 this.msg 访问到定义在 props 中的 msg,这个过程发生在 proxy 阶段, 实现原理如下:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
observe
observe 方法的作用就是给非 VNode 的对象类型数据添加一个 Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer 对象实例
Observer
Observer 是一个类,它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
// 对 value 做判断,对于数组会调用 observeArray 方法,否则对纯对象调用 walk 方法
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// defineReactive 的功能就是定义一个响应式对象,给对象动态添加 getter 和 setter
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array) {
for (let i = 0, l = items.length; i < l; i++) {
// observe 的功能就是用来监测数据的变化, 详细代码看源码
observe(items[i])
}
}
/**
* Define a property.
*/
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
响应式原理总结:核心就是利用 Object.defineProperty 给数据添加了 getter 和 setter,目的就是为了在我们访问数据以及写数据的时候能自动执行一些逻辑:getter 做的事情是依赖收集,setter 做的事情是派发更新