这是一篇很长又很详细(作者还咕了)的解析,所以需要笔记。
这真的只是笔记,即这这是给笔者个人使用的,而不是给其它人看的——这篇blog就是以此为前提写成的。这意味着对于不小心走到这里的人,必须警告:
- 你需要看过原文才能看这篇笔记。
- 里面的内容是不连贯的,很容易看得一头雾水。如果你能看懂这篇文章,大概率你不需要看它。
- 由于这篇文章的边版本已经落后,所以里面的代码也很混乱。这确实会造成很大的问题。
- 不保证内容的正确性。
Mixin 和 extend 的实现?
vue
有一套合并策略。
-
首先,
vue
会把传入的option
格式化。 -
其次,再对
option
进行合并。什么时候会需要合并两个
option
呢?- 使用
extend
创建子类的时候 - 使用
mixin
的时候
- 使用
针对不同的属性,vue
有不同的合并策略。这些策略可以在config.optionMergeStrategies
里找到。用户甚至可以自定义或者修改合并策略。
mixin
和 extend
都会经过一个几乎相同的合并过程。mixin
会在初始化的时候处理。它在合并的时候多了一个循环操作。extend
则是一个继承过程。在继承的过程中extend
也会进行一次合并操作。
为什么 data
可以使用 prop
初始化?
data
的合并策略非常复杂。但是,它的返回值一定是一个函数。
在初始化时,props
和 inject
是先于 data
执行的,所以我们可以用 prop
初始化 data
数据。
默认的合并策略
- 对于
el
、propsData
选项使用默认的合并策略defaultStrat
。 - 对于
data
选项,使用mergeDataOrFn
函数进行处理,最终结果是data
选项将变成一个函数,且该函数的执行结果为真正的数据对象。 - 对于
生命周期钩子
选项,将合并成数组,使得父子选项中的钩子函数都能够被执行 - 对于
directives
、filters
以及components
等资源选项,父子选项将以原型链的形式被处理,正是因为这样我们才能够在任何地方都使用内置组件、指令等。 - 对于
watch
选项的合并处理,类似于生命周期钩子,如果父子选项都有相同的观测字段,将被合并为数组,这样观察者都将被执行。 - 对于
props
、methods
、inject
、computed
选项,父选项始终可用,但是子选项会覆盖同名的父选项字段。 - 对于
provide
选项,其合并策略使用与data
选项相同的mergeDataOrFn
函数。 - 最后,以上没有提及到的选项都将使默认选项
defaultStrat
。 - 最最后,默认合并策略函数
defaultStrat
的策略是:只要子选项不是undefined
就使用子选项,否则使用父选项。
为何可以不通过显示声明就使用 keep-alive
等内置组件?
component
, directive
, filter
这三者被称为资源(assets
)。它们是通过 prototype
进行合并的。
Vue.options = { components: { KeepAlive // Transition 和 TransitionGroup 组件在 runtime/index.js 文件中被添加 // Transition, // TransitionGroup }, directives: Object.create(null), // 在 runtime/index.js 文件中,为 directives 添加了两个平台化的指令 model 和 show // directives:{ // model, // show // }, filters: Object.create(null), _base: Vue }
在Vue Option中设置自定义属性有效吗?
你可以在 vm.$options 上拿到自定义的属性。甚至可以自定义optionMergeStrategies
Vue的作用域
模版作用域
根据Render.js
中的vnode = render.call(vm._renderProxy, vm.$createElement)
,渲染模版时,作用域是vm._renderProxy
vm._renerProxy
在生产环境就是vm
,在开发环境中,绝大部分情况下是一个proxy
。这个proxy
进行了一次拦截,如果访问一个不存在的属性,会打印警告。
为什么Template可以不写 this
就访问自身的属性和方法?
首先解释一下这个问题。首先,我们知道 template
会被编译,然后 template
上的属性是从 vm
中获取的。一般来说,我们想要访问到属性至少要写成 vm.key
才行,但是我们在 template
中从来没有这样做过。
vue
在编译时,会对函数用 with
进行包裹,这样就不用写 this
了。编译后的render
大约长这样:
vm.$options.render = function () { // render 函数的 this 指向实例的 _renderProxy with(this){ return _c('div', [_v(_s(a))]) // 在这里访问 a,相当于访问 vm._renderProxy.a } }
注意:如果是手写 render
函数,自然是不会包这一层 width
的。但是,我们在手写时也是不会省略 this
的。
关于 with
,尤雨溪做过解释:
大概就是这个意思:vue并没有对模板中的javascript表达式进行ast语法分析,如果要移除with,就需要对javascript表达式进行ast语法分析,并且还需要一个专门的解释器对ast语法树进行解释,这样就会导致存在两个并行的解析器,这样维护成本高,还可能会有潜在的bug风险。所以呢,作者并没有想做这个事情,麻烦费力不讨好
引自https://segmentfault.com/q/1010000018552495 欢喜哥的回答
hook 中的作用域
hook调用使用 callHook(vm, hookname)
的方式。其原理是把格式化成数组的生命周期函数依次调用,同时做了处理防止重复收集依赖。
调用时的代码如下:handlers[i].call(vm)
,指定了 vm
为作用域。
nextTick 中的作用域
export function renderMixin (Vue: Class<Component>) { // 省略... Vue.prototype.$nextTick = function (fn: Function) { return nextTick(fn, this) } // 省略... }
export function nextTick (cb?: Function, ctx?: Object) { // 省略 try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } // 省略 }
nextTick
可以指定 cb
的作用域, $nextTick
强制指定为 this
,即 vm
Vue 如何获取 parent?
core/instance/lifecycle.js
中有一个变量:export let activeInstance: any = null
。该变量总是保存着当前正在渲染的实例的引用。- 在生成子组件实例时,调用
core/vdom/create-component.js
中的createComponentInstanceForVnode
方法,该方法有形参parent
core/vdom/create-component.js
中,componentVNodeHooks
的init
钩子函数,调用了createComponentInstanceForVnode
,parent
传值为activeInstance
- 如果从
options
中获取了parent
,且组件不是抽象组件,就会上探到第一个不是抽象组件的parent
,赋值给$parent
综上,vue
中有一个变量记录当前渲染的实例,该变量会在渲染子组件的时候传入。
hook 的调用顺序
beforeCreate, created
callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) // 包括 initProps、initMethods、initData、initComputed 以及 initWatch initProvide(vm) // resolve provide after data/props callHook(vm, 'created')
当 beforeCreate
钩子被调用时,所有与 props
、methods
、data
、computed
以及 watch
相关的内容都不能使用,当然了 inject/provide
也是不可用的。
在 created
钩子中,是完全能够使用以上提到的内容的。但由于此时还没有任何挂载的操作,所以在 created
中是不能访问DOM的,即不能访问 $el
。
data的代理
vue在格式化时会把data
格式化为函数,在initData
时会调用,并把返回值挂在vm
上。这就是vm._data
调用时会做处理,避免收集冗余依赖。
然后,vue
会遍历每一个key
值,并代理到_data
之上。
优先级:prop
> method
> data
同时,以 _
开头 或者以 $
开头的属性不会被代理
最后,会调用observe
将数据转换成响应式的
代理的方法
defineProperty
响应式原理
最基本的响应式思路
-
需要一个
deps
数组存放依赖 -
提供一个
observe
API,递归遍历obj
的属性,对其定义defineProperty
中的get
和set
-
set
的时候触发所有deps
的依赖 -
使用一个全部变量
target
指向当前依赖,在get
的时候把target
推入deps
-
给出一个
watch
函数,该函数接受key
和依赖。调用时把target
设置成传入的依赖。接着,访问变量 -
由于触发了
getter
,会将target
推入deps
-
将
watch
的第一个入参从key
修改为render
,执行render
方法。render中一旦触发setter
则会收集到依赖 -
watch(render, render)
。这样,收集到的依赖改变时就会重新触发render
-
将
deps
修改成一个Map
-
避免重复收集依赖。
Observer
Observer
并不复杂,它基本结构如下:
class Observer { value: any dep: Dep vmCount: number constructor(value: any) {} walk(obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } observeArray(itms: any[]) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }
observe
工厂函数
这个函数是Observer
的watch dog
。它判断对象是否需要进行观测。并且,如果asRootData
为true
,使 Observer
的vmCount
+1
即:如果__ob__
的vmCount
为 0
,它不是 rootData
全局还有一个开关 shouldObserve
,如果为 false
也不会执行观测
构造函数
constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) } }
Observer
在构造函数中,会给在被观测的对象(val)上挂上__ob__
,指向自身(this
,Observer
实例)- 同时,它的
value
指向被观测的对象,这是一个循环引用 - 接着,它判断观察的对象是否是
Array
,如果是则执行observeArray
,否则为每一个键值调用defineReactive
。
const data = { a: 1 } observe(data) // 假设应该观测 data /** * { * a: 1, * __ob__: { * vmCount: 0, * dep: Dep实例对象, * value: data * } * } */
defineReactive
这个函数里,我们可以看到熟悉的代码。
-
defineReactive(obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: Boolean)
-
创建一个
dep
实例。 -
获取
obj.key
的property
的get
和set
-
判断是否需要自己获取值。
if ((!getter || setter) && arguments.length === 2) { val = obj[key] }
这个判断条件有点复杂。比较好解释的部分是,当
arguments.length === 2
时,说明没有传入val
,那么自然需要手动获取。!getter
的部分也比较好理解。如果有getter
,应该调用getter
获得val
。结合第五点的代码,由于此时 val 为空,所以也就不会深度观测。现在看最难理解的
setter
。试想一种情况:我们本来有一个定义了setter
的对象,此时不会深度观测该属性。但是,经过defineReactive
的处理后,它被赋予了新的getter
和setter
。同时,
setter
中存在这样一行代码:childOb = !shallow && observe(newVal)
只要触发了
setter
,就会进行观测。这显然是不行的。所以,这里添加了处理,即使有
getter
,如果定义了setter
也要观测。个人认为这实在是一个苦涩的选择……
-
如果需要深层观测,那么递归调用
observe(val)
let childOb = !shallow && observe(val)
这里把 __ob__
赋值给 childOb
了,接下来还有用。
不需要深层观测的情况其实已经见过了,就是 $attrs
和 $linsteners
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true) // 最后一个参数 shallow 为 true defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
-
定义
get
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 },
这里先看了下有没有
getter
,如果有则调用获得val
。第3步获得的get
用在了这里。这个值在最后返回。注意这里往两个地方添加了依赖。
-
定义
set
set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val if (newVal === value || (newVal !== newVal && value !== value)) { return } if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() }
视情况把新值变成响应式对象,然后触发响应式。比
get
好理解很多
为什么有两个 Dep
实例?Vue.set 的实现
我们可以看到有两个实例。第一个放在响应式属性的 Observer
实例当中,第二个放在则在 get
和 set
中,用闭包储存其引用。在 get
中,我们发现它在两个 Dep
实例中都收集了依赖。这是为什么?
答案是触发的时机不同。definedProperty
虽然可以拦截赋值操作,却无法拦截对象新增属性的操作。赋值时的操作使用 set
就可以处理,但是新增属性就需要手动处理。
vue
为新增属性提供了API
: Vue.set
/$set
, 现在让我们看它们的实现。
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)}`) } // 数组处理(数组本身是响应式的,处理起来比较简单) if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) target.splice(key, 1, val) return val } // 如果已经是 target 的属性,简单赋值即可 if (key in target && !(key in Object.prototype)) { target[key] = val return val } // 否则,抓取__ob__ const ob = (target: any).__ob__ 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 } // 非响应对象,简单改变它的值 if (!ob) { target[key] = val return val } // 定义响应属性,注意这里给了第三个属性 val defineReactive(ob.value, key, val) // 触发 __ob__ 的依赖 ob.dep.notify() return val }
在 compute watcher 中,还存在第三个
dep
数组的处理
// src/core/observer/index.js /** * Augment an target Object or Array by intercepting * the prototype chain using __proto__ */ function protoAugment (target, src: Object, keys: any) { target.__proto__ = src } // src/core/observer/array.js /* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype */ import { def } from '../util/index' 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] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) 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.dep.notify() return result }) })
上面的代码中漏掉了实际调用部分。这部分代码在 Observer
的constructor
里。不过我们已经可以看出端倪:这不就是hack
了Array
的prototype
,然后用__proto__
进行链接嘛!
这时候,我们发现了__ob__
的第二个用处:用来进行数组的响应式更新。同时,我们发现Vue
处理了增加的属性,将它们也变成响应式的。
对于不支持__proto__
的情况,使用如下代码:
// 对比 protoAugment: target.__proto__ = src 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]) } }
在 defineReactive
中,也有一段针对数组的逻辑:
// ... if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } // ...
按照现在的依赖收集方案,数组中的对象的__ob__.dep
是不会收集到数组的依赖的。dependArray
就是为了处理这个问题,否则set
数组中的对象将无法触发更新。
function dependArray (value: Array<any>) { for (let e, i = 0, l = value.length; i < l; i++) { e = value[i] e && e.__ob__ && e.__ob__.dep.depend() if (Array.isArray(e)) { dependArray(e) } } }
为什么对象不需要这样处理?这是因为数组的索引是非响应式的。回想一下 Observer
,数组没有定义 get
和 set
。所以,访问数组索引不会触发 set
搜集依赖,于是只能通过 dependArray
一次性搜集了。
思考一下,对于
a.b.c.d
这样的访问,a
,b
,c
,d
的get
都会被触发。这意味着,访问d
的时候,a
,b
,c
的依赖都会被搜集。现在,让我们看看
a.b[0].d
,那么数组索引是没有get
的,这会导致访问d
的时候,b[0]
的依赖没有搜集到,也就不会触发更新。现在,让我们看上面那段代码的作用。
a.b
的get
发现a.b
是一个数组,于是执行dependArray
,于是d
也可以搜集到a.b
的依赖。当d
变化时,会触发视图更新。
总结
observe
干了什么?它实现了 Vue.set
,以及对 array
的监听——换句话说,如果监听的内容一定不是 Array
,且不允许 set
,那么可以不使用 observe
,直接使用 defineReactive
。
那么,有这种情况吗?答案是有的。对于 $prop
,就可以直接用defineReactive
。
Watcher
寻找入口
依赖收集源自mount
时从变量取值获取触发get
,mount
会调用packages/weex-vue-framework/factory.js
的mountComponent
方法。
mount
不同平台mount
方法不同。对于不需要编译器的版本,它只是简单地这样调用 mountComponent(this, el, hydrating)
完整版本的mount
会确保拿到template
。对于传递了el
的对象,会使用outerHTML
作为template
的值。如果template
传递了id
或者HTMLElement
,也会做对应的处理。其中id
有一个缓存机制。
之后,它会通过compileToFunctions
函数生成render
。这个函数以后再进行叙述。
最终,它依然调用mountComponent
mountComponent
挺长的,好在校验、性能观测等代码占了不少部分。省略后,大致逻辑如下:
function mountComponent ( vm, el, hydrating ) { vm.$el = el; if (!vm.$options.render) { vm.$options.render = createEmptyVNode; // 省略警告 } callHook(vm, 'beforeMount'); var updateComponent; /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { // 省略性能观测部分代码 } else { updateComponent = function () { vm._update(vm._render(), hydrating); }; } // 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: function 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 }
这样看来,这个函数做的事情其实不多。它触发了beforeMount
, beforeUpdate
, mounted
生命周期函数,生成watcher
, 使用render
进行挂载。
这个watcher
没有用变量储存。我们有理由相信,它实际上被挂在了vm
上。
-
vm.$el
问题。vm.$el
始终指向根元素,第一行的代码显然是无法实现这一点的。其实这里仅仅是暂时赋值而已,这是为了给虚拟DOM的patch
算法使用的,实际上vm.$el
会被patch
算法的返回值重写。这会在调用Vue.prototype._update
触发。 -
可以粗略地认为:
vm._render
函数的作用是调用vm.$options.render
函数并返回生成的虚拟节点(vnode
)vm._update
函数的作用是把vm._render
函数生成的虚拟节点渲染成真正的DOM
watcher
这个Class
比较复杂。
export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, // 要观察的表达式 cb: Function, // 当被观察的表达式的值变化时的回调函数 options?: ?Object, // 些传递给当前观察者对象的选项 isRenderWatcher?: boolean // 用来标识该观察者实例是否是渲染函数的观察者。 ) { } get () { // 省略... } addDep (dep: Dep) { // 省略... } cleanupDeps () { // 省略... } update () { // 省略... } run () { // 省略... } evaluate () { // 省略... } depend () { // 省略... } teardown () { // 省略... } }
constructor
if (options) { this.deep = !!options.deep this.user = !!options.user // 用来标识当前观察者实例对象是 开发者定义的 还是 内部定义的 this.computed = !!options.computed // 用来标识当前观察者实例对象是否是计算属性的观察者 this.sync = !!options.sync // 用来告诉观察者当数据变化时是否同步求值并执行回调 this.before = options.before // 当数据变化之后,触发更新之前,调用在创建渲染函数的观察者实例对象时传递的 before 选项。 } else { this.deep = this.user = this.computed = this.sync = false }
关于 before
,回想一下 mountComponent
时传了啥
new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)
其它属性:
this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.computed // for computed watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : ''
deps
, depIds
和 newDeps
, newDepIds
用来避免搜集重复依赖。
expOrFn
可能是表达式或者 fn
。表达式最终会被格式化成 function
。然后,不管是表达式生成的函数,还是原本就传入的函数,都会赋值给this.getter
。它们会在 this.get
里使用。
// parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } if (this.computed) { this.value = undefined this.dep = new Dep() } else { this.value = this.get() }
computed
是惰性求值。另外,我们在这里发现了第三种dep
get 与依赖搜集
get () { pushTarget(this) // 给 Dep.target 赋值 let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value }
pushTarget
会给 Dep.target
赋值。那之后,执行 getter
。这会触发访问的属性的 get
,从而搜集到依赖。
targetStack是一个栈,推出一次之后会变成上一次的target
现在,让我们看看dep.depend()
的代码
depend () { if (Dep.target) { Dep.target.addDep(this) } }
这时我们看到,依赖搜集实际上是 watcher
搜集dep
!
// Watcher.addDep 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) } } }
addDep
通过 dep
的id
避免重复搜集依赖。重复搜集依赖的情况很简单:当一个模版使用了某个变量两次,就有可能搜集到重复的依赖。
一次求值后,两个newDep
变量的值和两个dep
变量的值会互换,之后newDep
变量的值会被清空。就是说,dep
变量里存着的其实是上一次搜集到的变量。
如果之前一直没搜集到这个依赖,就会将依赖加入sub
之中。
newDep
在一次get
中去重,dep
用于在两次get
中去重。其目的是避免重复求值时收集重复的观察者。
在get
的最后,置空target
,清空newDep
系列变量。
cleanupDeps () { let i = this.deps.length // 清除废弃的观察者 while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 }
dep
变量还被用来清除废弃的观察者
teardown
export default class Watcher { // 省略... /** * Remove self from all dependencies' subscriber list. */ teardown () { if (this.active) { // remove self from vm's watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } } }
首先,在对应vm
的_watchers
里删除自身;再将自身的deps
中,依次删除自身;最后,将active
改成false
Dep, sub, 与触发
先看一下 Dep
export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { 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) } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
depend
是搜集依赖的容器。sub
自然是存放容器的地方。而notify
,我们可以发现它只是依次触发watcher
的update
方法而已。
// Watcher v2.5 update () { /* istanbul ignore else */ if (this.computed) { // A computed property watcher has two modes: lazy and activated. // It initializes as lazy by default, and only becomes activated when // it is depended on by at least one subscriber, which is typically // another computed property or a component's render function. if (this.dep.subs.length === 0) { // In lazy mode, we don't want to perform computations until necessary, // so we simply mark the watcher as dirty. The actual computation is // performed just-in-time in this.evaluate() when the computed property // is accessed. this.dirty = true } else { // In activated mode, we want to proactively perform the computation // but only notify our subscribers when the value has indeed changed. this.getAndInvoke(() => { this.dep.notify() }) } } else if (this.sync) { this.run() } else { queueWatcher(this) } }
computed
的实现另外再说。run
执行更新,queueWatcher
是更新队列,也稍后再说,目前聚焦在如何更新上。
于是,我们看到run
run () { if (this.active) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value if (this.user) { const info = `callback for watcher "${this.expression}"` invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info) } else { this.cb.call(this.vm, value, oldValue) } } } }
首先,第一句我们发现它重新求值了。这意味着重新搜集依赖。对于组件的watcher
,getter
就是updateComponent
,也意味着组件的更新。
由于组件的watcher
返回值是 undefined
,所以永远都不会触发下面的if
。不过,用户定义的则有可能触发,当值发生变化,或者被观察的是一个object
,则触发cb
——还记得组件watcher
的cb
是noop
吗?
总结
让我们想一想创建一个watcher
意味着什么?
watcher
的构造函数会调用传入的expOrFn
。对于组件,expOrFn
就是render
函数;求值之前,会先把Dep.target
指向自身。然后,求值过程中会触发getter
,这时候,getter
会搜集到Dep.target
为依赖。
也就是说,创建watcher
意味着访问到的,响应式属性会搜集到被创建的watcher
为依赖。
对computed
,情况有所不同。可以看到constructor
中,不会对computed
求值。
异步队列
scheduler
blog
里没有把scheduler
单独拿出来讲,我觉得是不太应该的。因为在异步触发watcher
更新的时候,调度的相关代码全在这个文件里面。
首先先让我们说一说queue
,这玩意容易和callbacks
搞混。callbacks
和queue
都是全局变量,queue
装着一次更新会用到的watcher
。
它在触发更新的时候,如果没有设置同步更新,就把watcher
推入queue
。过程为:触发set
,触发dep
的notify
,触发watcher
的queueWatcher
,推入。queueWatcher
也会把flushSchedulerQueue
推入callbacks
,在下一个tick
执行。
queueWatcher
queueWatcher
从命名就可以猜到大概的意思。当我们触发watcher
的update
函数时,会把watcher
推入一个queue
,避免重复重渲染。
这个qeueu
在src/core/observer/scheduler.js
里,是一个全局变量。
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } nextTick(flushSchedulerQueue) } } }
flushing
在queue
中的watcher
开始更新的时候会被设置为true
。所以,如果还没有开始update,就只是简单地推入队列。
waiting
保证了nextTick
只执行一次。
flushSchedulerQueue
queueWatcher
只是把watcher
入队,没有执行。执行是在nextTick
这一步骤。
/** * Flush both queues and run the watchers. */ function flushSchedulerQueue () { currentFlushTimestamp = getNow() flushing = true let watcher, id // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child) // 2. A component's user watchers are run before its render watcher (because // user watchers are created before the render watcher) // 3. If a component is destroyed during a parent component's watcher run, // its watchers can be skipped. queue.sort((a, b) => a.id - b.id) // do not cache length because more watchers might be pushed // as we run existing watchers for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { watcher.before() } id = watcher.id has[id] = null watcher.run() // 省略在dev环境,检测无限循环的代码 } // keep copies of post queues before resetting state const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() resetSchedulerState() // 重置状态。此时queue清空 // call component updated and activated hooks callActivatedHooks(activatedQueue) // 触发 active Hook callUpdatedHooks(updatedQueue) // 对符合条件的VM触发 updated Hook. watcher上留存的 vm 引用在这里使用。 // 省略 devtool 相关代码 }
执行watcher
时,会首先根据id
排序。然后调用 before
和 run
。接着,触发 hook
nextTick
$nextTick
即是对nextTick
的包装。
宏任务之间可能会穿插UI重渲染。使用微任务完成数据更新,然后在下一次重渲染完成全部的DOM更新是较好的策略。因此,nextTick
实现宏任务是优于微任务的。
vue会先尝试使用promise
,如果不支持再降级为setTimeout
先看下callbacks
。之前说了,这个有点容易和queue
搞混。callbacks
在调用nextTick
时推入,nextTick
则由queueWatcher
调用。它在callbacks
中推入flushSchedulerQueue
。flushSchedulerQueue
在异步执行时会先给watcher
们根据id
排序,然后触发更新,最后清空queue
export let isUsingMicroTask = false // cb 队列 const callbacks = [] let pending = false // 触发所有cb function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } let timerFunc // ...
if (typeof Promise !== 'undefined' && isNative(Promise)) { // 首先,优先使用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) } }
MicroTask 默认使用 Promise
,如果不支持则降级为 MutationObserver
,再不支持则降级为 MacroTask
MacroTask 默认支持 setImmediate
,如果不支持则降级为 setTimeout
。
timerFunc
即在下一次tick
执行callbacks
内的所有回调,并清空callbacks
。一旦执行,说明这一次tick
就结束了。我们可以发现有一个专门的变量pending
防止在清空的过程中增加回调。
Vue 曾经使用 MessageChannel
作为备选方案,但是因为这造成了一些移动端问题而改掉了。 In Vue 2.5 ,the use of MessageChannel in nextTick function will lead to audio can not play in some mobile browsers. #710
export function nextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { if (cb) { try { 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') { return new Promise(resolve => { _resolve = resolve }) } }
调用 nextTick
后,会往 callbacks
中推一个函数。之后,如果pading
为负,把pading
设置为true
,再执行timerFunc
。
flushCallbacks
会把 pending
重新设置回false
。如果padding
为正,说明下一个tick
即将清空回调,自然就不用再设置一个tick
来清空回调了。
function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } }
当没有传回调的时候,则返回一个promise
。这个promise
的resolv
,在包装的cb
里闭包保存,在cb
执行的时候决议。
一次更新过程
一次更新我们需要关注三个队列:queue
, callbacks
, 任务队列。
created () { this.name = 'HcySunYang' this.$nextTick(() => { this.name = 'hcy' this.$nextTick(() => { console.log('第二个 $nextTick') }) }) }
由于修改了name
,依赖的watcher
将被推入queue
之中。queueWatcher
将调用nextTick(flushSchedulerQueue)
。此时:
callbacks = [ flushSchedulerQueue // queue = [renderWatcher] ]
同时会将 flushCallbacks
函数注册为 microtask
,所以此时 microtask
队列如下:
// microtask 队列 [ flushCallbacks ]
接下来调用第一个$nextTick
,cb
被推入callbacks
callbacks = [ flushSchedulerQueue, // queue = [renderWatcher] () => { this.name = 'hcy' this.$nextTick(() => { console.log('第二个 $nextTick') }) } ]
调用栈执行完毕,开始进行第一次tick
首先会执行 flushSchedulerQueue
函数,这个函数会遍历 queue
中的所有观察者并重新求值,完成重新渲染(re-render
),在完成渲染之后,本次更新队列已经清空,queue
会被重置为空数组,一切状态还原。
然后,执行第二个cb
,又改变了name
。此时,由于在执行 flushCallbacks
函数时优先将 pending
的重置为 false
,所以 nextTick
函数会将 flushCallbacks
函数注册为一个新的 microtask
,此时 microtask
队列将包含两个 flushCallbacks
函数:
// microtask 队列 [ flushCallbacks, // 第一个 flushCallbacks flushCallbacks // 第二个 flushCallbacks ]
而另外除了将变量 pending
的值重置为 false
之外,我们要知道第一个 flushCallbacks
函数遍历的并不是 callbacks
本身,而是它的复制品 copies
数组,并且在第一个 flushCallbacks
函数的一开头就清空了 callbacks
数组本身。所以第二个 flushCallbacks
函数的一切流程与第一个 flushCallbacks
是完全相同。
watcher 的实现
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {} options.user = true const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { cb.call(vm, watcher.value) } return function unwatchFn () { watcher.teardown() } }
function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) { if (isPlainObject(handler)) { options = handler handler = handler.handler } if (typeof handler === 'string') { handler = vm[handler] } return vm.$watch(expOrFn, handler, options) }
基本上就是新建一个watcher
实例。其中vm
取this
。如果immediate
为true
则立刻执行一次。
对于创建vue
实例时的watch
选项,在initState
中的initWatch
初始化。
function initWatch (vm: Component, watch: Object) { for (const key in watch) { const handler = watch[key] if (Array.isArray(handler)) { for (let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]) } } else { createWatcher(vm, key, handler) } } }
对于需要深度观察的对象,则递归访问它的属性,使其可以搜集到依赖。当然,__ob__.dep
是手动推进去的。
为了避免循环引用,有一个seen
set,记录添加过的dep
id。
计算属性的实现
计算属性在initState
中的initComputed
初始化。它同样依赖watcher
,但是由于其惰性求值的特点,搜集依赖的方法有所不同。
惰性求值:
computed
不会在渲染时求值,而是在用到时求值。即:如果没有用到,那么computed
是不会求值的。相对的,data
在渲染时一定会访问一次。
const computedWatcherOptions = { lazy: true } function initComputed (vm: Component, computed: Object) { // $flow-disable-line const watchers = vm._computedWatchers = Object.create(null) // computed properties are just getters during SSR const isSSR = isServerRendering() for (const key in computed) { const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get // 省略报错 if (!isSSR) { // create internal watcher for the computed property. watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions // { lazy: true } ) } // 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') { // 省略报错 } } }
_computedWatchers
存放了所有computed
相关watcher
我们可以看到它一样创建了watcher
. 但是watcher
的第二个参数不是update
,而是getter
!
换句话说,当搜集到依赖的属性发生变化时,只会触发重新计算而已。那么,它是怎么更新页面的呢?
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop } // ... export function defineComputed ( target: any, key: string, userDef: Object | Function ) { const shouldCache = !isServerRendering() if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef) sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get) : noop sharedPropertyDefinition.set = userDef.set || noop } if (process.env.NODE_ENV !== 'production' && sharedPropertyDefinition.set === noop) { sharedPropertyDefinition.set = function () { warn( `Computed property "${key}" was assigned to but it has no setter.`, this ) } } Object.defineProperty(target, key, sharedPropertyDefinition) }
从这里,我们可以看出,defineComputed
主要作用是赋予属性getter
。我们可以大致猜到,这必然和依赖搜集有关。同时,我们还发现,在游览器环境,computed会缓存。而主要逻辑都在createComputedGetter
和createGetterInvoker
之中。
function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { watcher.depend() return watcher.evaluate() } } } // 只是简单执行一次 function createGetterInvoker(fn) { return function computedGetter () { return fn.call(this, this) } }
渲染时,实际执行的是 watcher.depend
方法。
class Watcher { // ... constructor(/* ... */) { // ... if (this.computed) { this.value = undefined this.dep = new Dep() } else { this.value = this.get() } } // ... depend () { if (this.dep && Dep.target) { this.dep.depend() } // ... evaluate () { if (this.dirty) { this.value = this.get() this.dirty = false } return this.value } }
当创建 computed watcher 时,不会求值(也就不会渲染),而是创建了一个dep
注:在2.6版本中,已经没有该dep了
这就是说,对于普通的属性,创建watcher
时即完成求值过程;对于计算属性,则是惰性求值的。
然后,创建 watcher 时,调用了 watcher 的 depend 方法,dep 搜集了依赖。那么,此时 Dep.target
到底指向哪里呢?
这个过程中的关键一步就是渲染函数的执行,我们知道在渲染函数执行之前 Dep.target
的值必然是 渲染函数的观察者对象。所以计算属性观察者对象的 this.dep
属性中所收集的就是渲染函数的观察者对象。
这里存疑。如果没有触发 get ,那么是在哪里设置
Dep.target
的呢?个人的想法是,由于初始化
computed
的时候,一定是在创建vm
的时候,此时的Dep.target
应该指向vm
的watcher
,也就是上文所说的渲染函数的观察者对象。由此,触发this.dep.notify
时,将会重新渲染。
接下来,执行watcher.evaluate
。这会触发computed
的计算。此时,调用了watcher.get
,意味着Dep.target
已经指向了computed watcher。这时候,访问到的响应式属性的 get
都搜集到了这个依赖。
现在看看改变 computed 依赖的属性会发生什么。首先,这会导致 computed watcher 进行一次更新,调用 watcher
的 update
方法。此时,触发 watcher 的 dep 的 notify 事件,于是重新渲染。vue
会比较两次的值,如果不同再重新渲染。
2.5.17 版本的computed
实现
不确定到底是哪个版本修改的,不过在这个版本中,多了lazy
属性,少了Watcher.dep
。这意味着computed
的实现发生了根本性变化。
function createComputedGetter(key) { return function() { // 获取到相应 key 的 computed-watcher var watcher = this._computedWatchers[key]; // 如果 computed 依赖的数据变化,dirty 会变成true,从而重新计算,然后更新缓存值 watcher.value if (watcher.dirty) { watcher.evaluate(); } // 这里是 月老computed 牵线的重点,让双方建立关系 if (Dep.target) { watcher.depend(); } return watcher.value } }
Watcher.prototype.evaluate = function() { this.value = this.get(); // 执行完更新函数之后,立即重置标志位 this.dirty = false; };
Watcher.prototype.update = function() { if (this.lazy) this.dirty = true; //....还有其他无关操作,已被省略 };
Watcher.prototype.depend = function() { var i = this.deps.length; while (i--) { // this.deps[i].depend(); dep.addSub(Dep.target) } };
-
页面更新,读取 computed 的时候,Dep.target 会设置为 页面 watcher。
-
computed 被读取,createComputedGetter 包装的函数触发,第一次会进行计算。 computed-watcher.evaluted 被调用,进而 computed-watcher.get 被调用,Dep.target 被设置为 computed-watcher,旧值 页面 watcher 被缓存起来。
-
computed 计算会读取 data,此时 data 就收集到 computed-watcher 同时 computed-watcher 也会保存到 data 的依赖收集器 dep(用于下一步)。 computed 计算完毕,释放Dep.target,并且Dep.target 恢复上一个watcher(页面watcher)
-
手动 watcher.depend, 让 data 再收集一次 Dep.target,于是 data 又收集到 恢复了的页面watcher
本质上来说,这是用computed
中包含的响应式属性值搜集到 render watcher ,代替了 watcher.dep 的功能。
总结
computed 需要实现三个功能:
-
访问computed的时候,再求值。
-
当其包含的响应式属性发生变化时,重新计算computed属性;
-
如果两次值不同重新渲染页面
惰性求值意味着我们不能在初始化的时候直接调用 computed。这就不能直接触发 watcher 的 get 方法——要知道,一旦调用将意味着求值。
所以,我们赋予 computed property get,这样,当它被访问的时候我们将可以触发一段逻辑。
在较早的版本里,我们使用一个单独的 dep
来储存 computed watcher。这样,我们可以在改变值时手动触发更新。流程大致为:
响应式属性 get -> 响应式属性 dep.notify -> computed watcher update -> computed watcher dep notify -> render watcher notify
。
在较新的版本里,用了更加简洁但是也更加难懂的方式。首先我们触发 watcher.get ,这样使用到的响应式属性就会搜集到 computed watcher;接着,由于 target 是一个栈,computed watcher 被弹出后,剩下的就是页面渲染的 watcher。我们把 target 推入 computed watcher 搜集到的依赖中,于是页面重渲染和 computed 更新的 watcher 就都被搜集到了。更新流程大致为:
响应式属性 get -> 响应式属性 dep.notify -> computed watcher update & render watcher update
Prop
function initProps (vm: Component, propsOptions: Object) { const propsData = vm.$options.propsData || {} const props = vm._props = {} // cache prop keys so that future props updates can iterate using Array // instead of dynamic object key enumeration. const keys = vm.$options._propKeys = [] const isRoot = !vm.$parent // root instance props should be converted if (!isRoot) { toggleObserving(false) } for (const key in propsOptions) { keys.push(key) const value = validateProp(key, propsOptions, propsData, vm) /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { // 省略警告 } else { defineReactive(props, key, value) } // static props are already proxied on the component's prototype // during Vue.extend(). We only need to proxy props defined at // instantiation here. if (!(key in vm)) { proxy(vm, `_props`, key) } } toggleObserving(true) }
-
toggleObserving
是一个开关,这里关闭了。它影响observe
export function observe (value: any, asRootData: ?boolean): Observer | void { // 省略... if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( // here !!! shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } // 省略... return ob }
关闭时,
observe
失效。由于prop
是用defineReactive
直接定义响应式的,所以vm._props
是响应式的。但是,observe
在深度遍历时,由于开关关闭,不会执行观测。这是由于
prop
几乎一定会传入响应式的。 -
prop validate
...好像没啥可说的
inject & provide
provide 会执行,然后挂在 vm._provide
上。inject
则会依次遍历,直到找到provide
。如果找不到,并有 default
就返回 default
inject
的值会用defineReactive
挂在vm
上,挂上去的时候会toggleObserving(false)
。因此,inject内的值是非响应式的。但是,如果你传了一个响应式对象,那它还是响应式的。