关于javascript:大前端进阶读懂vuejs源码2

7次阅读

共计 10681 个字符,预计需要花费 27 分钟才能阅读完成。

前文中,曾经剖析了在 vuejs 源码中是如何定义 Vue 类,以及如何增加实例属性和静态方法:大数据进阶 - 读懂 vuejs 源码 1。

Vue 实例化时调用_init,本文将深刻该办法外部做了哪些事件及 vuejs 如何实现数据响应式。

Vue 初始化

core/instance/index.js 文件中定义了 Vue 的构造函数:

function Vue (options) {
  // 执行_init 办法,此办法在 initMixin 中定义
  this._init(options)
}

_init 办法定义在 core/instance/init.js 中:

Vue.prototype._init = function (options?: Object) {
    //。。。// 1. 合并 options
    if (options && options._isComponent) {
        // 此处有重要的事件做。initInternalComponent(vm, options)
    } else {
        vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),
            options || {},
            vm
        )
    }
    // 2. 初始化属性
    // 初始化 $root,$parent,$children
    initLifecycle(vm)
    // 初始化_events
    initEvents(vm)
    // 初始化 $slots/$scopedSlots/_c/$createElement/$attrs/$listeners
    initRender(vm)
    // 执行生命周期钩子
    callHook(vm, 'beforeCreate')
    // 注册 inject 成员到 vue 实例上
    initInjections(vm) // resolve injections before data/props
    // 初始化_props/methods/_data/computed/watch
    initState(vm)
    // 初始化_provided
    initProvide(vm) // resolve provide after data/props
    // 执行生命周期钩子
    callHook(vm, 'created')

    // 3. 调用 $mount 办法
    if (vm.$options.el) {vm.$mount(vm.$options.el)
    }
}

在合并 options 的时候,如果 options 示意一个组件(_isComponent)则调用了 initInternalComponent 函数:

export function initInternalComponent(vm: Component, options: InternalComponentOptions) {
    // 此处保留组件之间的父子关系,const parentVnode = options._parentVnode
    opts.parent = options.parent
    opts._parentVnode = parentVnode
    //...
}

此办法中设置了组件之间的父子关系,在后续的注册及渲染组件的时候会用到。

initProvide

定义在 core/instance/inject.js 文件中。

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

在下面的代码中能够看出,如果 provide 是一个函数,那么会调用这个函数,并将 this 指向 vm 实例。因为 initProvide 在_init 办法中最初被调用,因而可能拜访到实例的属性。

initInjections

定义在 core/instance/inject.js 文件中。

export function initInjections (vm: Component) {const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    // 遍历 result 属性,利用 Object.defineProperty 将其增加到 vue 实例上
    // ...
  }
}

此办法调用 resolveInject 办法获取所有 inject 值。

export function resolveInject(inject: any, vm: Component): ?Object {if (inject) {const result = Object.create(null)
        const keys = hasSymbol
            ? Reflect.ownKeys(inject)
            : Object.keys(inject)

        for (let i = 0; i < keys.length; i++) {
            // ....
            const provideKey = inject[key].from
            let source = vm
            while (source) {if (source._provided && hasOwn(source._provided, provideKey)) {result[key] = source._provided[provideKey]
                    break
                }
                source = source.$parent
            }
            // ...
        }
        return result
    }
}

在 resolveInject 办法中会从以后实例登程,延着 parent 始终向上找,直到找到_provided 中存在。

总结

此时整个 Vue 定义和初始化流程能够总结为如下:

数据响应式

vuejs 框架的整个数据响应式实现过程比较复杂,代码散落在各个文件中。咱们都晓得,在定义组件的时候,组件会主动将 data 属性中的数据增加上响应式监听,因而咱们从_init 办法中调用 initState 函数开始。

启动监听

在 initState 函数中:

export function initState (vm: Component) {
  // ...
  if (opts.data) {
    // 解决 data 数据
    initData(vm)
  } else {observe(vm._data = {}, true /* asRootData */)
  }
  // ...
}

options 中的 data 数据会交由 initData 办法解决:

function initData(vm: Component) {
    // ... 1. 获取 data 数据,如果 data 是一个函数,但没有返回值,会提醒谬误。// ... 2. 遍历 data 所有的属性,首先判断在 props 和 methods 是否同名,而后将其代理到 vue 实例上。// 3. 增加响应式数据监听
    observe(data, true /* asRootData */)
}

