new Vue
创建一个Vue实例只执行了这一个方法,但是这里面有一大堆方法...
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
...
vm._self = vm
// 初始化实例的各种数据
initLifecycle(vm)
// 添加各种事件
initEvents(vm)
// 在实例上添加createElement
initRender(vm)
callHook(vm, 'beforeCreate')
// 获取inject,并对其observe
initInjections(vm) // resolve injections before data/props
// 这里处理的数据就多了,data、methods都有
initState(vm)
// 将project挂载到实例的_provided上
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
...
// 这个el就是options里的那个宿主元素
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
这里可以稍总结下:beforeCreate之前都是在初始化数据之外的各种方法,beforeCreate和created之间都在响应化数据
initLifecycle
export function initLifecycle (vm: Component) {
const options = vm.$options
let parent = options.parent
// 有parent的就不是root节点,在parent的children中push一个当前实例
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
// 定义根节点
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
// 以下就是清空各种数据
vm.$children = []
vm.$refs = {}
vm._watcher = null
...
}
initRender
export function initRender (vm: Component) {
...
// 主要用在render函数的 with(this){} 里边的_c就是这里,与用户无关
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// render(h){},里的h就是这里,就是经常提到的渲染函数
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// 使用科里化定义上边两个方法,目的就是保留当前的实例,以便在with(this)中使用
...
}
initInjections
这里的执行顺是在initData之前的,源码中也有说明
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
// 用defineReactive方法对inject每个属性进行数据拦截
Object.keys(result).forEach(key => {
...
})
toggleObserving(true)
}
}
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
...
for (let i = 0; i < keys.length; i++) {
...
// 这里是一个寻找inject属性的迭代,直到在parent中找到为止
...
}
return result
}
}
initState
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// props的响应化处理类似initData,就不做赘述了
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initMethods
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
...
// 这里做了是否和props重名的校验,不能以_、$开头,必须是fn
...
}
// 这里就是为什么可以直接this.fn,做了bind并绑定了作用域
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
initData
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
...
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
...
// 不能和methods、props重名的校验
...
// 这里做了proxy的转发,_data是在instance/index的initMixin作了拦截处理的,这就是为啥data中的数据可以直接访问
proxy(vm, `_data`, key)
observe(data, true /* asRootData */)
}
数据拦截和依赖收集
vue的响应式原理就是就是对数据的进行拦截,只有经过数据拦截过的属性才能响应式
// observe(data),首先判断data有没有一个ob,没有就添加
export function observe (value: any, asRootData: ?boolean): Observer | void {
...
// 如果有_ob_属性,说明是已做过处理的,直接返回
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
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
Observer
创建Observer实例有两个作用:
- 在对象上添加ob属性,每个ob属性都会挂上一个dep的实例,dep的作用在于收集与当前属性相关的Watcher实例
- 对val值进行数据拦截,dep的收集过程就在这里,对象和数组的处理不同
export class Observer {
...
constructor (value: any) {
this.value = value
// dep存在于每个被劫持的对象,收集跟这个属性有关的watcher
this.dep = new Dep()
this.vmCount = 0
// 在value上挂载_ob_指向这个ob实例
def(value, '__ob__', this)
if (Array.isArray(value)) {
...
this.observeArray(value)
} else {
this.walk(value)
}
}
// 对象的数据劫持
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// 数组的数据劫持
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
dep做的事情就比较简单了,存、删watcher,逐个派发watcher的update
export default class Dep {
...
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
...
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target
这个target不是挂在每个dep上,而是挂在Dep构造函数上的,这个指向是在哪里改变的呢?
export default class Watcher {
...
constructor (...) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
...
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
...
}
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)
}
}
}
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
if (this.active) {
const value = this.get()
...
}
}
...
}
在new Watcher()时,最后会执行get方法 => pushTarget(this)
defineReactive
数据拦截和依赖收集
- 创建一个dep实例:
- 由于get、set对dep有引用,所有该实例会一直存到内存中,数据太多就会产生大量的dep
- 这个dep和observe的dep作用是一样的,只不过现在的dep对应对象的每个属性,observe的dep对应包含属性的那个对象
- get在拦截数据时并不会触发:
getter指向哪个fn,取决于Dep.target的指向,Dep.target是组件那个watcher时,getter对应new Watcher()时定义的updateComponent,指向用户定义的watcher时指的就是那个watcher的回调- dep.depend就是在收集依赖,先在Dep.target指向的那个watcher中添加这个dep,再在dep中添加这个watcher,就是说dep中有watcher,watcher中有dep
- set做的两件事就比较简单了
- 首先如果新值是对象,那还要再重新进项数据拦截
- 派发dep的notify,就是循环调用dep收集的watcher的update方法,这里有一个queueWatcher,这是个存放watcher的队列,并不是改一下数据就执行一次,它会根据watcher的id限制watcher的入栈,id相同的watcher只会进去第一个,但是取的值就是最后一个watcher的
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 这里定义的dep不会挂载到对象本身,因为get和set中会用到,所以会保留到内存中
const dep = new Dep()
...
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
...
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
initComputed
后续更新
initWatch
后续更新
以上操作,数据的各种处理逻辑基本上就完事儿了,开始vm.$mount
$mount 生成render函数
mount的执行顺序就是
- 先找到render
- 没找到render,就先去找template,可以在这里看到render、template等的优先级
- render优先级是最高的,有定义render就直接执行
mount - template
- #开头的,用idToTemplate解析
- 也可以是元素节点,就取innerHTML
- el代表宿主元素,例如#app
- 找到template后,传入compileToFunctions,获取render函数
- render优先级是最高的,有定义render就直接执行
- 执行初始化时
mount变量存储的那个mountComponent方法
// entry-runtime-with-compiler.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
...
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
...
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
...
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
...
}
}
return mount.call(this, el, hydrating)
}
createCompilerCreator
这个方法做了三件事:
- parse:获取ast抽象语法树,parse(template)
- optimize:给静态节点打上标记
- generate:生成render函数
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
先写个测试文件
<body>
<div id="app">
<h1>
<span>sss</span>
</h1>
<p>{{obj.cur}}</p>
<button @click="handleClick">编辑</button>
</div>
<script src="../dist/vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
obj: {
cur: 1
}
},
methods: {
handleClick() {
this.obj.cur ++
}
}
})
</script>
</body>
ast就是类似这样的
<h1>
<span>sss</span>
</h1>
这样的标签被认为是可优化的,会在h1节点上添加两个属性
- staticRoot:true,标识这是一个静态的根节点
- static: true,标识这是个静态节点
span标签只会被标记一个static:true 假如标签是这样
<h1>sss</h1>
就不会被标记,可能觉得这个优化点太小了 最终的由generate生成的code中的render是这样的:
"with(this){return _c('div',{attrs:{"id":"app"}},[_m(0),_v(" "),_c('p',[_v(_s(obj.foo))]),_v(" "),_c('button',{on:{"click":handleClick}},[_v("编辑")])])}"
挂载render函数
- 调用
createFunction,new Function(render),创建了一个return执行render函数的函数 - 在实例的
$options上挂载了render和staticRenderFns
mountComponent 开始挂载
接下来就是触发callHook(vm,'beforeMount')了,这里可以稍作总结下:所有的数据都响应式,render函数也挂载到了组件实例上,最具代表性的vnode还没有出现,下一步肯定就是要生成vnode了
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
...
callHook(vm, 'beforeMount')
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
...
} else {
// 这里就是之前依赖收集提到的组件级别的update方法
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 组件级别的watcher
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
watcher里保留着后续更新的方法:
- 组件实例vm,某个dep变化了,就可以知道要更新哪个组件
- deps保留着该watcher相关联的dep,属性变化,执行get,就是updateComponent,与依赖收集那里就接上了
- updateComponent,这个分两步
- 执行回调vm._render(),的到vnode,挂载到实例上
- 调用_update里的的
__patch__
new Watcher()后,会立即调用watcher的get方法,就是上边的updateComponent
执行render函数,得到vnode
render,就是上边那个with开头的闭包函数,得到vnode,这里又涉及到依赖收集:
- render中对变量的访问,会触发数据拦截的get方法,接着往watcher添加dep,dep添加watcher,在此刻看来dep和watcher是一对一的关系,但是一旦用户添加多个watcher,甚至组件的内部组件又引用到parent的属性,那就变成多对多了...
- 属性中又有子属性,又在watcher中添加这个子的dep,子的dep也添加了这个watcher,如果是数组,又去递归添加子的子的dep...
个人认为执行render生成vnode,是一个由内而外的操作,比如:
_c('p',[_v(_s(obj.foo))])
- 先是访问obj.foo,就是触发以上的get操作,取到了值,收集了依赖,update会使用到
- 又对val做了_s转换即toString
- 执行更外层的_v,就是createTextVNode,这个详细可以看
render-helpers/index.js,这里只是最里层的text节点,到这已经生成了一个tag是undefined的text类型的vnode - 最后执行_c,就是initRender在实例上挂载的包着createElement的科里化函数,生成Vnode,new Vnode()过程会将子的vnode挂载到父的vnode的children上
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_m(0),_v(" "),_c('p',[_v(_s(obj.foo))]),_v(" "),_c('button',{on:{"click":handleClick}},[_v("编辑")])])}
})
以这段render语句为例,整个过程就是一层层地执行上边的步骤,最终生成一个类似这样的只有一个根的vnode:
纯手打,有错误还望指出,一起进步 源码调试地址