Vue全家桶实现原理简要梳理
网上Vue源码解读的文章有很多,但涉及到Vuex、vue-router的就比较少了。本文主要对描述了Vue的整体代码结构,用两个例子分别描述Vue实例化及ssr的流程;接着阐述Vue插件的注册方法,Vuex、vue-router大致实现原理。
Vue
如何用例子走一遍Vue代码
1.clone vue代码到本地
2.在example目录中写上任意你想要测试的例子
3.npm run dev && node e2e/test/runner.js
目录结构
- root/
- compiler/--------------------解析template,生成render函数和ast
- parser/-------------------正则遍历template里的字符串,通过栈记录元素关系,生成ast
- codegen/-----------------根据ast生成render函数
- directives/---------------解析ast中的v-bind,v-model指令,生成对应render函数
- index.js
- core/---------------------Vue实例相关,vue源码核心
- components/------------通用组件,keep-alive
- gloabal-api/----------注册Vue构造函数上的静态方法,比如Vue.install、Vue.set...
- instance/-------------注册vue.prototype,以及构造函数
- observer/-------------数据双向绑定相关,主要由watcher、observer、dep组成
- util/-------------工具
- vdom/-------------vnode相关,包含createVnode,patchNode等
- index.js
- platforms------------core基础上扩展
- web-------------将core中的代码包装成web平台所需的方法,比如Vue.prototype.$mount实际包装了core中的$mount
- weex
- server-----------ssr相关,执行Vue代码,生成Vue实例;输出流或字符串,传递给renderNode,renderNode通过Vnode生成各种HTML标签
- shared------------上述公共的工具
- util.js
- compiler/--------------------解析template,生成render函数和ast
vue构造函数
我们使用vue时都会先实例化一个Vue对象,先从Vue的构造函数说起。构造函数及原型相关代码绝大部分都在core/instance下。
至于怎么找到Vue构造函数的位置,运用从后向前的方法,从package.json一点点往会看就好了。
首先看core/instance/index.js文件,该文件主要定义了Vue的构造函数,并且初始化Vue.prototype中的一些方法。
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
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)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
initMixin就做了一件事情,在Vue的原型上增加_init方法,构造Vue实例的时候会调用这个_init方法来初始化Vue实例,下面常用使用原理中会详细说一下这一块。
stateMixin中主要声明了Vue.prototype.$data、Vue.prototype.$props、Vue.prototype.$set、Vue.prototype.$watch
export function stateMixin (Vue: Class<Component>) {
// flow somehow has problems with directly declared definition object
// when using Object.defineProperty, so we have to procedurally build up
// the object here.
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
......
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
Vue.prototype.$set = set
Vue.prototype.$delete = del
// 数据绑定相关后面会详细解读
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: Function,
options?: Object
): Function {
......
}
eventsMixin主要定义了Vue.prototype.$on/$off/$once,原理就是利用观察者模型,为每一个event维护一个观察队列,存放在Vue._events中。
lifecycleMixin中定义了我们Vue中经常用到的Vue.prototype._update方法,每当我们定义的组件data发生变化或其他原因需要重新渲染时,Vue会调用该方法,对Vnode做diff和patch操作。
export function lifecycleMixin (Vue: Class<Component>) {
/*更新节点*/
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
/*如果已经该组件已经挂载过了则代表进入这个步骤是个更新的过程,触发beforeUpdate钩子*/
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
/*基于后端渲染Vue.prototype.__patch__被用来作为一个入口*/
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// update __vue__ reference
/*更新新的实例对象的__vue__*/
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.
}
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
/* 调用beforeDestroy钩子 */
callHook(vm, 'beforeDestroy')
/* 标志位 */
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
/* 该组件下的所有Watcher从其所在的Dep中释放 */
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
/* 调用destroyed钩子 */
callHook(vm, 'destroyed')
// turn off all instance listeners.
/* 移除所有事件监听 */
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// remove reference to DOM nodes (prevents leak)
vm.$options._parentElm = vm.$options._refElm = null
}
}
renderMixin中定义了Vue.prototype._render等方法,_render()调用实例化时传入的render方法,生成VNode。经常与Vue.prototype.update一起使用。
// 组件更新时调用
vm._update(vm._render(), hydrating);
常见使用原理解读
创建实例
// boot up the demo
var demo = new Vue({
el: '#demo',
data: {
treeData: data,
a: 1
},
computed: {
hello() {
return this.treeData;
}
},
render(createElement) {
// @returns {VNode}
return createElement(
// {String | Object | Function}
// 一个 HTML 标签字符串,组件选项对象,或者一个返回值类型为 String/Object 的函数,必要参数
'div',
// {Object}
// 一个包含模板相关属性的数据对象
// 这样,您可以在 template 中使用这些属性。可选参数。
{
// (详情见下一节)
},
// {String | Array}
// 子节点 (VNodes),由 `createElement()` 构建而成,
// 或使用字符串来生成“文本节点”。可选参数。
[
// createElement(Profile3),
'先写一些文字',
createElement('h1', '一则头条'),
// createElement(Profile),
// createElement(Profile4)
]
)
}
})
new Vue()其实就是调用构造函数中的this._init(),this._init()就是调用上述instance/init中声明的Vue.prototype._init
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
......
// expose real self
vm._self = vm
/*初始化生命周期*/
initLifecycle(vm)
/*初始化事件*/
initEvents(vm)
/*初始化render*/
initRender(vm)
/*调用beforeCreate钩子函数并且触发beforeCreate钩子事件*/
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
/*初始化props、methods、data、computed与watch*/
initState(vm)
initProvide(vm) // resolve provide after data/props
/*调用created钩子函数并且触发created钩子事件*/
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
/*格式化组件名*/
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
/*挂载组件*/
vm.$mount(vm.$options.el)
}
}
}
initLifecycle,主要把自己push到parent.$children中
/*初始化生命周期*/
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
/* 将vm对象存储到parent组件中(保证parent组件是非抽象组件,比如keep-alive) */
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
......
}
initEvents,主要初始化了vm._events存放事件。$on()方法就是将事件监听存放在这里。
/*初始化事件*/
export function initEvents (vm: Component) {
/*在vm上创建一个_events对象,用来存放事件。*/
vm._events = Object.create(null)
......
}
initRender,定义了vm.$createElement方法,我们调用render()方法时,传入参数就是vm.$createElement。
/*初始化render*/
export function initRender (vm: Component) {
......
/*将createElement函数绑定到该实例上,该vm存在闭包中,不可修改,vm实例则固定。这样我们就可以得到正确的上下文渲染*/
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)
}
initState,这里主要initProps,initComputed,initData。我们首先介绍initData,并借initData来解读一下Vue的数据响应系统。
initData调用observer/index.js中的observe方法,生成observer对象,observer遍历data中的数据,把每一项数据都变成响应式的。
initData,主要就看最后一行调用observe()。
/*initData*/
function initData (vm: Component) {
/*得到data数据*/
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
/*对对象类型进行严格检查,只有当对象是纯javascript对象的时候返回true*/
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
)
}
// proxy data on instance
/*遍历data对象*/
const keys = Object.keys(data)
const props = vm.$options.props
let i = keys.length
//遍历data中的数据
while (i--) {
/*保证data中的key不与props中的key重复,props优先,如果有冲突会产生warning*/
if (props && hasOwn(props, keys[i])) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${keys[i]}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(keys[i])) {
/*判断是否是保留字段*/
/*这里是我们前面讲过的代理,将data上面的属性代理到了vm实例上*/
proxy(vm, `_data`, keys[i])
}
}
// observe data
/*从这里开始我们要observe了,开始对数据进行绑定,下面会进行递归observe进行对深层对象的绑定。*/
observe(data, true /* asRootData */)
}
observe中new Observer(), new Observer()会将data中的所有数据调用defineReactive变成响应式。主要原理就是利用Object.defineProperty,get()时增加依赖,也就是观察者,set时通知观察者。
/*为对象defineProperty上在变化时通知的属性*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: Function
) {
/*在闭包中定义一个dep对象*/
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
/*如果之前该对象已经预设了getter以及setter函数则将其取出来,新定义的getter/setter中会将其执行,保证不会覆盖之前已经定义的getter/setter。*/
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
/*对象的子对象递归进行observe并返回子节点的Observer对象*/
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
/*如果原本对象拥有getter方法则执行*/
const value = getter ? getter.call(obj) : val
if (Dep.target) {
/*进行依赖收集*/
dep.depend()
if (childOb) {
/*子对象进行依赖收集,其实就是将同一个watcher观察者实例放进了两个depend中,一个是正在本身闭包中的depend,另一个是子元素的depend*/
childOb.dep.depend()
}
if (Array.isArray(value)) {
/*是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。*/
dependArray(value)
}
}
return value
},
set: function reactiveSetter (newVal) {
/*通过getter方法获取当前值,与新值进行比较,一致则不需要执行下面的操作*/
const value = getter ? getter.call(obj) : val
/* 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()
}
if (setter) {
/*如果原本对象拥有setter方法则执行setter*/
setter.call(obj, newVal)
} else {
val = newVal
}
/*新的值需要重新进行observe,保证数据响应式*/
childOb = observe(newVal)
/*dep对象通知所有的观察者*/
dep.notify()
}
})
}
我们在声明组件时,经常使用watch,实际调用了new watcher(a, callback),watcher相当于一个观察者。我们来看watcher里的代码,其实也不难理解,watcher就是一个订阅者。关键在于watcher如何与observer联系在一起,observer中的数据set()时,如何找到对应的watcher呢?dep出现了!注意下面的get()中的pushTarget(),该方法就是将自己放到dep模块中的全局变量上,然后调用this.getter.call(vm, vm),也就是调用了obsever的get(),get()中取得dep中的全局变量,加到了自身的dep中,当set时,会遍历执行dep中存放所有watcher的run()方法,执行callback。
watcher和dep代码如下。
/*
一个解析表达式,进行依赖收集的观察者,同时在表达式数据变更时触发回调函数。它被用于$watch api以及指令
*/
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
/*_watchers存放订阅者实例*/
vm._watchers.push(this)
......
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
/*获得getter的值并且重新进行依赖收集*/
get () {
......
/*将自身watcher观察者实例设置给Dep.target,用以依赖收集。*/
pushTarget(this)
......
value = this.getter.call(vm, vm)
......
if (this.deep) {
/*递归每一个对象或者数组,触发它们的getter,使得对象或数组的每一个成员都被依赖收集,形成一个“深(deep)”依赖关系*/
traverse(value)
}
/*将观察者实例从target栈中取出并设置给Dep.target*/
popTarget()
this.cleanupDeps()
return value
}
/*
调度者工作接口,将被调度者回调。
*/
run () {
......
this.cb.call(this.vm, value, oldValue)
......
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
/*获取观察者的值*/
evaluate () {
this.value = this.get()
this.dirty = false
}
export default class Dep {
constructor () {
this.id = uid++
this.subs = []
}
/*添加一个观察者对象*/
addSub (sub: Watcher) {
this.subs.push(sub)
}
/*移除一个观察者对象*/
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
/*依赖收集,当存在Dep.target的时候添加观察者对象*/
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
/*通知所有订阅者*/
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
/*依赖收集完需要将Dep.target设为null,防止后面重复添加依赖。*/
Dep.target = null
const targetStack = []
/*将watcher观察者实例设置给Dep.target,用以依赖收集。同时将该实例存入target栈中*/
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
/*将观察者实例从target栈中取出并设置给Dep.target*/
export function popTarget () {
Dep.target = targetStack.pop()
}
总结一下initData及Vue的响应式数据。
1.observe(data) => defineReactive
2.watch(a, callback) => new Watcher() => pushTarget(this) => getter.call()
3.getter()中执行dep.depend,收集pushTarget的watcher
4.当执行a=3时,遍历dep中存储的所有watcher,执行其监听函数。
我们接下来看initComputed,其实就是new了一个watcher,然后执行computed函数时会调用其中所有依赖数据的getter,从而将该watcher加入到其依赖数据的dep中。
/*初始化computed*/
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
/*
计算属性可能是一个function,也有可能设置了get以及set的对象。
可以参考 https://cn.vuejs.org/v2/guide/computed.html#计算-setter
*/
let getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production') {
/*getter不存在的时候抛出warning并且给getter赋空函数*/
if (getter === undefined) {
warn(
`No getter function has been defined for computed property "${key}".`,
vm
)
getter = noop
}
}
// create internal watcher for the computed property.
/*
为计算属性创建一个内部的监视器Watcher,保存在vm实例的_computedWatchers中
这里的computedWatcherOptions参数传递了一个lazy为true,会使得watch实例的dirty为true
*/
watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
/*组件正在定义的计算属性已经定义在现有组件的原型上则不会进行重复定义*/
if (!(key in vm)) {
/*定义计算属性*/
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
/*如果计算属性与已定义的data或者props中的名称冲突则发出warning*/
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
声明组件
var Profile3 = Vue.component({
template: `<div id="demo">
<button v-on:click="show = !show">
Toggle
</button>
<transition name="fade">
<p v-if="show">hello</p>
</transition>
</div>`,
data: function () {
return {
firstName: '',
lastName: 'White',
alias: 'Heisenberg',
show: true
}
}
})
这里主要说一下Vue.component方法与Vue.extend方法。
Vue.extend(core/global-api/extend.js), 其实就是寄生组合继承了Vue。
/*
使用基础 Vue 构造器,创建一个“子类”。
其实就是扩展了基础构造器,形成了一个可复用的有指定选项功能的子构造器。
参数是一个包含组件option的对象。 https://cn.vuejs.org/v2/api/#Vue-extend-options
*/
Vue.extend = function (extendOptions: Object): Function {
......
/*
Sub构造函数其实就一个_init方法,这跟Vue的构造方法是一致的,在_init中处理各种数据初始化、生命周期等。
因为Sub作为一个Vue的扩展构造器,所以基础的功能还是需要保持一致,跟Vue构造器一样在构造函数中初始化_init。
*/
const Sub = function VueComponent (options) {
this._init(options)
}
/*继承父类*/
Sub.prototype = Object.create(Super.prototype)
/*构造函数*/
Sub.prototype.constructor = Sub
/*创建一个新的cid*/
Sub.cid = cid++
/*将父组件的option与子组件的合并到一起(Vue有一个cid为0的基类,即Vue本身,会将一些默认初始化的option何入)*/
Sub.options = mergeOptions(
Super.options,
extendOptions
)
/*利用super标记父类*/
Sub['super'] = Super
......
return Sub
}
}
Vue.component与Vue.extend类似,core/global-api/assets.js
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)// vue.extend
}
组件挂载
在Vue.prototype._init中最后调用了vm.$mount(vm.$options.el)
流程如下:
首先什么事vdom,其实很简单,就是一个组件就是一个vdom对象,维护在Vue中,方便取新老vnode去diff,然后针对性的去渲染。
Vnode:
export default class VNode {
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions
) {
/*当前节点的标签名*/
this.tag = tag
/*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.data = data
/*当前节点的子节点,是一个数组*/
this.children = children
/*当前节点的文本*/
this.text = text
/*当前虚拟节点对应的真实dom节点*/
this.elm = elm
/*当前节点的名字空间*/
this.ns = undefined
/*当前节点的编译作用域*/
this.context = context
/*函数化组件作用域*/
this.functionalContext = undefined
/*节点的key属性,被当作节点的标志,用以优化*/
this.key = data && data.key
/*组件的option选项*/
this.componentOptions = componentOptions
/*当前节点对应的组件的实例*/
this.componentInstance = undefined
/*当前节点的父节点*/
this.parent = undefined
/*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.raw = false
/*是否为静态节点*/
this.isStatic = false
/*是否作为跟节点插入*/
this.isRootInsert = true
/*是否为注释节点*/
this.isComment = false
/*是否为克隆节点*/
this.isCloned = false
/*是否有v-once指令*/
this.isOnce = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
渲染(patch)主要逻辑大致如下
patch
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
/*vnode不存在则直接调用销毁钩子*/
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
/*oldVnode未定义的时候,其实也就是root节点,创建一个新的节点*/
isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
/*标记旧的VNode是否有nodeType*/
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
/*是同一个节点的时候直接修改现有的节点*/
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
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)
)
}
/*调用insert钩子*/
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
patchNode:
/*如果这个VNode节点没有text文本时*/
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
/*新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren*/
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
/*如果老节点没有子节点而新节点存在子节点,先清空elm的文本内容,然后为当前节点加入子节点*/
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
/*当新节点没有子节点而老节点有子节点的时候,则移除所有ele的子节点*/
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
/*当新老节点都无子节点的时候,只是文本的替换,因为这个逻辑中新节点text不存在,所以直接去除ele的文本*/
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
/*当新老节点text不一样时,直接替换这段文本*/
nodeOps.setTextContent(elm, vnode.text)
}
如果两个节点都有children, updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, elmToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
/*前四种情况其实是指定key的时候,判定为同一个VNode,则直接patchVnode即可,分别比较oldCh以及newCh的两头节点2*2=4种情况*/
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
/*
生成一个key与旧VNode的key对应的哈希表(只有第一次进来undefined的时候会生成,也为后面检测重复的key值做铺垫)
比如childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}] beginIdx = 0 endIdx = 2
结果生成{key0: 0, key1: 1, key2: 2}
*/
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
/*如果newStartVnode新的VNode节点存在key并且这个key在oldVnode中能找到则返回这个节点的idxInOld(即第几个节点,下标)*/
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
if (isUndef(idxInOld)) { // New element
/*newStartVnode没有key或者是该key没有在老节点中找到则创建一个新的节点*/
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
/*获取同key的老节点*/
elmToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !elmToMove) {
/*如果elmToMove不存在说明之前已经有新节点放入过这个key的Dom中,提示可能存在重复的key,确保v-for的时候item有唯一的key值*/
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
if (sameVnode(elmToMove, newStartVnode)) {
/*如果新VNode与得到的有相同key的节点是同一个VNode则进行patchVnode*/
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
/*因为已经patchVnode进去了,所以将这个老节点赋值undefined,之后如果还有新节点与该节点key相同可以检测出来提示已有重复的key*/
oldCh[idxInOld] = undefined
/*当有标识位canMove实可以直接插入oldStartVnode对应的真实Dom节点前面*/
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
// same key but different element. treat as new element
/*当新的VNode与找到的同样key的VNode不是sameVNode的时候(比如说tag不一样或者是有不一样type的input标签),创建一个新的节点*/
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
}
if (oldStartIdx > oldEndIdx) {
/*全部比较完成以后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多,所以这时候多出来的新节点需要一个一个创建出来加入到真实Dom中*/
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
/*如果全部比较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,老节点多余新节点,这个时候需要将多余的老节点从真实Dom中移除*/
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
这块网上有很多讲解,篇幅已经够长了,就不在这啰嗦了。推荐Vue 2.0 的 virtual-dom 实现简析
ssr
const clientBundleFileUrl = '/bundle.client.js';
const clientBundleFilePath = path.join(__dirname, '../dist/bundle.client.js');
// Server-Side Bundle File
const serverBundleFilePath = path.join(__dirname, '../dist/bundle.server.js')
// Server-Side Rendering
app.get('/', function (req, res) {
// const vm = new App({ url: req.url })
const serverBundleFileCode = fs.readFileSync(serverBundleFilePath, 'utf8');
const bundleRenderer = vueServerRenderer.createBundleRenderer(serverBundleFileCode);
// Client-Side Bundle File
const stream = bundleRenderer.renderToStream()
res.write(`<!DOCTYPE html><html><head><title>...</title></head><body>`)
stream.on('data', chunk => {
console.log(chunk.toString())
res.write(chunk)
})
stream.on('end', () => {
res.end('</body></html>')
})
});
流程如下
const renderer = createRenderer(rendererOptions);返回的renderer主要是使用renderNode()方法,根据Vnode各种拼html。
if (isDef(node.tag)) {
renderElement(node, isRoot, context)
const run = createBundleRunner(entry, files, basedir, runInNewContext)返回一个Promise,在Promise中执行打包后的代码,resolve(app)=>返回实例。
return (userContext = {}) => new Promise(resolve => {
userContext._registeredComponents = new Set()
const res = evaluate(entry, createContext(userContext))
resolve(typeof res === 'function' ? res(userContext) : res)
})
那么我们打包后的ssr代码,如何执行的呢?在createBundleRunner有这样一段代码。其中NativeModule.wrap()方法也是node中包裹模块时使用的方法。
const code = files[filename]
const wrapper = NativeModule.wrap(code)
const script = new vm.Script(wrapper, {
filename,
displayErrors: true
})
下面我们来看renderToStream,其实是调用了renderer.renderToStream,下面我们来看renderer.renderToStream。
renderToStream (
component: Component,
context?: Object
): stream$Readable {
if (context) {
templateRenderer.bindRenderFns(context)
}
const renderStream = new RenderStream((write, done) => {
render(component, write, context, done)
})
if (!template) {
return renderStream
} else {
const templateStream = templateRenderer.createStream(context)
renderStream.on('error', err => {
templateStream.emit('error', err)
})
renderStream.pipe(templateStream)
return templateStream
}
}
其中new RenderStream(),RenderStream代码如下
export default class RenderStream extends stream.Readable {
constructor (render: Function) {
super()
this.buffer = ''
this.render = render
this.expectedSize = 0
this.write = createWriteFunction((text, next) => {
const n = this.expectedSize
this.buffer += text
if (this.buffer.length >= n) {
this.next = next
this.pushBySize(n)
return true // we will decide when to call next
}
return false
}, err => {
this.emit('error', err)
})
this.end = () => {
// the rendering is finished; we should push out the last of the buffer.
this.done = true
this.push(this.buffer)
}
}
pushBySize (n: number) {
const bufferToPush = this.buffer.substring(0, n)
this.buffer = this.buffer.substring(n)
this.push(bufferToPush)
}
tryRender () {
try {
this.render(this.write, this.end)
} catch (e) {
this.emit('error', e)
}
}
tryNext () {
try {
this.next()
} catch (e) {
this.emit('error', e)
}
}
_read (n: number) {
this.expectedSize = n
// it's possible that the last chunk added bumped the buffer up to > 2 * n,
// which means we will need to go through multiple read calls to drain it
// down to < n.
if (isTrue(this.done)) {
this.push(null)
return
}
if (this.buffer.length >= n) {
this.pushBySize(n)
return
}
if (isUndef(this.next)) {
// start the rendering chain.
this.tryRender()
} else {
// continue with the rendering.
this.tryNext()
}
}
}
主要看_read()方法。
``
所有实现可读流的实例必须实现readable._read() 方法去获得底层的数据资源。
当 readable._read() 被调用,如果读取的数据是可用的,应该在最开始的实现的时候使用this.push(dataChunk)方法将该数据推入读取队列。_read() 应该一直读取资源直到推送数据方法readable.push()返回false的时候停止。想再次调用_read()方法,需要再次往可读流里面push数据。
RenderStream继承stream.Readable,声明_read读取底层数据,在数据流缓冲队列超过max_size(node实现的16384),this.pushBySize;当steam done之后,this.tryRender
tryRender:
const context = new RenderContext({
activeInstance: component,
userContext,
write, done, renderNode,
isUnaryTag, modules, directives,
cache
})
installSSRHelpers(component)
normalizeRender(component)
renderNode(component._render(), true, context)
值得借鉴的地方
1.代码组织结构。Vue的代码耦合度还是比较低的,比如核心的部分都在Core中,在Platforms的web和weex中很方便的对其进行扩展;代码组织也比较清晰,基本一个模块只做一件事情,比如compile中就是compile template的,大家看起来一目了然
2.缓存也是用的不错的,基本上可能重复用到的地方都用了缓存。
Vue插件
Vue中如何自定义插件
Vue中绝大本分插件都是通过Vue.use()方法,该方法传入一个对象作为参数,执行对象的Install方法。
Vue.use = function (plugin: Function | Object) {
/* istanbul ignore if */
/*标识位检测该插件是否已经被安装*/
if (plugin.installed) {
return
}
// additional parameters
const args = toArray(arguments, 1)
/*a*/
args.unshift(this)
if (typeof plugin.install === 'function') {
/*install执行插件安装*/
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
plugin.installed = true
return this
}
那么我们一般使用插件时,以Vuex为例,直接在实例化Vue时加入,在组件中直接使用this.$store,这又是如何做到的呢?
一般会在install中注册beforeCreate的钩子,在钩子函数中将options或父组件中的方法或属性赋给自组件。利用了父组件create先于子组件的关系,从上到下的进行注册。下面以Vuex为例。
export default function (Vue) {
/*获取Vue版本,鉴别Vue1.0还是Vue2.0*/
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
/*通过mixin将vuexInit混淆到Vue实例的beforeCreate钩子中*/
Vue.mixin({ beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
/*将vuexInit放入_init中调用*/
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
/*Vuex的init钩子,会存入每一个Vue实例等钩子列表*/
function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
/*存在store其实代表的就是Root节点,直接执行store(function时)或者使用store(非function)*/
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
/*子组件直接从父组件中获取$store,这样就保证了所有组件都公用了全局的同一份store*/
this.$store = options.parent.$store
}
}
}
Vuex
从store的构造函数说起
constructor (options = {}) {
......
this._modules = new ModuleCollection(options)
/* 根据namespace存放module */
this._modulesNamespaceMap = Object.create(null)
/* 存放订阅者 */
this._subscribers = []
/* 用以实现Watch的Vue实例 */
this._watcherVM = new Vue()
// bind commit and dispatch to self
/*将dispatch与commit调用的this绑定为store对象本身,否则在组件内部this.dispatch时的this会指向组件的vm*/
const store = this
const { dispatch, commit } = this
/* 为dispatch与commit绑定this(Store实例本身) */
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}
/*初始化根module,这也同时递归注册了所有子modle,收集所有module的getter到_wrappedGetters中去,this._modules.root代表根module才独有保存的Module对象*/
installModule(this, state, [], this._modules.root)
......
}
this._modules = new ModuleCollection(options),初始化modules,返回一个Module树,数据结构如下:
- rootModule
- _children(k,v对象)
- _rawModule
- state
- namespace(parent/son/xxx/xxx),用path可以用来寻找父State
installModule(this, state, [], this._modules.root),根据上述module树,递归注册mutation,action。。。
/* 遍历注册mutation */
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
/* 遍历注册action */
module.forEachAction((action, key) => {
const namespacedType = namespace + key
registerAction(store, namespacedType, action, local)
})
/* 遍历注册getter */
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
/* 递归安装mudule */
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
最终形成Store的数据结构如下:
- _mutations
- { key(nameSpace+key): [] }
- _actions(在里面会执行commit等,所以特意构建了一个LocalContext。里面的type = namespace + type)
- { key(nameSpace+key): [] }
- _modules
mutation和action实现上又什么区别呢?可以从下面看出action执行handle,然后判断是否是Promise来决定返回。
/* 遍历注册mutation */
function registerMutation (store, type, handler, local) {
/* 所有的mutation会被push进一个数组中,这样相同的mutation就可以调用不同module中的同名的mutation了 */
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload)
})
}
/* 遍历注册action */
function registerAction (store, type, handler, local) {
/* 取出type对应的action */
const entry = store._actions[type] || (store._actions[type] = [])
entry.push(function wrappedActionHandler (payload, cb) {
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload, cb)
/* 判断是否是Promise */
if (!isPromise(res)) {
/* 不是Promise对象的时候转化称Promise对象 */
res = Promise.resolve(res)
}
if (store._devtoolHook) {
/* 存在devtool捕获的时候触发vuex的error给devtool */
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
})
}
state中的数据是怎样加入到Vue的响应体系中的呢?使用Vue.暴露出得$set。
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue-router
挂载方法与上述类似,只不过多做了router._init及注册组件router-view和router-link。
/* 混淆进Vue实例,在boforeCreate与destroyed钩子上混淆 */
Vue.mixin({
/* boforeCreate钩子 */
beforeCreate () {
if (isDef(this.$options.router)) {
/* 在option上面存在router则代表是根组件 */
/* 保存跟组件vm */
this._routerRoot = this
/* 保存router */
this._router = this.$options.router
/* VueRouter对象的init方法 */
this._router.init(this)
/* Vue内部方法,为对象defineProperty上在变化时通知的属性 */
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
/* 非根组件则直接从父组件中获取 */
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
/* 通过registerRouteInstance方法注册router实例 */
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
/* 在Vue的prototype上面绑定$router,这样可以在任意Vue对象中使用this.$router访问,同时经过Object.defineProperty,访问this.$router即访问this._routerRoot._router */
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
/* 以上同理,访问this.$route即访问this._routerRoot._route */
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
/* 注册touter-view以及router-link组件 */
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
我们接下来看一下router的构造函数
constructor (options: RouterOptions = {}) {
this.app = null
/* 保存vm实例 */
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
this.matcher = createMatcher(options.routes || [], this)根据pathList, pathMap, nameMap来找出跟路由匹配的route对象。
PathMap结构如下:
this.matcher.match用来查找匹配的路由,返回route对象,主要步骤有标准化路由(normalizeLocation)、从pathMap/pathList/nameMap中取响应记录、返回route对象。
参数
RawLocation,currentRoute, redirectedFrom
1.normalizeLocation(raw, currentRoute, false, router)
步骤:resolvePath, resolveQuery,hadleHash
返回
return {
_normalized: true,
path,
query,
hash
}
resolvePath
处理相对路径的逻辑
const segments = relative.replace(/^\//, '').split('/')
for (let i = 0; i < segments.length; i++) {
const segment = segments[i]
if (segment === '..') {
stack.pop()
} else if (segment !== '.') {
stack.push(segment)
}
}
2.如果有name,直接从nameMap中取出record就行;如果有path,则从遍历pathList,取matchRoute
matchRoute(record.regex, location.path, location.params),将正则匹配到的值赋给params
3.createRoute
_createRoute(record, location, redirectedFrom)
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : []
}
router.history,分history/hash/abstract三种,histoy、hash即咱们理解的history和hash,abstract是Vue router自己利用堆栈实现的一套记录路由的方式。大致操作方法如下。
H5:
pushState/replaceState
Hash
window.addEventListener(supportsPushState ? 'popstate' : 'hashchange’
// 拼一个#hash=>pushState/replaceState
Abstract
this.stack = this.stack.slice(0, this.index + 1).concat(route)
router-view和router-link为vue-router默认的组件
首先看router-view,router-view组件在render中首先向上遍历到根结点,找到当前router-view的深度,也就是定义router是children的深度;找到对应组件;render()。
* router-view组件 */
export default {
name: 'RouterView',
/*
https://cn.vuejs.org/v2/api/#functional
使组件无状态 (没有 data ) 和无实例 (没有 this 上下文)。他们用一个简单的 render 函数返回虚拟节点使他们更容易渲染。
*/
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
......
/* _routerRoot中中存放了根组件的实例,这边循环向上级访问,直到访问到根组件,得到depth深度 */
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++
}
/* 如果_inactive为true,代表是在keep-alive中且是待用(非alive状态) */
if (parent._inactive) {
inactive = true
}
parent = parent.$parent
}
/* 存放route-view组件的深度 */
data.routerViewDepth = depth
......
/* 注册实例的registration钩子,这个函数将在实例被注入的加入到组件的生命钩子(beforeCreate与destroyed)中被调用 */
data.registerRouteInstance = (vm, val) => {
/* 第二个值不存在的时候为注销 */
// val could be undefined for unregistration
/* 获取组件实例 */
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
/* 这里有两种情况,一种是val存在,则用val替换当前组件实例,另一种则是val不存在,则直接将val(这个时候其实是一个undefined)赋给instances */
matched.instances[name] = val
}
}
......
return h(component, data, children)
}
}
那么,routerView是如何得知路由变化,触发其render()的呢?这又回到了View的响应式中,Vue中Vm或数据发生变化时,会调用q前文提到的vue.update(vm.render())方法更新操作。
vue-router在初始化时Vue.util.defineReactive(this, '_route', this._router.history.current)将_route变成了响应式,在路由发生变化时,执行updateRoute()将新的route赋给_route。
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
router-link比较简单。默认a标签,监听click事件,确定是router.push还是router.replace。
路由和组件时怎么对应的呢,路由变化后,组件如何变化呢?
1.找出matched route
const route = this.router.match(location, this.current)
2.confirmTransition
A.找出哪些record要删除、保留、添加
B.confirmTransition
分别执行这些record中instance下的钩子
生命周期:beforeRouteLeave =》beforeRouteUpdate =》beforeRouteEnter
3.最后执行路由切换
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
4.confirmTransition的callback,更新app._route
updateRoute (route: Route) {
...
This.callback() => this._route = matchedRoute
...
}
onComplete(history子类中调用handle scroll等钩子)
学习了