0%

Vue2.x 源码 01

MVVM

  1. 从 MVC 到 MVVM

    view(pages) <–> Controller(路由、控制器) <–> Model(数据 json)

    View(HTML) <–> ViewModel(DOM监听、数据绑定) <–> Model(数据 json)

  • MVC
    Model 封装了一些逻辑和操作数据的接口
    View 视图用于展示页面
    Controller 作为控制器,通过调用接口对数据进行操作并返回给 View 层,承上启下的与 Model、View 双向交流

  • MVVM
    Model 数据层,只关心数据方面
    ViewModel 事件监听和数据绑定,起到双向绑定的作用,响应式更新数据
    View 视图展示页面

数据变化驱动视图更新,视图变化数据也会同时响应 如 V-Model,是一个双向的过程

Vue 源码

  • Vue 作为 构造函数,通过在构造函数的原型上扩展方法
    1
    2
    3
    function Vue(options) {
    this._init(options) // Vue.prototype._init(options)
    }

Vue 核心

compiler + reactive + runtime

compiler

编译器,将 template 编译成为 AST 抽象语法树 (以及 可执行函数的字符串形式 with(this){return ${code}})

reactive

响应式处理 data

runtime

虚拟 DOM(vnode) + diff 算法 + 真实 DOM 操作

Vue 初始化时做了什么

  1. 处理组件配置项 options
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Vue.prototype._init = function (options) {
if (options && options._isComponent) {
// 子组件:性能优化,减少原型链的动态查找,提高执行效率
// 基于 vm.constructor.options 创建 vm.$options 并将属性复制给 vm.$options
initInternalComponent(vm, options)
} else {
// 根组件:options 合并,将全局配置选项合并到根组件的局部配置上(如 全局组件合并为 options 内的 component)
// 组件选项的合并:三种情况
// 1. Vue.component(ComponentName, _Component) 注册全局组件时,Vue 内置的全局组件和自己定义的全局组件,最终都会合并到 components 选项中
// 2. { components: { _Component } } 局部注册组件时,执行编译器生成的 render 函数时做了选项合并,会合并全局配置项到组件的局部配置项上
// 3. 这里的根组件情况
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
}
  1. initLifecycle(vm)
  • 初始化一些组件实例的关系属性:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    vm.$parent = parent
    vm.$root = parent ? parent.$root : vm

    vm.$children = []
    vm.$refs = {}

    vm._watcher = null
    vm._inactive = null
    vm._directInactive = false
    vm._isMounted = false
    vm._isDestroyed = false
    vm._isBeingDestroyed = false
  1. initEvents(vm) 初始化自定义事件
  • 从父组件的配置项里拿到 监听器
    1
    const listeners = vm.$options._parentListeners
  • prototype 上定义 $on | $off | $emit 等方法
  1. initRender(vm)
  • 定义插槽 $slot
  • 定义 _c | createElement() 方法
1
2
3
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  1. callHook(vm, 'beforeCreate') 执行生命周期钩子 beforeCreate()

  2. initInjections(vm) 初始化 inject

  3. initState(vm) 初始化 state,响应式处理 props | data | computed | watch | methods ..
    props -> methods -> data -> computed -> watch 初始化,

  • 并判重:key 不得与其他 key 重复;
  • 将属性配置代理到 vue 实例上,允许通过 this.keyName 的形式访问
  1. initProvide(vm) 初始化 provide

  2. callHook(vm, 'created') 执行生命周期钩子 created()

  3. 查看是否有 el 选项,有则自动调用 $mount(),没有则需要 手动调用 $mount()

    1
    2
    3
    new Vue({
    // el: '#app',
    }).$mount()

响应式原理

如何实现 vue 响应式

  1. 响应式核心是通过 Object.defineProperty() 拦截对数据属性的访问和设置来实现的

  2. 响应式数据分为两类:

    • 对象: 循环遍历数据对象的所有属性,为其设置 getter/setter 方法,以达到拦截的目的

      如果其属性依旧为 对象时,则递归每一个 key ,为其设置 getter/setter

      • this.key 访问数据时被 getter 方法拦截,并调用 dep.depend() 方法,,进行依赖收集,在 dep 中存储相关 watcher
      • obj[key] = val 时,修改数据触发 setter 方法,在其中对新旧值对比,需要改变时 将新值设为被观测对象(observe)并进行响应式处理,并调用 dep.notify() 触发依赖通知 watcher 进行异步更新
    • 数组:以数组原方法为原型创建新的对象,并在其中重写增强了 7 个可以直接改变数组的方法,通过拦截这 7 个方法进行数组操作

      • 插入新数据时进行响应式处理,然后 dep 通知 watcher 去更新
      • 删除数据时也是由 dep 通知更新

Data 响应式

  • initData
    1
    2
    const keys = Object.keys(data)
    observe(data, true /** rootData */) // new Observe(data)

Observe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 如果 observe 的是数组
for (let i = 0, l = data.length; i < l; i++) {
// 为数组的每个成员 observe
observe(data[i])
}

// 对象
this.walk(value)

function walk (data) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 对象的每个属性 做响应式处理
defineReactive(obj, keys[i])
}
}

