Vue

coderljw 2024-10-13 Vue
  • Vue
  • 数据响应式
  • NextTick
大约 8 分钟

# 1. 数据响应式

  • Vue2 的数据响应式依赖 Object.defineProperty(Vue3 采用 Proxy)和发布订阅模式。

    1. 数据初始化时定义响应式对象:initState -- > observe -- > defineReactive -> Object.defineProperty
    export function defineReactive(
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
      const dep = new Dep()
      // ...
    
      let childOb = !shallow && observe(val)
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
          // ...
    
          // 收集依赖:添加订阅者(watcher)
          if (Dep.target) {
            dep.depend()
            if (childOb) {
              childOb.dep.depend()
              if (Array.isArray(value)) {
                dependArray(value)
              }
            }
          }
          return value
        },
        set: function reactiveSetter(newVal) {
          // ...
    
          // 派发更新:通知订阅者(watcher)
          dep.notify()
        },
      })
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    1. 挂载组件时收集依赖:mountComponent -> vm._update(vm._render(), hydrating),在解析模板生成 ast 时会标记 vm 上的数据,执行 render 函数时会访问改数据(getter)。

    src/core/instance/lifecycle (opens new window)

    export function mountComponent(
      vm: Component,
      el: ?Element,
      hydrating?: boolean
    ): Component {
      // ...
    
      let updateComponent
      if () {
        // ...
      } else {
        // 更新组件(patch)
        updateComponent = () => {
          vm._update(vm._render(), hydrating)
        }
      }
    
      // 每个组件都有一个对应的 RenderWatcher
      new Watcher(
        vm,
        updateComponent,
        noop,
        {
          before() {
            if (vm._isMounted && !vm._isDestroyed) {
              callHook(vm, 'beforeUpdate')
            }
          },
        },
        true /* isRenderWatcher */
      )
      // ...
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    1. 改变响应式对象的属性时派发更新:setter(Object.defineProperty) -> dep.notify -> watcher.update -> queueWatcher -> nextTick -> flushSchedulerQueue -> watcher.run -> watcher.get -> watcher.getter -> updateComponent -> vm._update(vm._render(), hydrating),最终 patch 后更新视图。

    src/core/observer/scheduler (opens new window)

    function flushSchedulerQueue() {
      // ...
    
      // 队列排序
      queue.sort((a, b) => a.id - b.id)
    
      for (index = 0; index < queue.length; index++) {
        // ...
    
        // 执行 run
        watcher.run()
        // ...
      }
    
      // ...
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  • Vue2 中 Array 为非响应式对象,Vue3 使用 Proxy 不会有此限制。

    1. Array.length 的 configurable 默认为 false(不可重新配置),也就不能使用 Object.defineProperty 重新定义 length 为响应式。vm.items[overflowLength] = value 及原生 push、pop 等可改变原数组长度的方法也就不能触发视图更新,而 Vue 重写了这些原生方法,在其中手动通知视图更新(src/core/observer/array (opens new window))。
    2. vm.items[indexOfItem] = newValue 不使用 Object.defineProperty 定义为响应式对象,是由于性能代价和获得的用户体验收益不成正比(为什么 vue 没有提供对数组属性的监听 (opens new window))。Vue 提供 Vue.set(vm.$set)、Vue.delete(vm.$delete)方法,在其中手动通知视图更新(src/core/observer/index (opens new window))。
  • Evan You (opens new window)

    function isObject(obj) {
      return (
        typeof obj === 'object' &&
        !Array.isArray(obj) &&
        obj !== null &&
        obj !== undefined
      )
    }
    
    function observe(obj) {
      if (!isObject(obj)) {
        throw new TypeError()
      }
    
      Object.keys(obj).forEach(key => {
        let internalValue = obj[key]
        let dep = new Dep()
        Object.defineProperty(obj, key, {
          get() {
            dep.depend()
            return internalValue
          },
          set(newValue) {
            const isChanged = internalValue !== newValue
            if (isChanged) {
              internalValue = newValue
              dep.notify()
            }
          },
        })
      })
    }
    
    window.Dep = class Dep {
      constructor() {
        this.subscribers = new Set()
      }
    
      depend() {
        if (activeUpdate) {
          this.subscribers.add(activeUpdate)
        }
      }
    
      notify() {
        this.subscribers.forEach(subscriber => subscriber())
      }
    }
    
    let activeUpdate
    
    // 作用:确保 update() 执行时,depend 可以访问到 activeUpdate
    function autorun(update) {
      // 作用:保证依赖项一直是最新的
      function wrappedUpdate() {
        activeUpdate = wrappedUpdate
        update()
        activeUpdate = null
      }
      wrappedUpdate()
    }
    
    const state = {
      count: 0,
    }
    
    observe(state)
    
    autorun(() => {
      console.log(state.count)
    })
    
    state.count++
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73

# 2. Watcher

    1. computed:computed watcher 是一个 lazy: true 的 watcher,通过 dirty 控制是否重新计算 value。

    实例化 Watcher 时 this.dirty = this.lazy 为 true;解析模板取值时 dirty: true 计算 value(evaluate),计算完后 dirty: false;之后取值直接返回 watcher.value,仅当依赖的 dep 变化触发 dep.notify 执行 watcher.update 后 dirty: true,取值时重新计算后 dirty: false

    // computed:getter
    function createComputedGetter(key) {
      return function computedGetter() {
        // ...
        if (watcher) {
          // dirty 为 true 重新计算
          if (watcher.dirty) {
            watcher.evaluate()
          }
          // 收集依赖
          if (Dep.target) {
            watcher.depend()
          }
          return watcher.value
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    export default class Watcher {
      constructor() {
        // ...
    
        // 实例化赋值 dirty
        this.dirty = this.lazy
        // ...
    
        // 计算属性实例化 Watcher 不会进行取值操作
        this.value = this.lazy ? undefined : this.get()
      }
      // dep setter 触发 notify 执行 update
      update() {
        if (this.lazy) {
          // 取值后将 dirty 置为 true
          this.dirty = true
        } // ...
      }
      // 计算值
      evaluate() {
        this.value = this.get()
        // 计算完将 dirty 置为 false
        this.dirty = false
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
  • src/core/instance/state (opens new window)

  • src/core/observer/watcher (opens new window)

    1. watch & $watch:user watcher 是用户自定义 watcher,会处理回调错误,可选择配置 deep 和 immediate。
    • immediate:立即触发回调。
    • deep:深度监听对象(递归遍历方式收集依赖)。
    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) {
        const info = `callback for immediate watcher "${watcher.expression}"`
        pushTarget()
        // 立即触发回调
        invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
        popTarget()
      }
      // 返回取消观察函数
      return function unwatchFn() {
        watcher.teardown()
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    export default class Watcher {
      get() {
        // ...
        try {
          // ...
        } catch (e) {
          // ...
        } finally {
          // 深度收集依赖
          if (this.deep) {
            traverse(value)
          }
        }
        return value
      },
      // 取消观察
      teardown () {
        if (this.active) {
          if (!this.vm._isBeingDestroyed) {
            remove(this.vm._watchers, this)
          }
          let i = this.deps.length
          while (i--) {
            this.deps[i].removeSub(this)
          }
          this.active = false
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // 递归遍历收集依赖(seenObjects 处理循环依赖)
    export function traverse(val: any) {
      _traverse(val, seenObjects)
      seenObjects.clear()
    }
    
    function _traverse(val: any, seen: SimpleSet) {
      let i, keys
      const isA = Array.isArray(val)
      if (
        (!isA && !isObject(val)) ||
        Object.isFrozen(val) ||
        val instanceof VNode
      ) {
        return
      }
      if (val.__ob__) {
        const depId = val.__ob__.dep.id
        if (seen.has(depId)) {
          return
        }
        seen.add(depId)
      }
      if (isA) {
        i = val.length
        while (i--) _traverse(val[i], seen)
      } else {
        keys = Object.keys(val)
        i = keys.length
        while (i--) _traverse(val[keys[i]], seen)
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
  • src/core/instance/state (opens new window)

  • src/core/observer/traverse (opens new window)

    1. Vuex strict mode:sync watcher 是 Vuex 严格模式下的 watcher。不经过 nextTick 异步执行,可以保证所有的状态变更都能被调试工具跟踪到(严格模式 - Vuex (opens new window))。
    export default class Watcher {
      update() {
        if (this.lazy) {
          // ...
        } else if (this.sync) {
          // 同步执行 run
          this.run()
        } // ...
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • src/core/observer/watcher (opens new window)

  • Vuex - src/store (opens new window)

# 3. extend

  • Vue.extend 使用原型继承的方式构造一个 Vue 的子类 Sub,并对构造的 Sub 进行缓存处理。
Vue.extend = function (extendOptions: Object): Function {
  const Super = this
  const SuperId = Super.cid
  // ...
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
  }
  // ...
  const Sub = function VueComponent(options) {
    this._init(options)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  // ...

  // cache constructor
  cachedCtors[SuperId] = Sub
  return Sub
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 4. proxy

  • proxy 的作用是把 props、data 等代理到实例上,使得可以通过 this 访问。
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop,
}

export function proxy(target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 重置 data
Object.assign(this.$data, this.$options.data.call(this))

// 重置 data 上的 form 属性
this.form = this.$options.data.call(this).form
1
2
3
4
5

# 5. nextTick

  • nextTick 在异步任务中遍历 callbacks 执行回调函数,采用的异步任务优先级:Promise -> MutationObserver -> setImmediate(IE 与 Node) -> setTimeout。

  • 使用 callbacks 而不是在 nextTick 直接执行回调函数,可以保证在同一个 tick 内多次执行 nextTick 不会开启多个异步任务。

  • 若 nextTick 没有传入回调函数,那么会返回一个 Promise。

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
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
  <div>
    <div id="div" @click="onClick">{{ papa }}</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      papa: 'jack',
    }
  },
  methods: {
    onClick() {
      // 异步:在组件 RenderWatcher 之后执行,视图已更新
      this.$nextTick().then(() => {
        console.log(div.innerHTML, 'promise') // pony
      })

      // 在组件 RenderWatcher 之前执行,视图尚未更新
      this.$nextTick(() => {
        console.log(div.innerHTML, 'papa before') // jack
      })

      // queue 添加 RenderWatcher
      this.papa = 'pony'

      // 在组件 RenderWatcher 之后执行,视图已更新
      this.$nextTick(() => {
        console.log(div.innerHTML, 'papa after') // pony
      })
    },
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
  <div>
    <div id="div" @click="onClick">{{ papa }}</div>
    <div>{{ assets }}</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      papa: 'jack',
      assets: 777,
    }
  },
  methods: {
    onClick() {
      // 异步:在组件 RenderWatcher 之后执行,视图已更新
      this.$nextTick().then(() => {
        console.log(div.innerHTML, 'promise') // pony
      })

      // queue 添加 RenderWatcher
      this.assets = 888

      // 在组件 RenderWatcher 之后执行,视图已更新
      this.$nextTick(() => {
        console.log(div.innerHTML, 'papa before') // pony
      })

      // RenderWatcher 已在 queue 队列中,这里不会再次添加 RenderWatcher
      // https://github.com/vuejs/vue/blob/v2.6.14/src/core/observer/scheduler.js#L165-L167
      this.papa = 'pony'

      // 在组件 RenderWatcher 之后执行,视图已更新
      this.$nextTick(() => {
        console.log(div.innerHTML, 'papa after') // pony
      })
    },
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 6. compiler

export const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 解析 template 生成 ast
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    // 优化 ast
    optimize(ast, options)
  }
  // 生成可执行代码
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns,
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 7. v-for 与 v-if

<template>
  <div>
    <!-- bad -->
    <ul>
      <li v-for="(item, index) in list" v-if="index % 2">
        {{ item }}
      </li>
    </ul>

    <!-- good -->
    <ul>
      <li v-for="(item, index) in filterList">
        {{ item }}
      </li>
    </ul>

    <!-- bad -->
    <ul>
      <li v-for="(item, index) in list" v-if="visible">
        {{ item }}
      </li>
    </ul>

    <!-- good -->
    <ul>
      <template v-if="visible">
        <li v-for="(item, index) in list">
          {{ item }}
        </li>
      </template>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      list: Array(100)
        .fill(0)
        .map((_, index) => index),
      visible: false,
    }
  },
  computed: {
    filterList({ list }) {
      return list.filter((_, index) => index % 2)
    },
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
以父之名
周杰伦.mp3