Vue 技术内幕

这是一篇很长又很详细(作者还咕了)的解析,所以需要笔记。

真的只是笔记,即这这是给笔者个人使用的,而不是给其它人看的——这篇blog就是以此为前提写成的。这意味着对于不小心走到这里的人,必须警告:

  1. 你需要看过原文才能看这篇笔记。
  2. 里面的内容是不连贯的,很容易看得一头雾水。如果你能看懂这篇文章,大概率你不需要看它。
  3. 由于这篇文章的边版本已经落后,所以里面的代码也很混乱。这确实会造成很大的问题。
  4. 不保证内容的正确性。

Mixin 和 extend 的实现?

vue 有一套合并策略。

  1. 首先,vue会把传入的option格式化。

  2. 其次,再对option进行合并。

    什么时候会需要合并两个option呢?

    1. 使用extend创建子类的时候
    2. 使用mixin的时候

针对不同的属性,vue有不同的合并策略。这些策略可以在config.optionMergeStrategies里找到。用户甚至可以自定义或者修改合并策略。

mixinextend 都会经过一个几乎相同的合并过程。mixin 会在初始化的时候处理。它在合并的时候多了一个循环操作。extend则是一个继承过程。在继承的过程中extend 也会进行一次合并操作。

为什么 data 可以使用 prop 初始化?

data的合并策略非常复杂。但是,它的返回值一定是一个函数。

在初始化时,propsinject 是先于 data 执行的,所以我们可以用 prop 初始化 data 数据。

默认的合并策略

  • 对于 elpropsData 选项使用默认的合并策略 defaultStrat
  • 对于 data 选项,使用 mergeDataOrFn 函数进行处理,最终结果是 data 选项将变成一个函数,且该函数的执行结果为真正的数据对象。
  • 对于 生命周期钩子 选项,将合并成数组,使得父子选项中的钩子函数都能够被执行
  • 对于 directivesfilters 以及 components 等资源选项,父子选项将以原型链的形式被处理,正是因为这样我们才能够在任何地方都使用内置组件、指令等。
  • 对于 watch 选项的合并处理,类似于生命周期钩子,如果父子选项都有相同的观测字段,将被合并为数组,这样观察者都将被执行。
  • 对于 propsmethodsinjectcomputed 选项,父选项始终可用,但是子选项会覆盖同名的父选项字段。
  • 对于 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 ,尤雨溪做过解释:

image

大概就是这个意思: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?

  1. core/instance/lifecycle.js 中有一个变量:export let activeInstance: any = null。该变量总是保存着当前正在渲染的实例的引用。
  2. 在生成子组件实例时,调用 core/vdom/create-component.js 中的 createComponentInstanceForVnode方法,该方法有形参parent
  3. core/vdom/create-component.js中,componentVNodeHooksinit 钩子函数,调用了 createComponentInstanceForVnodeparent 传值为 activeInstance
  4. 如果从 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 钩子被调用时,所有与 propsmethodsdatacomputed 以及 watch 相关的内容都不能使用,当然了 inject/provide 也是不可用的。

created 钩子中,是完全能够使用以上提到的内容的。但由于此时还没有任何挂载的操作,所以在 created 中是不能访问DOM的,即不能访问 $el

data的代理

vue在格式化时会把data格式化为函数,在initData 时会调用,并把返回值挂在vm 上。这就是vm._data

调用时会做处理,避免收集冗余依赖。

然后,vue会遍历每一个key值,并代理到_data之上。

优先级:prop > method > data

同时,以 _ 开头 或者以 $ 开头的属性不会被代理

最后,会调用observe将数据转换成响应式的

代理的方法

defineProperty

响应式原理

最基本的响应式思路

  1. 需要一个 deps 数组存放依赖

  2. 提供一个observe API,递归遍历obj的属性,对其定义defineProperty 中的 getset

  3. set的时候触发所有deps的依赖

  4. 使用一个全部变量target 指向当前依赖,在get的时候把target推入deps

  5. 给出一个watch 函数,该函数接受key和依赖。调用时把target 设置成传入的依赖。接着,访问变量

  6. 由于触发了getter,会将target推入deps

  7. watch的第一个入参从key修改为render,执行render方法。render中一旦触发setter则会收集到依赖

  8. watch(render, render)。这样,收集到的依赖改变时就会重新触发render

  9. deps修改成一个Map

  10. 避免重复收集依赖。

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工厂函数