Dep && Watcher

  • Dep 的作用就是收集属性值的变化,用一个数组 subs 保存订阅者 (subs: Array)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    Object.defineProperty(obj, key, {
    get: function () {
    // dep 和 watcher 双向收集
    dep.depend()
    if (childOb) {
    // 对嵌套对象进行 依赖收集
    childOb.dep.depend()
    // 数组
    if (Array.isArray(value)) {
    dependArray(value)
    }
    }
    },
    set: function () {
    // 对新值 观测 响应式处理
    childOb = !shallow && observe(newVal)
    // 当响应式数据更新时,做依赖通知更新,即通知所有 watcher,执行 watcher.update 方法,进入异步更新阶段
    dep.notify()
    }
    })
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // dep.js
    depend () {
    if (Dep.target) {
    // dep 放入 newDeps 数组 -> this.newDeps.push(dep)
    // watcher 放入 dep 数组(subs) -> dep.addSub(this)
    // 双向收集
    Dep.target.addDep(this)
    }
    }
    // watcher.js
    addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    // 将 dep 放到 watcher 中
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
    // 将 watcher 自己放到 dep 中,双向收集
    dep.addSub(this)
    }
    }
    }
  • 数据发生变化时,通知 dep 所有 watcher,执行 watcher.update(),见异步更新
1
2
3
4
5
6
set: function () {
// 对新值 观测 响应式处理
childOb = !shallow && observe(newVal)
// 当响应式数据更新时,做依赖通知更新,即通知所有 watcher,执行 watcher.update 方法,进入异步更新阶段
dep.notify()
}

computed

  • 每个计算属性都 实例化一个 watcher 对象,一个 computed key 对应一个 watcher key
  • computed 的两种形式:function、包含一个 get 方法的对象,最终都会转化为 getter
  • 初始化定义 computed 的时候,Object.defineProperty 方法将 computed 的属性代理到 vue,并定义 getter/setter 方法
  • 访问 computed 时自动调用 getter,执行 createComputedGetter
  • createComputedGetter 方法调用 computed 回调函数执行,并将 watcher.dirty = false,将回调函数执行结果 作为缓存 保存在 watcher.value 并返回
  • 当响应式数据更新时,遍历所有 watcher 并执行他们各自的 update 方法,将 watcher.dirty = true ,重新执行 computed 回调函数,计算新值,然后缓存到 watcher.value

计算属性缓存原理:

  每个 computed 对应一个 new Watcher,watcher 的作用就是调用 update 方法进行异步更新,而 update 只发生在 首次渲染以及后续视图更新

  所以 首次渲染时,watcher 也会通知对应的 computed 更新数据,也就是执行计算属性的回调函数,并把结果返回

  watcher 则是将其缓存到 watcher.value 上,并把 watcher.dirty 置为 false,后续对 computed 的访问都是将 watcher.value 的结果返回

  只有下一次视图更新时,才会 watcher.dirty = true,并重新执行 computed 的回调函数,且继续缓存结果,dirty = false

