vue3学习笔记

579 阅读5分钟

参考视频

参考文章

注:

  • 学习版本vue:3.2.23
  • 最新版使用了pnpm包管理工具
  • 启用dev后会生成packages/vue/dist/vue.global.js文件,为vue的SourceMap映射,可以参考examples文件夹中的实例使用,然后就可以调试了

项目结构

vue分为3个模块

  1. Reactivity Module(响应式系统)
  2. Compiler Module (编译阶段,将template转换成可供renderer调用的函数)
  3. Renderer Module
    • render phase(将render function转换成vartuil DOM)
    • mount phase(将vartuil DOM绘制成真实的DOM)
    • patch phase(将old vartuil DOM与new vartuil DOM对比,绘制不同部分)

reactivty

reactive

该方法主要判断接收值的类型,是否只读,是否已经被打上标记,而后利用Proxy代理

baseHandle

为Proxy代理的基础。

  • get、has、ownKeys中调用track触发订阅,记录需要订阅的方法。其中get还处理了更多的边界情况以及是否是浅层代理
  • set、deleteProperty中调用trigger,用于触发effect副作用,同时判断事件类型(新增、删除、修改等)

effect

创建ReactiveEffect对象用于管理自身需要监听的值,以及调用副作用方法时的一些上下文。如果不是lazy类型,则立马执行副作用,副作用中如果有被Proxy值的get,则触发了track方法

  • track:为每个对象建立在全局WeakMap上的弱引用Map,为对象上的每个键创建Dep(发布订阅),调用trackEffects方法,判断是否要将activtyEffect(全局定义,用于存储当前执行的effect)加入至Dep中,同时在ReactiveEffect上引用当前dep方便ReactiveEffect主动解除绑定
  • trigger:接受事件类型后判断要通知哪些Dep,调用triggerEffects,执行所有effect(effect中包含scheduler和run方法,如果有则会执行scheduler,scheduler可以是queueJob)

ref

该api内部实现相比reactive简单得多,判断如果是深度拷贝并且isObject,则value = toReactive(value),否则直接使用。然后编写set、get逻辑

get value() {
  trackRefValue(this) // 内部创建了Dep,调用了trackEffects方法
  return this._value
}

set value(newVal) {
  newVal = this._shallow ? newVal : toRaw(newVal)
  if (hasChanged(newVal, this._rawValue)) {
    this._rawValue = newVal
    this._value = this._shallow ? newVal : toReactive(newVal)
    triggerRefValue(this, newVal)
  }
}

computed

如果传入的是Function,则禁用set,如果是ref,则复用ref的set和get。内部由ReactiveEffect实现,并重写scheduler

内部包含变_dirty,只有当computed创建对象被别人使用时scheduler才生效。并通过改值的true和false交换,使得一次更新只计算一次

nextTick(异步执行核心)

该方法在runtime-core包中

  • 全局定义两个Promise,resolvedPromise、currentFlushPromise(会在resolvedPromise结束后有则执行)
  • nextTick将执行函数fn设置为在currentFlushPromise|resolvedPromise结束后执行
  • queueJob,在forceUpdata、reload、renderer中会调用,状态值设置后最终也会执行,表示要开始执行队列中的任务了。内部去重排序任务,然后开启Promise(只会开启一次直到执行结束)。开启的同时设置currentFlushPromise
if (!isFlushing && !isFlushPending) {
  isFlushPending = true
  currentFlushPromise = resolvedPromise.then(flushJobs)
}
  • flushJobs中消耗任务,内部再次对任务进行排序执行,并使用全局变量flushIndex应对当前job中新生成的job插入位置。(执行job时可能会产生新的副作用,而新的副作用可能是之前执行过的job,需要再次执行。因此新添加job只需以flushIndex以后的对比去重)
<div id='app'>
  <div v-if='state.bool'>
    <div id='text'>
      {{state.num}}
    </div>
  </div>
  <button @click='click'>点击</button>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
  const { createApp, reactive, nextTick } = Vue;
  const vue = Vue.createApp({
    setup() {
      const state = reactive({
        bool: false,
        num: 1
      })
      const click = () => {
        nextTick(() => { // fn1
          console.log('before', document.querySelector("#text")) // null
        })
        state.bool = !state.bool // set
        state.num++
        nextTick(() => { // fn2
          console.log('after', document.querySelector("#text"))
        })
      }
      return {
        state,
        click
      }
    }
  })
  vue.mount("#app")
</script>

从以上代码中可以看到第一个nextTick将fn加入到resolvedPromise中,第二个nextTick将fn加入到currentFlushPromise中,所以微任务添加顺序是fn1加到resolvedPromise,flushJobs加到resolvedPromise,设置currentFlushPromise为resolvedPromise.then()的执行结果,fn2加到currentFlushPromise。所以点击第一下fn1检查不到元素,点击第二下fn2检查不到元素。

流程

mount -> 收集effect -> get -> track绑定至Dep -> ... -> set -> queueJob -> flushJobs消耗effect -> patch -> ...

核心代码实现

关于mount的实现

递归遍历h对象

