目标:
- 如何阅读VUE源码
- 弄清楚模板和数据如何渲染成最终的 DOM
- 深入响应式原理
如何阅读VUE源码
vue源码的目录结构
scripts # 构建配置相关
src
├── compiler # 编译相关 把模板解析成 ast 语法树,ast 语法树优化,代码生成等
├── core # 核心代码内置组件、全局 API 封装,Vue 实例化、观察者、虚拟 DOM等
├── platforms # 不同平台的支持
├── server # 服务端渲染
├── sfc # .vue 文件解析
├── shared # 共享代码
Vue.js 的源码利用了 Flow 做了静态类型检查 TypeScript vs Flow - 掘金
Vue.js 源码是基于 Rollup 构建的, 它的构建相关配置都在 scripts 目录下
Runtime Only VS Runtime + Compiler
当我们的代码执行 import Vue from 'vue' 的时候,就是从src/platforms/web/entry-runtime-with-compiler.js 这个入口来初始化Vue的
Vue本质上就是一个用 Function 实现的 Class,然后在它的原型 prototype 以及它本身上扩展了一系列的方法和属性
// 用 Function 实现的类,我们只能通过 new Vue 去实例化它
// src/core/instance/index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
// xxxMixin 的函数调用,并把 Vue 当参数传入,它们的功能都是给 Vue 的 prototype 上扩展一些方法
// Vue 按功能把这些扩展分散到多个模块中去实现,而不是在一个模块里实现所有
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
生命周期
模板渲染
New Vue 发生了什么
export function initMixin(Vue: Class < Component > ) {
// 合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等
Vue.prototype._init = function(options ? : Object) {
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
// 把 Vue 构造函数的 options 和用户传入的 options 做一层合并,到 vm.$options 上
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 把不同的功能逻辑拆成一些单独的函数执行,让主线逻辑一目了然
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化 props、data、methods、watch、computed 等属性
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// 组件初始化的时候是不传 el 的,因此组件是自己接管了 $mount 的过程
if (vm.$options.el) {
// $mount 这个方法的实现是和平台、构建方式都相关的
vm.$mount(vm.$options.el)
}
}
}
Vue 实例挂载的实现
Virtual DOM
- Vue.js 中 Virtual DOM 是借鉴了一个开源库 snabbdom 的实现,然后加入了一些 Vue.js 特色的东西
- 核心定义就几个关键属性,标签名、数据、子节点、键值等
- 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法
$mount 这个方法的实现是和平台、构建方式都相关的
export function mountComponent(
vm: Component,
el: ? Element,
hydrating ? : boolean ): Component {
// ...
// 执行beforeMount钩子函数
// 因为先执行 在调用child,所以先父后子
callHook(vm, 'beforeMount')
let updateComponent
updateComponent = () => {
// vm._render 渲染出来vnode
// 调用 vm._update 更新 DOM 一次真实的渲染
vm._update(vm._render(), hydrating)
}
// 在组件 mount 的过程中,会实例化一个渲染的 Watcher 去监听 vm 上的数据变化重新渲染
// 实例化的过程中,在它的构造函数里会判断 isRenderWatcher,接着把当前 watcher 的实例赋值给 vm._watcher
// 定义在 src/core/observer/watcher.js 中 观察者模式
new Watcher(vm, updateComponent, noop, {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */ )
// vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例,是我们通过外部 new Vue 初始化过程
if (vm.$vnode == null) {
vm._isMounted = true
// _update调用完 才会执行到这里 所以先子后父
callHook(vm, 'mounted')
}
return vm }
createElement
createElement创建 VNode 的过程
- 每个 VNode 有
children,children每个元素也是一个 VNode,这样就形成了一个 VNode Tree
如果是组件会调用createComponent
export function createComponent(
Ctor: Class < Component > | Function | Object | void,
data: ? VNodeData,
context : Component,
children: ? Array < VNode > ,
tag ? : string ): VNode | Array < VNode > | void {
if (isUndef(Ctor)) {
return
}
// baseCtor 实际上就是 Vue
const baseCtor = context.$options._base
// 构造子类构造函数
// Vue.extend 函数的定义 : src/core/global-api/extend.js
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// install component management hooks onto the placeholder node
// 安装组件的钩子
installComponentHooks(data)
// 通过 new VNode 实例化一个 vnode 并返回 注意组件的vnode没有children
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context, {
Ctor,
propsData,
listeners,
tag,
children
},
asyncFactory
)
return vnode
}
// 构造组件构造函数
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
// 组件上挂载的钩子函数
init(vnode: VNodeWithData, hydrating: boolean): ? boolean {
const child = vnode.componentInstance =
createComponentInstanceForVnode(
vnode,
activeInstance
)
// hydrating 为 true 一般是服务端渲染的情况,我们只考虑客户端渲染,所以这里 $mount 相当于执行 child.$mount(undefined, false)
// 它最终会调用 mountComponent 方法,进而执行 vm._render() 方法
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
export function createComponentInstanceForVnode(vnode: any, parent: any): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode, // 占位节点
parent
}
// 子组件的实例化实际上就是在这个时机执行的
// 执行实例的 _init 方法
return new vnode.componentOptions.Ctor(options)
}
update
Vue 的 _update 是实例的一个私有方法,它的作用是把 VNode 渲染成真实的 DOM,它被调用的时机有 2 个,一个是首次渲染,一个是数据更新的时候
渲染的时候会先判断vnode上是否有init钩子 如果有就会调用该钩子,否则使用封装的原生操作dom的方法渲染为真实dom
举个例子:
import Vue from 'vue'
import App from './App.vue'
new Vue({
el: "#app",
render: h => h(App),
})
// new Vue({
// el: '#app',
// render(createElement) {
// return createElement('div', {
// attrs: {
// id: '#app2'
// }
// }, [this.message, createElement('h1', {
// attrs: {
// id: 'app3'
// }
// }, this.test)])
// },
// data: {
// message: 'Hello Vue2!',
// test: '商业化技术'
// }
// })
// initial render
// hydrating 表示是否是服务端渲染;
// removeOnly 是给 transition-group 用的
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
总结
响应式原理
所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据
initData:
// 定义一个响应式对象,给对象动态添加 getter 和 setter
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter ? : ? Function,
shallow ? : boolean ) {
// 实例化一个 Dep 的实例 dep是getter依赖收集的核心 它的定义在 src/core/observer/dep.js 中
const dep = new Dep()
val = obj[key]
// 对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,
// 这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter 和 setter。
let childOb = !shallow && observe(val)
// 给 obj 的属性 key 添加 getter 和 setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val if (Dep.target) {
// 依赖收集
// 当前watcher会收集所依赖的dep
// 数据的dep也会收集变更时需要通知的watcher
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 // 如果 shallow 为 false 的情况,会对新设置的值变成一个响应式对象
childOb = !shallow && observe(newVal)
// 通知所有的订阅者
dep.notify()
}
})
}
dep
export default class Dep {
static target: ? Watcher; // 全局唯一 Watcher
id: number;
subs: Array < Watcher > ; // 订阅这个数据变化的Watcher 的数组
constructor() {
this.id = uid++
this.subs = []
}
addSub(sub: Watcher) {
this.subs.push(sub)
}
removeSub(sub: Watcher) {
remove(this.subs, sub)
}
depend() {
if (Dep.target) {
// addDep 在watcher中定义
Dep.target.addDep(this)
}
}
notify() {
const subs = this.subs.slice()
// 通知所有的订阅者
// 遍历所有的 subs,也就是 Watcher 的实例数组,然后调用每一个 watcher 的 update 方法
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
watcher
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options ? : ? Object,
isRenderWatcher ? : boolean ) {
this.getter = expOrFn this.value = this.lazy ?
undefined // 计算属性走这里
:
this.get()
}
// 和依赖收集相关的原型方法
get() {
// 把 this 赋值为当前的渲 染 watcher 并压栈(为了恢复用)
pushTarget(this)
const vm = this.vm
let value = this.getter.call(vm, vm)
// 递归去访问 value,触发它所有子项的 getter
if (this.deep) {
traverse(value)
}
popTarget()
// 依赖清空(将newdeps赋值给deps,清空newdeps,其实清空的是没有用的依赖)
this.cleanupDeps()
return value
}
// 和依赖收集相关的原型方法
addDep(dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) { // 保证同一数据不会被添加多次
// 执行 dep.addSub(this),那么就会执行 this.subs.push(sub)
// 把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备
dep.addSub(this)
}
}
}
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
// 同步watcher
this.run()
} else {
queueWatcher(this)
}
}
run() {
// 这就是当我们去修改组件相关的响应式数据的时候,会触发组件重新渲染的原因,接着就会重新执行 patch 的过程,但它和首次渲染有所不同
const value = this.get()
this.cb.call(this.vm, value, oldValue)
}
evaluate() {
// 真正访问到computed的getter
this.value = this.get()
this.dirty = false
}
depend() {
// deps中是computed watcher 所依赖的响应式数据
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
initComputed:
const computedWatcherOptions = { lazy: true }
function initComputed(vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
// 对 computed 对象做遍历,拿到计算属性的每一个 userDef,然后尝试获取这个 userDef 对应的 getter 函数
for (const key in computed) {
const userDef = computed[key]
const getter = userDef
// 为每一个 getter 创建一个 watcher,这个 watcher 和渲染 watcher 有一点很大的不同,它是一个 computed watcher
// 因为 const computedWatcherOptions = { lazy: true }
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
defineComputed(vm, key, userDef)
}
}
export function defineComputed(
target: any,
key: string,
userDef: Object | Function ) {
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = noop
Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 将计算watcher放在数据的dep.subs中
if (watcher.dirty) {
watcher.evaluate()
}
// 将渲染watcher放在数据的dep.subs中
// 因为computer没有自己的dep,更新的时候不能通知渲染watcher更新
// 需要依赖的数据通知渲染watcher
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
这块的逻辑比较绕,简单来说就是:
computed依赖的响应式数据收集了computed watcher和渲染watcher
computed依赖的响应式数据变更时会通知这两个watcher更新,computed watcher会直接返回,渲染watcher会执行,然后触发computed的get,开始计算computed的值