异步更新

  • dep.notify()
    此方法通知所有的 watcher,执行他们的 watcher.update()
    1
    2
    3
    4
    5
    6
    7
    8
    notify () {
    // 收集的 watcher
    const subs = this.subs.slice()
    // 遍历当前 dep 收集的所有 watcher,让这些 watcher 依次去执行自己的 update 方法
    for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
    }
    }
  • watcher.update()

    根据 watcher 配置项( lazy | async | none )决定怎么走,一般情况是 queueWatcher(this) 将当前 watcher 放进 watcher 队列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    update () {
    if (this.lazy) {
    // 懒执行,比如 computed
    // 将 dirty 置为 true,在组件更新后,当响应式数据再次被更新时,执行 computed getter
    // 重新执行 computed 回调函数,计算新值,然后缓存到 watcher.value
    this.dirty = true
    } else if (this.sync) {
    // 同步执行
    // this.$watch() 或者 watch 选项传递一个 sync 配置({ sync: true })
    this.run()
    } else {
    // 放入 watcher 队列(一般都是走这个)
    queueWatcher(this)
    }
    }

queueWatcher 方法中,将当前 watcher 放进 queueWatcher 队列后,调用 nextTick() 方法把 flushSchedulerQueue 函数传入 callbacks 数组,

  • queueWatcher

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    export function queueWatcher (watcher: Watcher) {
    if (!flushing) {
    // flushing = false 表示当前 watcher 队列没有在刷新,可以直接入队
    queue.push(watcher)
    } else {
    // 当前 watcher 队列已经在刷新,watcher 入队需要特殊操作
    // watcher 队列是有序的
    // 找到 队列中比要插入的 watcher id 大1个的成员,从它的下一个位置插入队列
    // 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)
    }

    // 即 this.$nextTick() | Vue.nextTick()
    // 将回调函数 flushSchedulerQueue 放入 callbacks 数组
    // 通过 pending 控制,向浏览器异步任务队列添加 flushCallBack 函数
    nextTick(flushSchedulerQueue)
    }

    flushSchedulerQueue 函数遍历 watcher 并执行 run()

  • nextTick()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 将传入的 callback 用 try catch 包一层,便于异常捕获
// 然后将包装后的函数插入 callbacks
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
// pending = false 执行 timerFunc() 函数,此函数的作用为 将 flushCallbacks(遍历callbacks中的 cb 并执行)插入到 浏览器的 异步任务队列
// vue 优先使用 promise,即 微任务队列
// 再重置为 true
pending = true
timerFunc()
}
}
  • flushCallbacks
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function flushCallbacks () {
    // 再次置为 false,下一个 flushCallbacks 可以进入异步微任务队列
    // 异步微任务队列 只能存在一个 flushCallbacks 函数
    pending = false
    // 缓存 callbacks 所有
    const copies = callbacks.slice(0)
    // 清空 callbacks
    callbacks.length = 0
    // 执行 callbacks 中所有 函数
    for (let i = 0; i < copies.length; i++) {
    copies[i]()
    }
    }
    const p = Promise.resolve()
    timerFunc = () => {
    p.then(flushCallbacks) // flushCallbacks() 执行 callbacks 中所有
    }

如何实现的异步更新机制

  1. 核心是利用浏览器的异步任务队列来实现,首选两种微任务 (promise | MutationObserver),其次宏任务 (setImmediate | setTimeout)
  2. 响应式数据更新后,调用 dep.notify() 方法,通知 dep 中收集的 watcher 去执行自己的 update 方法
  3. watcher.update() 方法将当前 watcher 放入一个 watcher 队列 (全局的 queue 数组)
  4. 然后通过 nextTick(flushSchedulerQueue) 方法把这个 刷新 watcher 队列的方法(flushSchedulerQueue)放入一个全局的 callbacks 数组中
  5. 如果此时浏览器的异步任务队列中没有一个叫 flushCallbacks 的函数,则执行 timeFunc 函数,将 flushCallbacks 函数放入浏览器的异步任务队列(promise)
  6. 如果已经存在 flushCallbacks 则等待执行完成,下一波再放入
  7. 这个 flushCallbacks 函数 负责执行 callbacks 数组中的所有 flushSchedulerQueue 函数,也就是 刷新当前 watcher 队列并执行成员的 watcher.run 方法
  8. run 方法就是执行 组件更新函数(rendering watcher)new Watcher 时传入的 updateComponent 方法,进入组件更新;或者用户 watch 的回调函数

