Vue.js 框架源码与进阶
一、手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法
1.vue-router的实现原理
1.1 路由规则中的props属性,可以设置动态路由的入参
路由规则中设置如下:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
...
{
path: '/c/:id',
// 动态路由通过设置props属性,可以在子组件中通过props属性获取
props: true,
name: 'IndexC',
component: () => import('../pages/c.vue')
},
]
const router = new VueRouter({
routes
})
export default router
子组件中获取动态路由的id:
<template>
<div class="wrapper">c</div>
</template>
<script>
export default {
components: {},
// 获取动态路由的id
props: ['id'],
data() {
return {
};
},
created() {},
mounted() {},
methods: {},
computed: {},
watch: {},
};
</script>
推荐使用此方法传递动态路由的id,可以避免去依赖$route。
1.2 嵌套路由的使用
import Vue from 'vue'
import VueRouter from 'vue-router'
import Index from '../pages/a.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Index',
component: Index,
// children属性设置嵌套路由
children: [
{
path: '',
name: 'IndexB',
component: () => import('../pages/b.vue')
},
{
path: 'c/:id',
// 动态路由通过设置props属性,可以在子组件中通过props属性获取
props: true,
name: 'IndexC',
component: () => import('../pages/c.vue')
},
]
}
...
]
const router = new VueRouter({
routes
})
export default router
1.3 编程式导航
this.$router.push('/')
this.$router.replace('/')
this.$router.go(-1)
this.$router.back()
...
1.4 hash模式和history模式的区别
- hash模式是基于锚点,以及onhashchange事件
- history模式是基于HTML5中的History API
- history.pushState() IE 10 以后的才支持
- history.replaceState()
- history模式的使用:
- 需要服务端配置,否则刷新时出现404
二、Vue源码学习
Vue源码的获取
-
项目地址:github.com/vuejs/vue
-
Fork 一份到自己仓库,克隆到本地,可以自己写注释提交到 github
源码目录结构
src
├─compiler 编译相关
├─core Vue 核心库
├─platforms 平台相关代码
├─server SSR,服务端渲染
├─sfc .vue 文件编译为 js 对象
└─shared 公共的代码
了解 Flow
-
官网:flflow.org/
-
JavaScript 的静态类型检查器
-
Flow 的静态类型检查错误是通过静态类型推断实现的
- 文件开头通过
// @flow
或者/* @flow */
声明
- 文件开头通过
/* @flow */
function square(n: number): number {
return n * n;
}
square("2"); // Error!
调试设置
打包
-
打包工具 Rollup
- Vue.js 源码的打包工具使用的是 Rollup,比 Webpack 轻量
- Webpack 把所有文件当做模块,Rollup 只处理 js 文件更适合在 Vue.js 这样的库中使用
- Rollup 打包不会生成冗余的代码
-
安装依赖
npm i
-
设置 sourcemap
- package.json 文件中的 dev 脚本中添加参数 --sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web- full-dev"
-
执行 dev
- npm run dev 执行打包,用的是 rollup,-w 参数是监听文件的变化,文件变化自动重新打包
- 结果:

调试
-
examples 的示例中引入的 vue.min.js 改为 vue.js
-
打开 Chrome 的调试工具中的 source
Vue 的不同构建版本
-
npm run build 重新打包所有文件
-
官方文档 - 对不同构建版本的解释
-
dist\README.md
UMD | CommonJS | ES Module | |
---|---|---|---|
Full | vue.js | vue.common.js | vue.esm.js |
Runtime-only | vue.runtime.js | vue.runtime.common.js | vue.runtime.esm.js |
Full (production) | vue.min.js | ||
Runtime-only (production) | vue.runtime.min.js |
术语
-
完整版:同时包含编译器和运行时的版本。
-
编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码,体积大、效率低。
-
运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码,体积小、效率高。基本上就是除去编译器的代码。
-
UMD:UMD 版本通用的模块版本,支持多种模块方式。 vue.js 默认文件就是运行时 + 编译器的UMD 版本
-
CommonJS**(cjs)**:CommonJS 版本用来配合老的打包工具比如 Browserify 或 webpack 1。
-
ES Module:从 2.6 开始 Vue 会提供两个 ES Modules (ESM) 构建文件,为现代打包工具提供的版本。
-
ESM 格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行“tree-shaking”并将用不到的代码排除出最终的包。
-
ES6 模块与 CommonJS 模块的差异
-
Runtime + Compiler vs. Runtime-only
// Compiler
// 需要编译器,把 template 转换成 render 函数
// const vm = new Vue({
// el: '#app',
// template: '<h1>{{ msg }}</h1>',
// data: {
// msg: 'Hello Vue'
// }
// })
// Runtime
// 不需要编译器
const vm = new Vue({
el: '#app',
render (h) {
return h('h1', this.msg)
},
data: {
msg: 'Hello Vue'
}
})
-
推荐使用运行时版本,因为运行时版本相比完整版体积要小大约 30%
-
基于 Vue-CLI 创建的项目默认使用的是 vue.runtime.esm.js
- 通过查看 webpack 的配置文件
vue inspect > output.js
- 注意: *.vue 文件中的模板是在构建时预编译的,最终打包后的结果不需要编译器,只需要运行时版本即可
寻找入口文件
- 查看 dist/vue.js 的构建过程
执行构建
npm run dev
# "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
# --environment TARGET:web-full-dev 设置环境变量 TARGET
- script/config.js 的执行过程
- 作用:生成 rollup 构建的配置文件
- 使用环境变量 TARGET = web-full-dev
// 判断环境变量是否有 TARGET
// 如果有的话 使用 genConfig() 生成 rollup 配置文件
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
// 否则获取全部配置
exports.getBuild = genConfig exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
- genConfifig(name)
- 根据环境变量 TARGET 获取配置信息
- builds[name] 获取生成配置的信息
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
- resolve()
- 获取入口和出口文件的绝对路径
const aliases = require('./alias')
const resolve = p => {
// 根据路径中的前半部分去alias中找别名
const base = p.split('/')[0]
if (aliases[base]) {
return path.resolve(aliases[base], p.slice(base.length + 1))
} else {
return path.resolve(__dirname, '../', p)
}
}
结果
-
把 src/platforms/web/entry-runtime-with-compiler.js 构建成 dist/vue.js,如果设置 --sourcemap 会生成 vue.js.map
-
src/platform 文件夹下是 Vue 可以构建成不同平台下使用的库,目前有 weex 和 web,还有服务器端渲染的库
从入口开始
-
src/platform/web/entry-runtime-with-compiler.js
-
阅读源码记录
-
el 不能是 body 或者 html 标签
-
如果没有 render,把 template 转换成 render 函数
-
如果有 render 方法,直接调用 mount 挂载 DOM
-
// 1. el 不能是 body 或者 html
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
}
const options = this.$options if (!options.render) {
// 2. 把 template/el 转换成 render 函数
……
}
// 3. 调用 mount 方法,挂载 DOM
return mount.call(this, el, hydrating)
- 调试代码
- 调试的方法
const vm = new Vue({
el: '#app',
template: '<h3>Hello template</h3>',
render (h) {
return h('h4', 'Hello render')
}
})