这个函数是Observerwatch dog。它判断对象是否需要进行观测。并且,如果asRootDatatrue,使 ObservervmCount +1

即:如果__ob__vmCount0,它不是 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) } }
  1. Observer在构造函数中,会给在被观测的对象(val)上挂上__ob__,指向自身(this, Observer实例)
  2. 同时,它的value 指向被观测的对象,这是一个循环引用
  3. 接着,它判断观察的对象是否是Array,如果是则执行observeArray,否则为每一个键值调用defineReactive
const data = { a: 1 } observe(data) // 假设应该观测 data /** * { * a: 1, * __ob__: { * vmCount: 0, * dep: Dep实例对象, * value: data * } * } */

defineReactive

这个函数里,我们可以看到熟悉的代码。

  1. defineReactive(obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: Boolean)

  2. 创建一个dep实例。

  3. 获取obj.keypropertygetset

  4. 判断是否需要自己获取值。

    if ((!getter || setter) && arguments.length === 2) { val = obj[key] }

    这个判断条件有点复杂。比较好解释的部分是,当arguments.length === 2时,说明没有传入val,那么自然需要手动获取。

    !getter的部分也比较好理解。如果有 getter ,应该调用 getter 获得 val结合第五点的代码,由于此时 val 为空,所以也就不会深度观测。

    现在看最难理解的 setter 。试想一种情况:我们本来有一个定义了setter的对象,此时不会深度观测该属性。但是,经过defineReactive的处理后,它被赋予了新的gettersetter

    同时,setter中存在这样一行代码:childOb = !shallow && observe(newVal)

    只要触发了setter,就会进行观测。这显然是不行的。

    所以,这里添加了处理,即使有getter,如果定义了setter也要观测。

    个人认为这实在是一个苦涩的选择……

  5. 如果需要深层观测,那么递归调用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)
  1. 定义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用在了这里。这个值在最后返回。

    注意这里往两个地方添加了依赖。

  2. 定义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 实例当中,第二个放在则在 getset 中,用闭包储存其引用。在 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 }) })

上面的代码中漏掉了实际调用部分。这部分代码在 Observerconstructor里。不过我们已经可以看出端倪:这不就是hackArrayprototype,然后用__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,数组没有定义 getset 。所以,访问数组索引不会触发 set 搜集依赖,于是只能通过 dependArray 一次性搜集了。

思考一下,对于 a.b.c.d 这样的访问,a, b, c, dget都会被触发。这意味着,访问d的时候,a,b,c的依赖都会被搜集。

现在,让我们看看a.b[0].d,那么数组索引是没有 get的,这会导致访问d的时候,b[0]的依赖没有搜集到,也就不会触发更新。

现在,让我们看上面那段代码的作用。a.bget发现a.b是一个数组,于是执行dependArray,于是d也可以搜集到a.b的依赖。当d变化时,会触发视图更新。

总结

observe 干了什么?它实现了 Vue.set ,以及对 array 的监听——换句话说,如果监听的内容一定不是 Array,且不允许 set ,那么可以不使用 observe ,直接使用 defineReactive

那么,有这种情况吗?答案是有的。对于 $prop ,就可以直接用defineReactive

Watcher

寻找入口

依赖收集源自mount时从变量取值获取触发getmount会调用packages/weex-vue-framework/factory.jsmountComponent方法。

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上。

  1. vm.$el问题。vm.$el始终指向根元素,第一行的代码显然是无法实现这一点的。其实这里仅仅是暂时赋值而已,这是为了给虚拟DOM的 patch 算法使用的,实际上 vm.$el 会被 patch 算法的返回值重写。这会在调用Vue.prototype._update触发。

  2. 可以粗略地认为: 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, depIdsnewDeps, 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 通过 depid避免重复搜集依赖。重复搜集依赖的情况很简单:当一个模版使用了某个变量两次,就有可能搜集到重复的依赖。

一次求值后,两个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,我们可以发现它只是依次触发watcherupdate方法而已。

// 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) } } } }

首先,第一句我们发现它重新求值了。这意味着重新搜集依赖。对于组件的watchergetter就是updateComponent,也意味着组件的更新。