增加监听

observe 函数

定义在 core/observer/index.js 文件中:

export function observe (value: any, asRootData: ?boolean): Observer | void {if (!isObject(value) || value instanceof VNode) {return}
  let ob: Observer | void
  // 通过__ob__属性判断该属性是否增加过响应式监听
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    // 如果增加过,不做解决,间接返回
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 创立 Observer 实例,其为响应式的外围
    ob = new Observer(value)
  }
  // 通过 vmCount 能够判断某个响应式数据是否是根数据,能够了解为 data 属性返回的对象是根数据,如果 data 对象的某个属性也是一个对象,那么就不再是根数据。// vmCount 属性后续会被用到
  if (asRootData && ob) {ob.vmCount++}
  return ob
}

该办法的外围就是为 data 数据创立 Observer 实例 ob,ob 对象会为 data 增加 getter/setter 办法,其能够用来收集依赖并在变动的时候触发 dom 更新。

Observer 类

定义在 core/observer/index.js 文件中,在其构造函数中,依据传入 data 的类型(Array/Object),别离进行解决。

Object
constructor(value: any) {
    this.value = value
    // Observer 实例上蕴含 dep 属性,这个属性后续会有很大作用,有些无奈监听的数据变动能够由此属性实现
    this.dep = new Dep()
    this.vmCount = 0
    // 为 data 增加__ob__属性
    def(value, '__ob__', this)
    if (Array.isArray(value)) {// ... 解决数组} else {
        // 解决对象
        this.walk(value)
    }
}
  • walk

遍历 data 的所有属性,调用 defineReactive 函数增加 getter/setter。

walk(obj: Object) {const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
        // 增加数据拦挡
        defineReactive(obj, keys[i])
    }
}
  • defineReactive

数据响应式实现的外围办法,原理是通过 Object.defineProperty 为 data 增加 getter/setter 拦挡,在拦挡中实现依赖收集和触发更新。

export function defineReactive(
    obj: Object,
    key: string,
    val: any,
    customSetter?: ?Function,
    shallow?: boolean
) {
    // 1. 创立闭包作用域内的 Dep 对象,用于收集观察者,当数据发生变化的时候,会触发观察者进行 update
    const dep = new Dep()
    const property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {return}
    // 2. 获取对象形容中原有的 get 和 set 办法
    const getter = property && property.get
    const setter = property && property.set
    if ((!getter || setter) && arguments.length === 2) {val = obj[key]
    }
    let childOb = !shallow && observe(val)
    // 3. 增加 getter/setter
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {const value = getter ? getter.call(obj) : val
            // 动态属性 target 存储的是以后观察者。if (Dep.target) {dep.depend()
                if (childOb) {
                    // 将观察者增加到 Obsetver 实例属性 dep 中。childOb.dep.depend()
                    if (Array.isArray(value)) {dependArray(value)
                    }
                }
            }
            return value
        },
        set: function reactiveSetter(newVal) {const value = getter ? getter.call(obj) : val
            /* eslint-disable no-self-compare */
            if (newVal === value || (newVal !== newVal && value !== value)) {return}
            // ... 一些判断,省略
            // 当赋值的时候,如果值为对象,须要为新赋值的对象增加响应式
            childOb = !shallow && observe(newVal)
            // 调用 set 就是为属性赋值,赋值阐明有新的变动,所以要触发更新
            dep.notify()}
    })
}

整个 defineReactive 有两个中央比拟难以了解:

  1. 通过 Dep.target 获取依赖

因为这个中央波及到前面的编译局部,所以咱们把这部分逻辑独自拿进去,用一段简短的代码来形容整个过程,如下:

// 模仿 Dep
let Dep = {}
Dep.target = null

// 模仿变动数据
let data = {foo: 'foo'}

Object.defineProperty(data, 'foo', {get() {if (Dep.target) {console.log(Dep.target)
        }
    }
})