Vue 的 nextTick API 如何实现

Vue.nextTick() 或者 vm.$nextTick() 就做了两件事:

  • 将传递的回调函数用 try catch 包裹(捕捉异常)后,放入 callbacks 数组

    callbacks 数组收集了用户调用 nextTick 传递的回调 或者是 flushSchedulerQueue 函数(所有 watcher.run()

  • 执行 timerFunc 函数,在浏览器的异步任务队列中 放入一个 刷新 callbacks 数组的函数

    1
    2
    3
    4
    const p = Promise.resolve()
    timerFunc = () => {
    p.then(flushCallbacks) // flushCallbacks() 执行 callbacks 中所有
    }

Vue 在更新 DOM 是异步执行的,响应式数据发生变化时,通过一个 callbacks 数组收集 在同一个事件循环中的所有数据变更,然后在下一次事件循环当中执行

1
2
3
4
5
6
vm.msg = 'new message'
vm.$el.msg === 'new message' // false DOM更新是异步的,无法立即获取更新后的 DOM

vm.nextTick(function () {
vm.$el.msg === 'new message' // true 回调函数将在 DOM 更新后被调用
})

wen

  1. <comp @click="handleClick"></comp> 中谁监听的 click 事件
  • 组件上的事件监听由组件本身来监听,谁触发谁监听
  • 最终会转化为: this.$emit() | this.on('click', function handleClick() {}) 的形式
  1. provide/inject 原理
  • inject 组件主动从 祖代 中查找匹配 key 的结果,并赋值给 injectkey 对应的值,最后得到一个 result[key] = value 的对象返回
    1
    2
    3
    4
    5
    6
    7
    8
    9
    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
    }
  1. computedmethods 区别
  • methods 每次访问都会执行

  • computed 通过 watcher 来实现的,对每个 computed key 实例化一个 watcher,默认 lazy 执行;computed 每次渲染只执行一次,并将 watcher.dirty 置为 false,当再次渲染页面调用 watcher.update() 方法时,会将 watcher.dirty 重置为 true,并且执行一次 computed

  • computed 有两种形式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // function
    computed: {
    show() { return 1 }
    }
    // 包含 getter 的对象
    computed: {
    showA: {
    get: function () {} // 默认只有 getter,也可以定义 setter
    }
    }
  1. watch
  • watch 实际上就是 每个 观察对象 调用 vm.$watch 方法,其中会实例化一个 Watcher 对象,并配置参数(deep, immediate)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
watch: {
a: function(val, oldVal) {},
b: 'someMethod', // 方法名

// 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
c: {
handler: function(val, oldVal) {},
deep: true
},
d: {
handler: function(val, oldVal) {},
immediate: true // 侦听开始后立即调用
},
e: [ // 数组,逐一调用
'handle1',
function handle2() {}
]
}
  1. computed | watch | methods 区别
  • 使用场景
    • methods 一般用于封装一些复杂的逻辑,同步异步处理
    • computed 一般用于简单的同步逻辑,将处理后的数据返回,显示在模板中,以减轻模板的重量
    • watch 一般用于需要监测数据变化时,执行异步或者开销较大的操作

computed | watch 都是通过 watcher 来实现的

  • computed 默认 lazy 执行,且不可更改,watch 可配置(deep | immediate)

  • 使用场景不同,watch 通常用于异步操作,computed同步多

  • watch | methods 两种东西没有可比性,不过可以把 watch 中一些复杂的逻辑抽离到 methods 中,提高可读性

  1. is 属性
  1. 内联模板 inline-template
-------------本文结束感谢您的阅读-------------