这是一篇很长又很详细(作者还咕了)的解析,所以需要笔记。
这真的只是笔记,即这这是给笔者个人使用的,而不是给其它人看的——这篇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数组存放依赖 -
提供一个
observeAPI,递归遍历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)
-
定义
getget: 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用在了这里。这个值在最后返回。注意这里往两个地方添加了依赖。
-
定义
setset: 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,记录添加过的depid。
计算属性的实现
计算属性在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是一个开关,这里关闭了。它影响observeexport 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内的值是非响应式的。但是,如果你传了一个响应式对象,那它还是响应式的。