学习总结:
1、 Vue 首次渲染的过程。
- 在首次渲染之前,首先进行Vue初始化,初始化实例成员和静态成员
- 当初始化结束之后,要调用Vue的构造函数
new Vue()
,在构造函数中调用了_init()
方法,这个方法相当于我们整个Vue的入口 - 在
_init
方法中,最终调用了$mount
,一共有两个$mount
,第一个定义在entry-runtime-with-compiler.js
文件中,也就是我们的入口文件$mount
,这个$mount()
的核心作用是帮我们把模板编译成render
函数,但它首先会判断一下当前是否传入了render
选项,如果没有传入的话,它会去获取我们的template
选项,如果template
选项也没有的话,他会把el
中的内容作为我们的模板,然后把模板编译成render
函数,它是通过compileToFunctions()
函数,帮我们把模板编译成render
函数的,当把render
函数编译好之后,它会把render
函数存在我们的options.render
中。 - 接着会调用
src/platforms/web/runtime/index.js
文件中的$mount
方法,在这个中首先会重新获取el
,因为如果是运行时版本的话,是不会走entry-runtime-with-compiler.js
这个入口中获取el,所以如果是运行时版本的话,我们会在runtime/index.js的$mount()中重新获取el。 - 接下来调用
mountComponent()
,这个方法在src/core/instance/lifecycle.js
中定义的,在mountComponent()
中,首先会判断render
选项,如果没有render
选项,但是我们传入了模板,并且当前是开发环境的话会发送一个警告,目的是如果我们当前使用运行时版本的Vue,而且我们没有传入render,但是传入了模版,告诉我们运行时版本不支持编译器。接下来会触发beforeMount这个生命周期中的钩子函数,也就是开始挂载之前。 - 然后定义了updateComponent(),在这个函数中,调用
vm._render
和vm._update
,vm._render
的作用是生成虚拟DOM,vm._update
的作用是将虚拟DOM
转换成真实DOM
,并且挂载到页面上 - 创建
Watcher
对象,在创建Watcher
时,传递了updateComponent
这个函数,这个函数最终是在Watcher
内部调用的。在Watcher
内部会用了get
方法,当Watcher创建完成之后,会触发生命周期中的mounted
钩子函数,在get方法中,会调用updateComponent() - 挂载结束,最终返回Vue实例。
2、Vue 响应式原理。
Vue响应式的重点就是数据劫持/发布订阅模式,其他更多介绍查看Vue的官网的 深入响应式原理一节,这里主要总结一下内部代码的实现。
- 先找找入口在哪里(src/core/instance/init.js)
initState(vm) --> initData(vm) --> observe()
//初始化Vue实例的状态,初始化了_props,methods,_data,computed,watch
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm) // 看这里
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
// 把 data 属性属性注入到Vue实例上
function initData(vm) {
let data = vm.$options.data
// 初始化 _data, 组件中 data 是函数,调用函数返回结果,否则直接返回data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
//...
// 响应式处理
observe(data, true /* asRootData */)
}
- 现在终于到正题了 observe(value) 响应式的入口
export function observe(alue: any, asRootData: ?boolean) {
// 判断 value 是否是对象
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 如果 value 有 __ob__(observer对象) 属性 结束(说明已经做过响应化处理)
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 创建一个 Observer 对象
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob // 返回observer对象
}
- 数据劫持
export class Observer {
value: any; // 观察对象
dep: Dep; // 依赖对象
vmCount: number; // 实例计数器
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0 // 初始化实例的 vmCount 为0
def(value, '__ob__', this) // 将实例挂载到观察者对象的 __ob__ 属性
// 数组的响应式处理
if (Array.isArray(value)) {
...
// 为数组中的每一个对象创建一个 observer 实例
this.observeArray(value)
} else {
// 遍历对象中的每一个属性,转换成 setter/getter
this.walk(value)
}
}
walk (obj: Object) {
// 获取观察对象的每一属性
const keys = Object.keys(obj)
// 遍历每一个属性,设置为响应式数据
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// Vue重写了数组 push,popt,splice等有副作用的方法,当这些方法被调用的时候,会发送通知;
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
// 为对象定义一个响应式的属性
exprot function defineReactive (obj, key, val, ...) {
// 创建依赖对象实例
const dep = new Dep()
// 获取 obj 的属性描述符对象
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 提供预定义的存取器函数
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 判断是否递归观察子对象,并将子对象属性都转成 getter/setter, 返回观察对象
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 收集依赖,为每个属性收集依赖,如果这个属性的值是对象,也要为这个子对象收集依赖
// 如果预定义的 getter 存在则 value 等于 getter 调用的返回值
// 否则直接赋值属性值
const value = getter ? getter.call(obj) : val
// 如果存在当前依赖目标,即 watcher 对象,则建立依赖
if (Dep.target) {
dep.depend()
// 如果子观察目标存在,建立子对象的依赖关系
if (childOb){
childOb.dep.depend()
// 如果属性是数组,则特殊处理收集数组对象依赖
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value // 返回属性值
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val // 同上
// 如果新值等于旧值或者新值旧值为NaN则不执行
if (newVal === val || (newVal !== newVal && value !== value)) {
return
}
if (getter && !setter) return // 如果没有 setter 直接返回
// 如果预定义setter存在则调用,否则直接更新新值
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 如果新值是对象,观察子对象并返回 子的 observer 对象
childOb = !shallow && observe(newVal)
// 派发更新(发布更改通知)
dep.notify()
}
})
}
4.依赖收集
- 首先调用Watcher对象的get方法,在get方法中调用pushTarget,把当前的watcher记录到Dep.target属性中
- 访问data中成员的时候收集依赖,当访问这个属性值的时候会触发defineReactive的getter,在getter中收集依赖
- 把属性对应的watcher对象添加到dep的subs数组中,也就是为属性收集依赖
- 如果这个属性的值也是对象,要创建childOb,给子对象收集依赖,目的是子对象添加和删除成员时发送通知
// src/core/observer/dep.js
export default class Dep {
static target: ?Watcher; // 静态属性,watcher 对象
id: number; // dep 实例 Id
subs: Array<Watcher>; // dep 实例对应的 watcher 对象/订阅者数组
constructor () {
this.id = uid++
this.subs = []
}
// 添加新的订阅者 watcher 对象
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移除订阅者
removeSub (sub: Watcher) {
remove(this.subs, sub)
// this.subs.splice(this.subs.indexOf(sub), 1)
}
// 将观察对象和 watcher 建立依赖
depend () {
if (Dep.target) {
// 如果 target 存在,把 dep 对象添加到 watcher 的依赖中
Dep.target.addDep(this)
}
}
// 发送通知
notify () {
const subs = this.subs.slice()
...
// 调用每个订阅者的update方法实现更新
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
- Watcher(观察者)
- 当数据发生变化的时候,会调用dep.notify()发送通知,再调用wacher对象的update()方法
- 在update()方法中会去调用queueWatcher()判断wacher是否被处理,如果没有的话添加到queue队列中,并调用flushSchedulerQueue()
3、虚拟 DOM 中 Key 的作用和好处。
key 的特殊属性主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试修复/再利用相同类型元素的算法。使用 key,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
4、Vue 中模板编译的过程。
- 缓存公共的 mount 函数,并重写浏览器平台的 mount
- 判断是否传入了 render 函数,没有的话,是否传入了 template ,没有的话,则获取 el 节点的 outerHTML 作为 template
- 调用 baseCompile 函数
- 解析器(parse) 将模板字符串的模板编译转换成 AST 抽象语法树
- 优化器(optimize) - 对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化
- 通过 generate 将 AST 抽象语法树转换为 render 函数的 js 字符串
- 将 render 函数 通过 createFunction 函数 转换为 一个可以执行的函数
- 将 最后的 render 函数 挂载到 option 中
- 执行 公共的 mount 函数
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。