Vue 的构造函数在哪里
-
src/platform/web/entry-runtime-with-compiler.js 中引用了 './runtime/index'
-
src/platform/web/runtime/index.js
-
设置 Vue.confifig
-
设置平台相关的指令和组件
-
指令 v-model、v-show
-
组件 transition、transition-group
-
-
设置平台相关的 patch 方法(打补丁方法,对比新旧的 VNode)
-
设置 $mount 方法,挂载 DOM
-
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
-
src/platform/web/runtime/index.js 中引用了 'core/index'
-
src/core/index.js
- 定义了 Vue 的静态方法
- initGlobalAPI(Vue)
-
src/core/index.js 中引用了 './instance/index'
-
src/core/instance/index.js
- 定义了 Vue 的构造函数
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')
}
// 调用 _init() 方法
this._init(options) }
// 注册 vm 的 _init() 方法,初始化
vm initMixin(Vue)
// 注册 vm 的 $data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相关方法
// $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入 render
// $nextTick/_render
renderMixin(Vue)
四个导出 Vue 的模块
- src/platforms/web/entry-runtime-with-compiler.js
- web 平台相关的入口
- 重写了平台相关的 $mount() 方法
- 注册了 Vue.compile() 方法,传递一个 HTML 字符串返回 render 函数
- src/platforms/web/runtime/index.js
- web 平台相关
- 注册和平台相关的全局指令:v-model、v-show
- 注册和平台相关的全局组件: v-transition、v-transition-group
- 全局方法:
- patch:把虚拟 DOM 转换成真实 DOM
- $mount:挂载方法
- src/core/index.js
- 与平台无关
- 设置了 Vue 的静态方法,initGlobalAPI(Vue)
- src/core/instance/index.js
- 与平台无关
- 定义了构造函数,调用了 this._init(options) 方法
- 给 Vue 中混入了常用的实例成员
Vue 的初始化
src/core/global-api/index.js
-
初始化 Vue 的静态方法
// 注册 Vue 的静态属性/方法 initGlobalAPI(Vue) // src/core/global-api/index.js // 初始化 Vue.config 对象 Object.defineProperty(Vue, 'config', configDef) // exposed util methods. // NOTE: these are not considered part of the public API - avoid relying on // them unless you are aware of the risk. // 这些工具方法不视作全局API的一部分,除非你已经意识到某些风险,否则不要去依赖他们 Vue.util = { warn, extend, mergeOptions, defineReactive } // 静态方法 set/delete/nextTick Vue.set = set Vue.delete = del Vue.nextTick = nextTick // 2.6 explicit observable API // 让一个对象可响应 Vue.observable = <T>(obj: T): T => { observe(obj) return obj } // 初始化 Vue.options 对象,并给其扩展 // components/directives/filters/_base Vue.options = Object.create(null) ASSET_TYPES.forEach(type => { Vue.options[type + 's'] = Object.create(null) }) // this is used to identify the "base" constructor to extend all plain- object // components with in Weex's multi-instance scenarios. Vue.options._base = Vue // 设置 keep-alive 组件 extend(Vue.options.components, builtInComponents) // 注册 Vue.use() 用来注册插件 initUse(Vue) // 注册 Vue.mixin() 实现混入 initMixin(Vue) // 注册 Vue.extend() 基于传入的 options 返回一个组件的构造函数 initExtend(Vue) // 注册 Vue.directive()、 Vue.component()、Vue.filter() initAssetRegisters(Vue)
src/core/instance/index.js
- 定义 Vue 的构造函数
- 初始化 Vue 的实例成员
// 此处不用 class 的原因是因为方便,后续给 Vue 实例混入实例成员
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)
}
// 注册 vm 的 _init() 方法,初始化
vm initMixin(Vue)
// 注册 vm 的 $data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相关方法
// $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入 render // $nextTick/_render
renderMixin(Vue)
- initMixin(Vue)
- 初始化 _init() 方法
// src\core\instance\init.js
export function initMixin (Vue: Class<Component>) {
// 给 Vue 实例增加 _init() 方法
// 合并 options / 初始化操作
Vue.prototype._init = function (options?: Object) {
// a flag to avoid this being observed
// 如果是 Vue 实例不需要被 observe
vm._isVue = true
// merge options
// 合并 options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm )
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// vm 的生命周期相关变量初始化
// $children/$parent/$root/$refs initLifecycle(vm)
// vm 的事件监听初始化, 父组件绑定在当前组件上的事件
initEvents(vm)
// vm 的编译render初始化
// $slots/$scopedSlots/_c/$createElement/$attrs/$listeners
initRender(vm)
// beforeCreate 生命钩子的回调
callHook(vm, 'beforeCreate')
// 把 inject 的成员注入到 vm 上
initInjections(vm)
// resolve injections before data/props
// 初始化状态 vm 的 _props/methods/_data/computed/watch
initState(vm)
// 初始化 provide
initProvide(vm)
// resolve provide after data/props
// created 生命钩子的回调
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 如果没有提供 el,调用 $mount() 挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
首次渲染过程
-
Vue 初始化完毕,开始真正的执行
-
调用 new Vue() 之前,已经初始化完毕
-
通过调试代码,记录首次渲染过程

数据响应式原理
通过查看源码解决下面问题
-
vm.msg = { count: 0 } ,重新给属性赋值,是否是响应式的?
-
vm.arr[0] = 4 ,给数组元素赋值,视图是否会更新
-
vm.arr.length = 0 ,修改数组的 length,视图是否会更新
-
vm.arr.push(4) ,视图是否会更新
响应式处理的入口
整个响应式处理的过程是比较复杂的,下面我们先从
-
src\core\instance\init.js
-
initState(vm) vm 状态的初始化
-
初始化了 _data、_props、methods 等
-
-
src\core\instance\state.js
// 数据的初始化
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
- initData(vm) vm 数据的初始化
function initData (vm: Component) {
let data = vm.$options.data
// 初始化 _data,组件中 data 是函数,调用函数返回结果
// 否则直接返回 data
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
……
// proxy data on instance
// 获取 data 中的所有属性
const keys = Object.keys(data)
// 获取 props / methods
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
// 判断 data 上的成员是否和 props/methods 重名
……
// observe data
// 数据的响应式处理
observe(data, true /* asRootData */) }
- src\core\observer\index.js
- observe(value, asRootData)
- 负责为每一个 Object 类型的 value 创建一个 observer 实例
export function observe (
value: any,
asRootData: ?boolean
): Observer | void {
// 判断 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
- src\core\observer\index.js
- 对对象做响应化处理
- 对数组做响应化处理
export class Observer {
// 观测对象
value: any;
// 依赖对象
dep: Dep;
// 实例计数器
vmCount: number;
// number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
// 初始化实例的 vmCount 为0
this.vmCount = 0
// 将实例挂载到观测对象的 __ob__ 属性,设置为不可枚举
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 数组的响应式处理
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 为数组中的每一个对象创建一个 observer 实例
this.observeArray(value)
} else {
// 对象的响应化处理
// 遍历对象中的每一个属性,转换成 setter/getter
this.walk(value)
}
}
/**
* Walk through all properties 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(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
- walk(obj)
- 遍历 obj 的所有属性,为每一个属性调用 defifineReactive() 方法,设置 getter/setter
defifineReactive()
-
src\core\observer\index.js
-
defifineReactive(obj, key, val, customSetter, shallow)
- 为一个对象定义一个响应式的属性,每一个属性对应一个 dep 对象
- 如果该属性的值是对象,继续调用 observe
- 如果给属性赋新值,继续调用 observe
- 如果数据更新发送通知
对象响应式处理
// 为一个对象定义一个响应式的属性
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean ) {
// 1. 为每一个属性,创建依赖对象实例
const dep = new Dep()
// 获取 obj 的属性描述符对象
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 提供预定义的存取器函数
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 2. 判断是否递归观察子对象,并将子对象属性都转换成 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() 添加相互的依赖
// 1个组件对应一个 watcher 对象
// 1个watcher会对应多个dep(要观察的属性很多)
// 我们可以手动创建多个 watcher 监听1个属性的变化,1个dep可以对应多个watcher
dep.depend()
// 如果子观察目标存在,建立子对象的依赖关系,将来 Vue.set() 会用到
if (childOb) {
childOb.dep.depend()
// 如果属性是数组,则特殊处理收集数组对象依赖
if (Array.isArray(value)) {
dependArray(value)
}
}
}
// 返回属性值
return value
},
set: function reactiveSetter (newVal) {
// 如果预定义的 getter 存在则 value 等于getter 调用的返回值
// 否则直接赋予属性值
const value = getter ? getter.call(obj) : val
// 如果新值等于旧值或者新值旧值为null则不执行
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// 如果没有 setter 直接返回
// #7981: for accessor properties without setter
if (getter && !setter) return
// 如果预定义setter存在则调用,否则直接更新新值
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 3. 如果新值是对象,观察子对象并返回 子的 observer 对象
childOb = !shallow && observe(newVal)
// 4. 发布更改通知
dep.notify()
}
})
}
数组的响应式处理
- Observer 的构造函数中
// 数组的响应式处理
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 为数组中的每一个对象创建一个 observer 实例
this.observeArray(value)
} else {
// 编译对象中的每一个属性,转换成 setter/getter
this.walk(value)
}
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
/* istanbul ignore next */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
- 处理数组修改数据的方法
- src\core\observer\array.js
const arrayProto = Array.prototype
// 克隆数组的原型
export const arrayMethods = Object.create(arrayProto)
// 修改数组元素的方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
// 保存数组原方法
const original = arrayProto[method]
// 调用 Object.defineProperty() 重新定义修改数组的方法
def(arrayMethods, method, function mutator (...args) {
// 执行数组的原始方法
const result = original.apply(this, args)
// 获取数组对象的 ob 对象
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 对插入的新元素,重新遍历数组元素设置为响应式数据
if (inserted) ob.observeArray(inserted)
// notify change
// 调用了修改数组的方法,调用数组的ob对象发送通知
ob.dep.notify()
return result
})
})
Dep 类
-
src\core\observer\dep.js
-
依赖对象
-
记录 watcher 对象
-
depend() -- watcher 记录对应的 dep
-
发布通知
1. 在 defineReactive() 的 getter 中创建 dep 对象,并判断 Dep.target 是否有值(一会 再来看有什么 时候有值得), 调用 dep.depend()
2. dep.depend() 内部调用 Dep.target.addDep(this),也就是 watcher 的 addDep() 方 法,它内部最 调用 dep.addSub(this),把 watcher 对象,添加到 dep.subs.push(watcher) 中,也 就是把订阅者 添加到 dep 的 subs 数组中,当数据变化的时候调用 watcher 对象的 update() 方法
3. 什么时候设置的 Dep.target? 通过简单的案例调试观察。调用 mountComponent() 方法的时 候,创建了 渲染 watcher 对象,执行 watcher 中的 get() 方法
4. get() 方法内部调用 pushTarget(this),把当前 Dep.target = watcher,同时把当前 watcher 入栈, 因为有父子组件嵌套的时候先把父组件对应的 watcher 入栈,再去处理子组件的 watcher,子 组件的处理完毕 后,再把父组件对应的 watcher 出栈,继续操作
5. Dep.target 用来存放目前正在使用的watcher。全局唯一,并且一次也只能有一个 watcher 被使用 1234567891011
// dep 是个可观察对象,可以有多个指令订阅它
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
// 静态属性,watcher 对象
static target: ?Watcher;
// dep 实例 Id
id: number;
// dep 实例对应的 watcher 对象/订阅者数组
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
// 添加新的订阅者 watcher 对象
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移除订阅者
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 将观察对象和 watcher 建立依赖
depend () {
if (Dep.target) {
// 如果 target 存在,把 dep 对象添加到 watcher 的依赖中
Dep.target.addDep(this)
}
}
// 发布通知
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
// 调用每个订阅者的update方法实现更新
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// Dep.target 用来存放目前正在使用的watcher
// 全局唯一,并且一次也只能有一个watcher被使用
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
// 入栈并将当前 watcher 赋值给Dep.target
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
// 出栈操作
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
Watcher 类
-
Watcher 分为三种,Computed Watcher、用户 Watcher (侦听器)、渲染 Watcher
-
渲染 Watcher 的创建时机
- /src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el ……callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
……
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 创建渲染 Watcher,expOrFn 为 updateComponent
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, { before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
} }, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true callHook(vm, 'mounted')
}
return vm
}
-
渲染 wacher 创建的位置 lifecycle.js 的 mountComponent 函数中
-
Wacher 的构造函数初始化,处理 expOrFn (渲染 watcher 和侦听器处理不同)
-
调用 this.get() ,它里面调用 pushTarget() 然后 this.getter.call(vm, vm) (对于渲染 wacher 调 用 updateComponent),如果是用户 wacher 会获取属性的值(触发get操作)
-
当数据更新的时候,dep 中调用 notify() 方法,notify() 中调用 wacher 的 update() 方法
-
update() 中调用 queueWatcher()
-
queueWatcher() 是一个核心方法,去除重复操作,调用 flflushSchedulerQueue() 刷新队列并执行watcher
-
flflushSchedulerQueue() 中对 wacher 排序,遍历所有 wacher ,如果有 before,触发生命周期的钩子函数 beforeUpdate,执行 wacher.run(),它内部调用 this.get(),然后调用 this.cb() (渲染wacher 的 cb 是 noop)
-
整个流程结束
实例方法/数据
vm.$set
- 功能
- 向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于 向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性 (比如 this.myObject.newProperty = 'hi')
**注意:**对象不能是 Vue 实例,或者 Vue 实例的根数据对象。
- 示例
vm.$set(obj,'foo','test')
定义位置
- Vue.set()
- global-api/index.js
// 静态方法 set/delete/nextTick Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
- vm.$set()
- instance/index.js
// 注册 vm 的 $data/$props/$set/$delete/$watch
// instance/state.js
stateMixin(Vue)
// instance/state.js
Vue.prototype.$set = set
Vue.prototype.$delete = del
源码
- set() 方法
- observer/index.js
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*/
export function set (target: Array<any> | Object, key: any, val: any): any
{
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
){
warn(`Cannot set reactive property on undefined, null, or primitive
value: ${(target: any)}`)
}
// 判断 target 是否是对象,key 是否是合法的索引
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
// 通过 splice 对key位置的元素进行替换
// splice 在 array.js进行了响应化的处理 target.splice(key, 1, val)
return val
}
// 如果 key 在对象中已经存在直接赋值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 获取 target 中的 observer 对象
const ob = (target: any).__ob__
// 如果 target 是 vue 实例或者$data 直接返回
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data
'+
'at runtime - declare it upfront in the data option.'
)
return val
}
// 如果 ob 不存在,target 不是响应式对象直接赋值
if (!ob) {
target[key] = val
return val
}
// 把 key 设置为响应式属性
defineReactive(ob.value, key, val)
// 发送通知
ob.dep.notify()
return val
}
vm.$delete
- 功能
- 删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制,但是你应该很少会使用它。
**注意:**目标对象不能是一个 Vue 实例或 Vue 实例的根数据对象。
- 示例
vm.$delete(vm.obj,'msg')
定义位置
- Vue.delete()
- global-api/index.js
// 静态方法 set/delete/nextTick
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
- vm.$delete()
- instance/index.js
// 注册 vm 的 $data/$props/$set/$delete/$watch
stateMixin(Vue)
// instance/state.js
Vue.prototype.$set = set
Vue.prototype.$delete = del
源码
- src\core\observer\index.js
/**
* Delete a property and trigger change if necessary.
*/
export function del (target: Array<any> | Object, key: any) {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target)) ){
warn(`Cannot delete reactive property on undefined, null, or primitive
value: ${(target: any)}`)
}
// 判断是否是数组,以及 key 是否合法
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 如果是数组通过 splice 删除
// splice 做过响应式处理
target.splice(key, 1)
return
}
// 获取 target 的 ob 对象
const ob = (target: any).__ob__
// target 如果是 Vue 实例或者 $data 对象,直接返回
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
// 如果 target 对象没有 key 属性直接返回 if (!hasOwn(target, key)) {
return
}
// 删除属性
delete target[key]
if (!ob) {
return
}
// 通过 ob 发送通知
ob.dep.notify()
}
vm.$watch
vm.$watch( expOrFn, callback, [options] )
-
功能
- 观察 Vue 实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。表达式只 接受监督的键路径。对于更复杂的表达式,用一个函数取代。
-
参数
-
expOrFn:要监视的 $data 中的属性,可以是表达式或函数
-
callback:数据变化后执行的函数
- 函数:回调函数
- 对象:具有 handler 属性(字符串或者函数),如果该属性为字符串则 methods 中相应 的定义
-
options:可选的选项
- deep:布尔类型,深度监听
- immediate:布尔类型,是否立即执行一次回调函数
-
-
示例
const vm = new Vue({ el: '#app', data: { a: '1', b: '2', msg: 'Hello Vue', user: { firstName: '诸葛', lastName: '亮' } } }) // expOrFn 是表达式 vm.$watch('msg', function (newVal, oldVal) { console.log(newVal, oldVal) }) vm.$watch('user.firstName', function (newVal, oldVal) { console.log(newVal) }) // expOrFn 是函数 vm.$watch(function () { return this.a + this.b }, function (newVal, oldVal) { console.log(newVal) }) // deep 是 true,消耗性能 vm.$watch('user', function (newVal, oldVal) { // 此时的 newVal 是 user 对象 console.log(newVal === vm.user) }, { deep: true }) // immediate 是 true vm.$watch('msg', function (newVal, oldVal) { console.log(newVal) }, { immediate: true })
三种类型的 Watcher 对象
- 没有静态方法,因为 $watch 方法中要使用 Vue 的实例
- Watcher 分三种:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher
- 创建顺序:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher
- vm.$watch()
- src\core\instance\state.js
源码
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
// 获取 Vue 实例 this
const vm: Component = this
if (isPlainObject(cb)) {
// 判断如果 cb 是对象执行 createWatcher
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
// 标记为用户 watcher
options.user = true
// 创建用户 watcher 对象
const watcher = new Watcher(vm, expOrFn, cb, options)
// 判断 immediate 如果为 true
if (options.immediate) {
// 立即执行一次 cb 回调,并且把当前值传入
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher
"${watcher.expression}"`)
} }
// 返回取消监听的方法
return function unwatchFn () {
watcher.teardown()
}
}
- 查看渲染 watcher 的执行过程
- 当数据更新,defineReactive 的 set 方法中调用 dep.notify()
- 调用 watcher 的 update()
- 调用 queueWatcher(),把 wacher 存入队列,如果已经存入,不重复添加
- 循环调用 flushSchedulerQueue()
- 通过 nextTick(),在消息循环结束之前时候调用 flushSchedulerQueue()
- 调用 wacher.run()
- 调用 wacher.get() 获取最新值
- 如果是渲染 wacher 结束
- 如果是用户 watcher,调用 this.cb()
异步更新队列nextTick()
- Vue 更新 DOM 是异步执行的,批量的
- 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
- vm.$nextTick(function () { /* 操作 DOM */ }) / Vue.nextTick(function () {})
vm.$nextTick() 代码演示
<div id="app">
<p ref="p1">{{ msg }}</p>
</div>
<script src="../../dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
msg: 'Hello nextTick',
name: 'Vue.js',
title: 'Title'
},
mounted() {
this.msg = 'Hello World'
this.name = 'Hello snabbdom'
this.title = 'Vue.js'
this.$nextTick(() => {
console.log(this.$refs.p1.textContent)
}) }
})
</script>
定义位置
- src\core\instance\render.js
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
源码
- 手动调用 vm.$nextTick()
- 在 Watcher 的 queueWatcher 中执行 nextTick()
- src\core\util\next-tick.js
let timerFunc
// The nextTick behavior leverages the microtask queue, which can be
accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break,
but
// it can get stuck in a weird state where callbacks are pushed into
the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick (cb?: Function, ctx?: Object) { let _resolve
// 把 cb 加上异常处理存入 callbacks 数组中 callbacks.push(() => {
if (cb) {
try {
// 调用 cb()
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve)
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') { // 返回 promise 对象
return new Promise(resolve => {
_resolve = resolve
})
} }
三、Vue.js 源码剖析-虚拟 DOM
虚拟 DOM 回顾
什么是虚拟 DOM
虚拟 DOM(Virtual DOM) 是使用 JavaScript 对象来描述 DOM,虚拟 DOM 的本质就是 JavaScript 对 象,使用 JavaScript 对象来描述 DOM 的结构。应用的各种状态变化首先作用于虚拟 DOM,最终映射 到 DOM。Vue.js 中的虚拟 DOM 借鉴了 Snabbdom,并添加了一些 Vue.js 中的特性,例如:指令和组 件机制。
Vue 1.x 中细粒度监测数据的变化,每一个属性对应一个 watcher,开销太大Vue 2.x 中每个组件对应一 个 watcher,状态变化通知到组件,再引入虚拟 DOM 进行比对和渲染
为什么要使用虚拟 DOM
- 使用虚拟 DOM,可以避免用户直接操作 DOM,开发过程关注在业务代码的实现,不需要关注如 何操作 DOM,从而提高开发效率
- 作为一个中间层可以跨平台,除了 Web 平台外,还支持 SSR、Weex。
- 关于性能方面,在首次渲染的时候肯定不如直接操作 DOM,因为要维护一层额外的虚拟 DOM, 如果后续有频繁操作 DOM 的操作,这个时候可能会有性能的提升,虚拟 DOM 在更新真实 DOM 之前会通过 Diff 算法对比新旧两个虚拟 DOM 树的差异,最终把差异更新到真实 DOM
Vue.js 中的虚拟 DOM
- 演示 render 中的 h 函数
- h 函数就是 createElement()
const vm = new Vue({
el: '#app',
render (h) {
// h(tag, data, children)
// return h('h1', this.msg)
// return h('h1', { domProps: { innerHTML: this.msg } })
// return h('h1', { attrs: { id: 'title' } }, this.msg)
const vnode = h(
'h1', {
attrs: { id: 'title' }
},
this.msg )
console.log(vnode)
return vnode
},
data: {
msg: 'Hello Vue'
}
})
虚拟 DOM 创建过程

createElement
功能
createElement() 函数,用来创建虚拟节点 (VNode),我们的 render 函数中的参数 h,就是 createElement()
render(h) {
// 此处的 h 就是 vm.$createElement
return h('h1', this.msg)
}
定义
在 vm._render() 中调用了,用户传递的或者编译生成的 render 函数,这个时候传递了 createElement
- src/core/instance/render.js
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
vm.*c vm.createElement 在用户传入的 render 函数内部调用。当用户传入 render 函数的时候,要对用户传入的参数做处理
- src/core/vdom/create-element.js
执行完 createElement 之后创建好了 VNode,把创建好的 VNode 传递给 vm._update() 继续处理
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 判断第三个参数
// 如果 data 是数组或者原始值的话就是 children,实现类似函数重载的机制 if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data: any).__ob__)) {
......
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
......
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
){
data = data || {}
data.scopedSlots = { default: children[0] } children.length = 0
}// 去处理 children
if (normalizationType === ALWAYS_NORMALIZE) {
// 当手写 render 函数的时候调用
// 判断 children 的类型,如果是原始值的话转换成 VNode 的数组 // 如果是数组的话,继续处理数组中的元素
// 如果数组中的子元素又是数组(slot template),递归处理
// 如果连续两个节点都是字符串会合并文本节点
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 把二维数组转换为一维数组
// 如果 children 中有函数组件的话,函数组件会返回数组形式
// 这时候 children 就是一个二维数组,只需要把二维数组转换为一维数组 children = simpleNormalizeChildren(children)
}
let vnode, ns
// 判断 tag 是字符串还是组件
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) ||
config.getTagNamespace(tag)
// 如果是浏览器的保留标签,创建对应的 VNode
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
// 否则的话创建组件
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
update
功能
内部调用 vm.patch() 把虚拟 DOM 转换成真实 DOM
定义
- src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly
*/)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
patch 函数初始化 功能
对比两个 VNode 的差异,把差异更新到真实 DOM。如果是首次渲染的话,会把真实 DOM 先转换成 VNode
Snabbdom 中 patch 函数的初始化
- src/snabbdom.ts
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
}
}
- vnode
export function vnode (sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined): VNode {
const key = data === undefined ? undefined : data.key
return { sel, data, children, text, elm, key }
}
Vue.js 中 patch 函数的初始化
- src/platforms/web/runtime/index.js
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
- src/platforms/web/runtime/patch.js
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
- src/core/vdom/patch.js
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
// 把模块中的钩子函数全部设置到 cbs 中,将来统一触发 // cbs --> { 'create': [fn1, fn2], ... }
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
......
......
......
return function patch (oldVnode, vnode, hydrating, removeOnly) {
}
}
patch 函数执行过程
function patch (oldVnode, vnode, hydrating, removeOnly) { // 如果没有 vnode 但是有 oldVnode,执行销毁的钩子函数
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// 如果没有 oldVnode,创建 vnode 对应的真实 DOM
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// 判断当前 oldVnode 是否是 DOM 元素(首次渲染)
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 如果不是真实 DOM,并且两个 VNode 是 sameVnode,这个时候开始执行 Diff
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null,
removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
......
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
createElm
把 VNode 转换成真实 DOM,插入到 DOM 树上
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
){
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
......
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
patchVnode
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
){
// 如果新旧节点是完全相同的节点,直接返回
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
......
// 触发 prepatch 钩子函数
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// 获取新旧 VNode 的子节点
const oldCh = oldVnode.children
const ch = vnode.children
// 触发 update 钩子函数
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode,vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 如果 vnode 没有 text 属性(说明有可能有子元素)
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 如果新旧节点都有子节点并且不相同,这时候对比和更新子节点 if (oldCh !== ch)
updateChildren(elm, oldCh, ch,insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
// 如果新节点有子节点,并且旧节点有 text
// 清空旧节点对应的真实 DOM 的文本内容
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 把新节点的子节点添转换成真实 DOM,添加到 elm
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 如果旧节点有子节点,新节点没有子节点
// 移除所有旧节点对应的真实 DOM
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 如果旧节点有 text,新节点没有子节点和 text
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 如果新节点有 text,并且和旧节点的 text 不同 // 直接把新节点的 text 更新到 DOM 上 nodeOps.setTextContent(elm, vnode.text)
}
// 触发 postpatch 钩子函数 if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode,vnode)
}
}
updateChildren
updateChildren 和 Snabbdom 中的 updateChildren 整体算法一致,这里就不再展开了。我们再来看 下它处理过程中 key 的作用,再 patch 函数中,调用 patchVnode 之前,会首先调用 sameVnode()判 断当前的新老 VNode 是否是相同节点,sameVnode() 中会首先判断 key 是否相同。
- 通过下面代码来体会 key 的作用
<div id="app">
<button @click="handler">按钮</button>
<ul>
<li v-for="value in arr">{{value}}</li>
</ul>
</div>
<script src="../../dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
arr: ['a', 'b', 'c', 'd']
},
methods: {
handler () {
this.arr = ['a', 'x', 'b', 'c', 'd']
}
} })
</script>
- 当没有设置 key 的时候
在 updateChildren 中比较子节点的时候,会做三次更新 DOM 操作和一次插入 DOM 的操作
- 当设置 key 的时候
在 updateChildren 中比较子节点的时候,因为 oldVnode 的子节点的 b,c,d 和 newVnode 的 x,b,c 的 key 相同,所以只做比较,没有更新 DOM 的操作,当遍历完毕后,会再把 x 插入到 DOM 上DOM 操 作只有一次插入操作。
总结

四、模板编译
模板编译
- 模板编译的主要目的是将模板 (template) 转换为渲染函数 (render)
<div>
<h1 @click="handler">title</h1>
<p>some content</p>
</div>
- 渲染函数 render
render (h) {
return h('div', [
h('h1', { on: { click: this.handler} }, 'title'),
h('p', 'some content')
])
}
- 模板编译的作用
- Vue 2.x 使用 VNode 描述视图以及各种交互,用户自己编写 VNode 比较复杂
- 用户只需要编写类似 HTML 的代码 - Vue 模板,通过编译器将模板转换为返回 VNode 的 render 函数
- vue 文件会被 webpack 在构建的过程中转换成 render 函数
体验模板编译的结果
- 带编译器版本的 Vue.js 中,使用 template 或 el 的方式设置模板
<div id="app"> <h1>Vue<span>模板编译过程</span></h1> <p>{{ msg }}</p>
<comp @myclick="handler"></comp>
</div>
<script src="../../dist/vue.js"></script>
<script>
Vue.component('comp', {
template: '<div>I am a comp</div>'
})
const vm = new Vue({
el: '#app',
data: {
msg: 'Hello compiler'
},
methods: {
handler () {
console.log('test')
}
}
})
console.log(vm.$options.render)
</script>
- 编译后 render 输出的结果
(function anonymous() {
with (this) {
return _c(
"div",
{ attrs: { id: "app" } },
[
_m(0),
_v(" "),
_c("p", [_v(_s(msg))]),
_v(" "),
_c("comp", { on: { myclick: handler } }),
],
1
);
}
});
- _c 是 createElement() 方法,定义的位置 instance/render.js 中
- 相关的渲染函数(_开头的方法定义),在 instance/render-helps/index.js 中
// instance/render-helps/index.js
target._v = createTextVNode
target._m = renderStatic
// core/vdom/vnode.js
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
// 在 instance/render-helps/render-static.js
export function renderStatic (
index: number,
isInFor: boolean
): VNode | Array<VNode> {
const cached = this._staticTrees || (this._staticTrees = [])
let tree = cached[index]
// if has already-rendered static tree and not inside v-for,
// we can reuse the same tree.
if (tree && !isInFor) {
return tree
}
// otherwise, render a fresh tree.
tree = cached[index] = this.$options.staticRenderFns[index].call(
this._renderProxy,
null,
this // for render fns generated for functional component templates
)
markStatic(tree, `__static__${index}`, false)
return tree
}
- 把 template 转换成 render 的入口 src\platforms\web\entry-runtime-with-compiler.js
Vue Template Explorer
- vue-template-explorer
- Vue 2.6 把模板编译成 render 函数的工具
- vue-next-template-explorer
- Vue 3.0 beta 把模板编译成 render 函数的工具
模板编译过程
- 解析、优化、生成
编译的入口
- src\platforms\web\entry-runtime-with-compiler.js
Vue.prototype.$mount = function (
......
// 把 template 转换成 render 函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
......
)
- 调试 compileToFunctions() 执行过程,生成渲染函数的过程
- compileToFunctions: src\compiler\to-function.js
- complie(template, options):src\compiler\create-compiler.js
- baseCompile(template.trim(), finalOptions):src\compiler\index.js

解析 - parse
- 解析器将模板解析为抽象语树 AST,只有将模板解析成 AST 后,才能基于它做优化或者生成代码字符串。
- src\compiler\index.js
const ast = parse(template.trim(), options)
//src\compiler\parser\index.js
parse()
-
查看得到的 AST tree
- astexplorer
-
结构化指令的处理
- v-if 最终生成单元表达式
// src\compiler\parser\index.js // structural directives
// 结构化的指令
// v-for
processFor(element)
processIf(element)
processOnce(element)
// src\compiler\codegen\index.js
export function genIf (
el: any,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
el.ifProcessed = true // avoid recursion
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
// 最终调用 genIfConditions 生成三元表达式
- v-if 最终编译的结果
ƒ anonymous( ){
with(this){
return _c('div',{attrs:{"id":"app"}},[
_m(0),
_v(" "),
(msg)?_c('p',[_v(_s(msg))]):_e(),_v(" "),
_c('comp',{on:{"myclick":onMyClick}})
],1) }
}
v-if/v-for 结构化指令只能在编译阶段处理,如果我们要在 render 函数处理条件或循环只能使用 js 中的 if 和 for
Vue.component('comp', {
data: () {
return {
msg: 'my comp'
} },
render (h) {
if (this.msg) {
return h('div', this.msg)
}
return h('div', 'bar')
}
优化 - optimize
-
优化抽象语法树,检测子节点中是否是纯静态节点
-
一旦检测到纯静态节点,例如:
hello整体是静态节点
- 永远不会更改的节点
- 提升为常量,重新渲染的时候不在重新创建节点
- 在 patch 的时候直接跳过静态子树
// src\compiler\index.js
if (options.optimize !== false) {
optimize(ast, options)
}
// src\compiler\optimizer.js
/**
* Goal of the optimizer: walk the generated template AST tree
* and detect sub-trees that are purely static, i.e. parts of
* the DOM that never needs to change.
*
* Once we detect these sub-trees, we can:
*
* 1. Hoist them into constants, so that we no longer need to
* create fresh nodes for them on each re-render;
* 2. Completely skip them in the patching process.
*/
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes. // 标记非静态节点
markStatic(root)
// second pass: mark static roots.
// 标记静态根节点
markStaticRoots(root, false)
}
生成 - generate
// src\compiler\index.js
const code = generate(ast, options)
// src\compiler\codegen\index.js
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
// 把字符串转换成函数
// src\compiler\to-function.js
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
}
}
组件化机制
-
组件化可以让我们方便的把页面拆分成多个可重用的组件
-
组件是独立的,系统内可重用,组件之间可以嵌套
-
有了组件可以像搭积木一样开发网页
-
下面我们将从源码的角度来分析 Vue 组件内部如何工作
-
组件实例的创建过程是从上而下
-
组件实例的挂载过程是从下而上
-
组件声明
- 复习全局组件的定义方式
Vue.component('comp', {
template: '<h1>hello</h1>'
})
- Vue.component() 入口
- 创建组件的构造函数,挂载到 Vue 实例的 vm.options.component.componentName = Ctor
// src\core\global-api\index.js
// 注册 Vue.directive()、 Vue.component()、Vue.filter()
initAssetRegisters(Vue)
// src\core\global-api\assets.js
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
......
// 全局注册,存储资源并赋值
// this.options['components']['comp'] = Ctor
this.options[type + 's'][id] = definition
// src\core\global-api\index.js
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
// src\core\global-api\extend.js
Vue.extend()
- 组件构造函数的创建
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}