function h(tag, props, children) {
  return {
    tag,
    props,
    children
  }
}
function mount(vnode, container) {
   // 记录vnode.el方便patch阶段获取dom节点
  let element = vnode.el = document.createElement(vnode.tag)
  if (vnode.props) {
    for (const key in vnode.props) {
      const value = vnode.props[key]
      element.setAttribute(key, value)
    }
  }
  if (vnode.children) {
    if (typeof vnode.children === 'string') {
      element.innerText = vnode.children
    } else {
      vnode.children.forEach(item => {
        if (typeof item === 'string') {
          element.appendChild(document.createTextNode(item))
        } else {
          mount(item, element)
        }
      })
    }
  }
  container.appendChild(element)
}

const vdom = h('div', { class: 'red' }, [
  h('span', null, 'hello') // children暂不支持[text]
])
mount(vdom, document.querySelector("#app"))

关于patch实现

利用递归,先对比tag和key,然后对比props,最后递归children。我们将看到patch里面递归效率极低,vue Compiler将优化节点,为响应式节点或props打上标记,递归时智能识别有修改的节点

function patch(oldNode, newNode) {
    if (oldNode.tag === newNode.tag) {
        const el = newNode.el = oldNode.el
        // 处理props
        const oldProps = oldNode.props || {}
        const newProps = newNode.props || {}
        for (const key in newProps) {
            const newValue = newProps[key]
            const oldValue = oldProps[key]
            if (newValue !== oldValue) {
                if (key.startsWith('on')) {
                    el.removeEventListener(key.substring(2).toLocaleLowerCase(), oldValue)
                    el.addEventListener(key.substring(2).toLocaleLowerCase(), newValue)
                } else {
                    el.setAttribute(key, newValue)
                }
            }
        }
        for (const key in oldProps) {
            const newValue = newProps[key]
            const oldValue = oldProps[key]
            if (!newValue) {
                if (key.startsWith('on')) {
                    el.removeEventListener(key.substring(2).toLocaleLowerCase(), oldValue)
                } else {
                    el.removeAttribute(key)
                }
            }
        }

        // 处理children
        const oldChildren = oldNode.children
        const newChildren = newNode.children
        if (typeof newChildren === 'string') {
            if (newChildren !== oldChildren) {
                el.innerText = newChildren
            }
        } else if (!newChildren) {
            el.innerHTML = ""
        } else if (typeof oldChildren === 'string') {
            el.innerHTML = ""
            newChildren.forEach(item => {
                mount(item, el)
            })
        } else {
            const minLength = Math.min(oldChildren.length, newChildren.length)
            for (let i = 0; i < minLength; i++) {
                patch(oldChildren[i], newChildren[i])
            }
            if (oldChildren.length > minLength) {
                oldChildren.slice(minLength).forEach(item => {
                    el.removeChild(item.el)
                })
            }
            if (newChildren.length > minLength) {
                newChildren.slice(minLength).forEach(item => {
                    mount(item, el)
                })
            }
        }
    } else {
        // 此处处理移除后重新创建逻辑
    }
}

响应式原理

利用发布订阅模式,第一次调用watchEffect便订阅,设置值后通知

let currentEffect
class Dep {
  list = new Set()
  add() {
    if (currentEffect) {
      this.list.add(currentEffect)
    }
  }
  notify() {
    this.list.forEach(fn => {
      fn()
    })
  }
}

const weakMap = new WeakMap()
function getDep(obj, key) {
  let map = weakMap.get(obj)
  if (!map) {
    map = new Map()
    weakMap.set(obj, map)
  }
  let dep = map.get(key)
  if (!dep) {
    dep = new Dep()
    map.set(key, dep)
  }
  return dep
}

const handler = {
  get(obj, key, rec) {
    const dep = getDep(obj, key)
    dep.add()
    return Reflect.get(obj, key, rec)
  },
  set(obj, key, value, rec) {
    const dep = getDep(obj, key)
    const result = Reflect.set(obj, key, value, rec)
    dep.notify()
    return result
  }
}
function reactive(state) {
  return new Proxy(state, handler)
}

function watchEffect(effect) {
  currentEffect = effect
  effect()
  currentEffect = undefined
  // 为保证每次执行时收集effect中最新的依赖,可以使用闭包封装
  // function fn() {
  //    currentEffect = fn
  //    effect()
  //    currentEffect = undefined
  // }
  // fn()
  // 应对如下写法
  // watchEffect(() => {
  //    if (state.bool) {
  //      console.log(state.count);
  //    }
  // })
}



const state = reactive({
  count: 0,
})
watchEffect(() => {
  console.log(state.count);
})

state.count++

vue-minni,点击数字数字加一

const App = {
    data: reactive({ number: 1 }),
    render() {
        return h('div', {
            onClick: () => {this.data.number++}
        }, String(this.data.number))
    }
}
function createApp(app, container) {
    let isMount = true
    let preDom
    watchEffect(() => {
        if (isMount) {
            preDom = app.render()
            mount(preDom, container)
            isMount = false
        } else {
            const nowDom = app.render()
            patch(preDom, nowDom)
            preDom = nowDom
        }
    })

}
createApp(App, document.querySelector('#app'))

结束语

本文是我花了几天时间对vue源码的一个大体理解,如果有有问题的地方,望指正。后续我将继续对vue源码进行更深入的学习,同时修补该篇文章的问题。