// 模仿编译 {{foo}}
// 1. 解析到 template 中须要 foo 属性的值
const key = 'foo'
// 2. 在 foo 属性对应的值渲染到页面之前,为 Dep.target 赋值
Dep.target = () => {console.log('察看 foo 的变动')
}
// 3. 获取 foo 属性的值,此时会触发 get 拦挡
const value = data[key]
// 4. 获取实现后,须要将 Dep.target 的值从新赋值 null,这样下一轮解析的时候,可能存储新的观察者
Dep.target = null
  1. 在闭包作用域内曾经蕴含了 Dep 对象,在 set 中通过此对象的 notify 办法触发更新,为什么还须要在 get 办法中,将依赖增加到 Observer 对象的实例属性 dep 中。

其实,这是为了不便在其余手动触发更新,因为 defineReactive 办法外部的 dep 对象是闭包作用域,在内部无奈间接拜访,只能通过赋值形式触发。

如果在 Observer 对象上保留一份,那么就能够通过 data.__ob__.dep 的形式拜访到,间接手动调用 notify 办法就能够触发更新,在 Vue.set 办法外部实现就能够这种触发更新形式。

Array

家喻户晓,Object.defineProperty 是无奈监控到通过 push,pop 等办法扭转数组,此时,vuejs 通过另外一种形式实现了数组响应式。该形式批改了数组原生的 push,pop 等办法,在新定义的办法中,通过调用数组对象的 __ob__ 属性的 notify 办法,手动触发更新。

Observer 构造函数中:

if (Array.isArray(value)) {if (hasProto) {
        // 反对__proto__,那么就通过 obj.__proto__的形式批改原型
        protoAugment(value, arrayMethods)
    } else {
        // 不反对,就将新定义的办法遍历增加到数组对象上,这样能够笼罩原型链上的原生办法
        copyAugment(value, arrayMethods, arrayKeys)
    }
    // 遍历数组项,如果某项是对象,那么为该对象增加响应式
    this.observeArray(value)
}

其中 arrayMethods 就是从新定义的数组操作方法。

  • arrayMethods

定义在 core/Observer/array.js 文件中,该文件次要作了两件事件:

  1. 创立新的集成自 Array.prototype 的原型对象 arrayMethods。
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
  1. 在新的原型对象上,增加自定义办法笼罩原生办法。
// 定义所有会触发更新的办法
const methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
]


methodsToPatch.forEach(function (method) {
    // 获取 Array 中原生的同名办法
    const original = arrayProto[method]
    // 通过 Object.defineProperty 为办法调用增加拦挡
    def(arrayMethods, method, function mutator(...args) {
        // 调用原生办法获取本该失去的后果
        const result = original.apply(this, args)
        const ob = this.__ob__
        let inserted
        // push,unshift,splice 三个办法会向数组中插入新值,此处依据状况获取新插入的值
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                inserted = args.slice(2)
                break
        }
        // 如果新插入的值是对象,那么须要为对象增加响应式,解决逻辑和 data 解决逻辑类似
        if (inserted) ob.observeArray(inserted)
        // 手动触发更新
        ob.dep.notify()
        return result
    })
})

从下面的解决逻辑能够看出,上面的数组操作能够触发自动更新:

// 批改数组项
[].push(1)
[].pop()
[].unshift(1)
[].shift()
[].splice()
// 批改数组项程序
[].sort()
[].reverse()

而上面的操作不能触发:

// 批改数组项
[1, 2][0] = 3
[1, 2].length = 0

Dep

在增加数据监听的过程中用到了 Dep 类,Dep 类相当于观察者模式中的 指标,用于存储所有的观察者和发生变化时调用观察者的 update 方进行更新。

export default class Dep {
    // 以后须要增加的观察者
    static target: ?Watcher;
    // id,惟一标识
    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)
    }

    // 调用观察者的 addDep 办法,将指标增加到每一个观察者中,观察者会调用 addSub 办法
    depend() {if (Dep.target) {Dep.target.addDep(this)
        }
    }

    // 将观察者排序,而后顺次调用 update
    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()}
    }
}

Watcher

Watcher 类是观察者模式中的观察者,当 Dep 触发变动的时候,会调用外部存储的所有 Watcher 实例的 update 办法进行更新操作。

在 vuejs 中,Watcher 可大抵分为三种:Computed Watcher,用户 Watcher(侦听器)和渲染 Watcher(触发 Dom 更新)。