由于组件的watcher返回值是 undefined,所以永远都不会触发下面的if。不过,用户定义的则有可能触发,当值发生变化,或者被观察的是一个object,则触发cb——还记得组件watchercbnoop吗?

总结

让我们想一想创建一个watcher意味着什么?

watcher的构造函数会调用传入的expOrFn。对于组件,expOrFn就是render函数;求值之前,会先把Dep.target指向自身。然后,求值过程中会触发getter,这时候,getter会搜集到Dep.target为依赖。

也就是说,创建watcher意味着访问到的,响应式属性会搜集到被创建的watcher为依赖。

computed,情况有所不同。可以看到constructor中,不会对computed求值。

异步队列

scheduler

blog里没有把scheduler单独拿出来讲,我觉得是不太应该的。因为在异步触发watcher更新的时候,调度的相关代码全在这个文件里面。

首先先让我们说一说queue,这玩意容易和callbacks搞混。callbacksqueue都是全局变量,queue装着一次更新会用到的watcher

它在触发更新的时候,如果没有设置同步更新,就把watcher推入queue过程为:触发set,触发depnotify,触发watcherqueueWatcher,推入。queueWatcher也会把flushSchedulerQueue推入callbacks,在下一个tick执行。

queueWatcher

queueWatcher 从命名就可以猜到大概的意思。当我们触发watcherupdate函数时,会把watcher推入一个queue,避免重复重渲染。

这个qeueusrc/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) } } }

flushingqueue中的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排序。然后调用 beforerun。接着,触发 hook

nextTick

$nextTick即是对nextTick的包装。

宏任务之间可能会穿插UI重渲染。使用微任务完成数据更新,然后在下一次重渲染完成全部的DOM更新是较好的策略。因此,nextTick实现宏任务是优于微任务的。

vue会先尝试使用promise,如果不支持再降级为setTimeout

先看下callbacks。之前说了,这个有点容易和queue搞混。callbacks在调用nextTick时推入,nextTick则由queueWatcher调用。它在callbacks中推入flushSchedulerQueueflushSchedulerQueue在异步执行时会先给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。这个promiseresolv,在包装的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 ]

接下来调用第一个$nextTickcb被推入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实例。其中vmthis。如果immediatetrue则立刻执行一次。

对于创建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会缓存。而主要逻辑都在createComputedGettercreateGetterInvoker之中。

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应该指向vmwatcher,也就是上文所说的渲染函数的观察者对象。由此,触发this.dep.notify时,将会重新渲染。

接下来,执行watcher.evaluate。这会触发computed的计算。此时,调用了watcher.get,意味着Dep.target已经指向了computed watcher。这时候,访问到的响应式属性的 get 都搜集到了这个依赖。

现在看看改变 computed 依赖的属性会发生什么。首先,这会导致 computed watcher 进行一次更新,调用 watcherupdate方法。此时,触发 watcher 的 dep 的 notify 事件,于是重新渲染。vue 会比较两次的值,如果不同再重新渲染。

2.5.17 版本的computed实现

来自 【Vue原理】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) } };
  1. 页面更新,读取 computed 的时候,Dep.target 会设置为 页面 watcher。

  2. computed 被读取,createComputedGetter 包装的函数触发,第一次会进行计算。 computed-watcher.evaluted 被调用,进而 computed-watcher.get 被调用,Dep.target 被设置为 computed-watcher,旧值 页面 watcher 被缓存起来。

  3. computed 计算会读取 data,此时 data 就收集到 computed-watcher 同时 computed-watcher 也会保存到 data 的依赖收集器 dep(用于下一步)。 computed 计算完毕,释放Dep.target,并且Dep.target 恢复上一个watcher(页面watcher)

  4. 手动 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) }
  1. 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 几乎一定会传入响应式的。

  2. prop validate

    ...好像没啥可说的

inject & provide

provide 会执行,然后挂在 vm._provide 上。inject则会依次遍历,直到找到provide。如果找不到,并有 default 就返回 default

inject的值会用defineReactive挂在vm上,挂上去的时候会toggleObserving(false)。因此,inject内的值是非响应式的。但是,如果你传了一个响应式对象,那它还是响应式的。