Watcher 类蕴含大量的实例成员,在构造函数中,次要逻辑如下:

constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options ?: ? Object,
    isRenderWatcher ?: boolean
) {
    // ... 依据参数为实例成员赋值
    // 调用 get 办法
    this.value = this.lazy
        ? undefined
        : this.get()}

在 get 办法中,获取初始值并将本身增加到 Dep.target。

get() {
    // 1. 和上面的 popTarget 绝对应,这里次要是为 Dep.target 赋值
    // 因为存在组件之间的父子关系,所以在 pushTarget 中还会将以后对象寄存到队列中,不便解决实现子组件后持续解决父组件
    pushTarget(this)
    let value
    const vm = this.vm
    try {
        // 2. 获取初始值,并触发 get 监听,Dep 会收集该 Watcher
        value = this.getter.call(vm, vm)
    } catch (e) {if (this.user) {handleError(e, vm, `getter for watcher "${this.expression}"`)
        } else {throw e}
    } finally {
        // 实现 deep 深度监听
        if (this.deep) {traverse(value)
        }
        // 3. 将 Dep.target 值变为 null
        popTarget()
        this.cleanupDeps()}
    return value
}
addDep

addDep 办法用于将以后 Watcher 实例增加到 Dep 中。

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 办法,将 Watcher 实例增加到 Dep 中
            dep.addSub(this)
        }
    }
}
update

update 次要解决两种状况:

  1. 如果是用户增加的监听器,在变动的时候会执行 run 办法。
  2. 如果是渲染 Dom 时增加的,在变动的时候会执行 queueWatcher 函数,在 queueWatcher 函数中,通过队列的形式批量执行更新。
update() {if (this.lazy) {this.dirty = true} else if (this.sync) {
        // 用户增加的监听器会执行 run 办法
        this.run()} else {
        // 触发 dom 更新会执行此办法,以队列形式执行 update 更新
        queueWatcher(this)
    }
}
run

run 办法次要用于在数据变动后,执行用户传入的回调函数。

run() {if (this.active) {
        // 1. 通过 get 办法获取变动后的值
        const value = this.get()
        if (
            value !== this.value ||
            isObject(value) ||
            this.deep
        ) {
            // 2. 获取初始化时保留的值作为旧值
            const oldValue = this.value
            this.value = value
            if (this.user) {
                try {
                    // 3. 调用用户定义的回调函数
                    this.cb.call(this.vm, value, oldValue)
                } catch (e) {handleError(e, this.vm, `callback for watcher "${this.expression}"`)
                }
            } else {this.cb.call(this.vm, value, oldValue)
            }
        }
    }
}

生成渲染 Watcher

在查找编译入口那局部讲到了 platforms/web/runtime/index.js 文件定义了 $mount 办法,此办法用于首次渲染 Dom。

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

其外部执行了 mountComponent 函数。

mountComponent

定义在 core/instance/lifecycle.js 文件中,该函数次要执行三块内容:

  1. 触发 beforeMount,beforeUpdatemounted生命周期钩子函数。
  2. 定义 updateComponent 办法。
  3. 生成 Watcher 实例,传入 updateComponent 办法,此办法会在首次渲染和数据变动的时候被调用。
export function mountComponent(
    vm: Component,
    el: ?Element,
    hydrating?: boolean
): Component {
    // ... 1. 触发生命周期钩子
    // 2. 定义 updateComponent 办法
    let updateComponent
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {updateComponent = () => {
            // ...
            vm._update(vnode, hydrating)
            // ...
        }
    } else {updateComponent = () => {vm._update(vm._render(), hydrating)
        }
    }

    // 生成 watcher 实例
    new Watcher(vm, updateComponent, noop, {before() {if (vm._isMounted && !vm._isDestroyed) {callHook(vm, 'beforeUpdate')
            }
        }
    }, true /* isRenderWatcher */)
    hydrating = false

    // ... 触发生命周期钩子
    return vm
}

_update,_render

_update,_render 是 Vue 的实例办法,_render 办法用于依据用户定义的 render 或者模板生成的 render 生成虚构 Dom。_update 办法依据传入的虚构 Dom,执行 patch,进行 Dom 比照更新。

总结

至此,响应式解决的整个闭环脉络曾经摸清。

正文完
 0