vue3简要解析以及模拟vue3的响应式原理

文章内容输出来源:拉勾教育前端高薪训练营

Vue3.0和Vue2.0的区别

  • 源码全部采用typescript重写,使用Monorepo的组织方式把独立的功能模块都提取到不同的包中
  • 增加Composition API(组合API)
  • 使用proxy重写响应式,对编译器做了优化,重写了虚拟DOM,让渲染和update的性能大幅度提升,服务端渲染的性能也提升了2-3倍
  • 官方开发工具Vite提升了开发效率

Vue3.0不同的构建版本

cjs

  • vue.cjs.js
  • vue.cjs.prod.js

global

  • vue.global.js
  • vue.global.prod.js
  • vue.runtime.global.js
  • vue.runtime.global.prod.js

browser

  • vue.esm.browser.js
  • vue.esm.browser.prod.js
  • vue.runtime.esm-browser.js
  • vue.runtime.esm-browser.prod.js

bundle

  • vue.esm-bundler.js
  • vue.runtime.esm-bundler.js

Composition API(组合式 API)

设计动机

  • Vue2的Options API

    • 包含一个描述组件选项(data、methods、props)的对象
    • Options API开发复杂组件,同一个功能逻辑的代码被拆分到不同选项,逻辑关注点的列表也会增长。尤其对于那些一开始没有编写这些组件的人来说,这会导致组件难以阅读和理解
      Alt
      这是一个大型组件的示例,其中逻辑关注点按颜色进行分组
      这种碎片化使得理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。
      如果能够将同一个逻辑关注点相关代码收集在一起会更好。而这正是组合式 API 使我们能够做到的
  • Vue3的Composition API

    • 一组基于函数的API
    • 可以更灵活的组织组件的逻辑
      vue3简要解析以及模拟vue3的响应式原理_第1张图片

Composition API初体验

DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Documenttitle>
head>
<body>
  <div id="app">
    x: {{ position.x }}
    y: {{ position.y }}
  div>
  <script type="module">
    import { createApp, reactive } from './node_modules/vue/dist/vue.esm-browser.js'

    const app = createApp({
      setup () {
        // 第一个参数 props
        // 第二个参数 context:attrs、emit、slots
        const position = reactive({
          x: 0,
          y: 0,
        })

        return {
          position,
        }
      },
      mounted () {
        this.position.x = 100
      }
    })

    console.log(app)

    app.mount('#app')
  script>
body>
html>

setup函数及Composition中常用的API
setup是在组件初始化之前执行的,在beforeCreate和created之间,这两个生命周期内的代码可以直接放在setup中。生命周期包含

Options API Hook inside inside setup
beforeCreate Not needed
created Not needed
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered
  • 使用onMounted和onUnmounted改造,使同一块逻辑的代码处于相同的位置
import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'

const useMouseMove = () => {
  // 第一个参数 props
  // 第二个参数 context:attrs、emit、slots
  const position = reactive({
    x: 0,
    y: 0,
  })

  const update = e => {
    position.x = e.pageX
    position.y = e.pageY
  }

  onMounted(() => {
    window.addEventListener('mousemove', update)
  })

  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })

  return position
}

const app = createApp({
  setup () {
    const position = useMouseMove()
    return {
      position,
    }
  }
})

app.mount('#app')
  • reactive、toRefs、ref
    reactive:通过Proxy将对象转换成响应式
    toRefs:接受一个代理对象作为参数,内部创立一个新的对象,遍历传入的代理对象的所有数据并转换成响应式对象
    ref:将基本类型数据包装成响应式对象,内部会创建一个只有value属性的对象,该value属性具有getter和setter,在html模板中使用时可以省略value属性
DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Documenttitle>
head>
<body>
  <div id="app">
    x: {{ x }}
    y: {{ y }}
    <button @click="handleAdd">addbutton>
    <span>now count is {{ count }}span>
  div>
  <script type="module">
    import { createApp, reactive, onMounted, onUnmounted, toRefs, ref } from './node_modules/vue/dist/vue.esm-browser.js'

    const useMouseMove = () => {
      // 第一个参数 props
      // 第二个参数 context:attrs、emit、slots
      const position = reactive({
        x: 0,
        y: 0,
      })

      const update = e => {
        position.x = e.pageX
        position.y = e.pageY
      }

      onMounted(() => {
        window.addEventListener('mousemove', update)
      })

      onUnmounted(() => {
        window.removeEventListener('mousemove', update)
      })

      return toRefs(position)
    }

    const useCount = () => {
      const count = ref(0)
      return {
        count,
        handleAdd: () => {
          count.value++
        },
      }
    }

    const app = createApp({
      setup () {
        const { x, y } = useMouseMove()
        return {
          x,
          y,
          ...useCount(),
        }
      }
    })

    app.mount('#app')
  script>
body>
html>
  • computed
    用法1:
computed(() => count.value + 1)

用法2:

const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})
DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Documenttitle>
head>
<body>
  <div id="app">
    <button @click="push">按钮button>
    未完成:{{ activeCount }}
  div>
  <script type="module">
    import { createApp, reactive, computed } from './node_modules/vue/dist/vue.esm-browser.js'
    const data = [
      { text: '看书', completed: false },
      { text: '敲代码', completed: false },
      { text: '约会', completed: true }
    ]

    createApp({
      setup () {
        const todos = reactive(data)

        const activeCount = computed(() => {
          return todos.filter(item => !item.completed).length
        })

        return {
          activeCount,
          push: () => {
            todos.push({
              text: '开会',
              completed: false
            })
          }
        }
      }
    }).mount('#app')
  script>
body>
html>
  • watch
    有三个参数:
    1、要监听的数据
    2、监听到数据变化后执行的函数,这个函数有两个参数分别是新值和旧值
    3、选项对象,deep和immediate
    返回值:取消监听的函数
DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Documenttitle>
head>
<body>
  <div id="app">
    <p>
      请问一个 yes/no 的问题:
      <input v-model="question">
    p>
    <p>{{ answer }}p>
  div>

  <script type="module">
    // https://www.yesno.wtf/api
    import { createApp, ref, watch } from './node_modules/vue/dist/vue.esm-browser.js'

    createApp({
      setup () {
        const question = ref('')
        const answer = ref('')

        watch(question, async (newValue, oldValue) => {
          const response = await fetch('https://www.yesno.wtf/api')
          const data = await response.json()
          answer.value = data.answer
        })

        return {
          question,
          answer
        }
      }
    }).mount('#app')
  script>
body>
html>
  • watchEffect
    watch函数的简化版本,也用来监视数据的变化,接收一个函数作为参数,监听函数内响应式数据的变化
DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Documenttitle>
head>
<body>
  <div id="app">
    <button @click="increase">increasebutton>
    <button @click="stop">stopbutton>
    <br>
    {{ count }}
  div>

  <script type="module">
    import { createApp, ref, watchEffect } from './node_modules/vue/dist/vue.esm-browser.js'

    createApp({
      setup () {
        const count = ref(0)
        const stop = watchEffect(() => {
          console.log(count.value)
        })

        return {
          count,
          stop,
          increase: () => {
            count.value++
          }
        }
      }
    }).mount('#app')
  script>
body>
html>
  • 实现一个todolist
    首先看看Vue2和Vue3指令书写的区别
    Vue2
Vue.directive('editingFocus', {
  bind(el, binding, vnode, prevVnode) {
    binding.value && el.focus
  },
  inserted() {},
  update() {}, // remove
  componentUpdated() {},
  unbind() {},
})

Vue3

app.directive('editingFocus', {
  beforeMount(el, binding, vnode, prevVnode) {
    binding.value && el.focus
  },
  mounted() {},
  beforeUpdate() {}, // new
  updated() {},
  beforeUnmount() {}, // new
  unmounted() {},
})

App.vue






utils/useLocalStorage.js

function parse (str) {
  let value
  try {
    value = JSON.parse(str)
  } catch {
    value = null
  }
  return value
}

function stringify (obj) {
  let value
  try {
    value = JSON.stringify(obj)
  } catch {
    value = null
  }
  return value
}

export default function useLocalStorage () {
  function setItem (key, value) {
    value = stringify(value)
    window.localStorage.setItem(key, value)
  }

  function getItem (key) {
    let value = window.localStorage.getItem(key)
    if (value) {
      value = parse(value)
    }
    return value
  }

  return {
    setItem,
    getItem
  }
}

Vue3性能的提升

响应式系统升级

  • Vue2中的响应式系统的核心是defineProperty,初始化时遍历data中的所有成员及子成员并转换成getter和setter,即时不使用也会转换
  • Vue3中使用Proxy对象重写响应式系统,Proxy代理对象可以拦截属性的访问、赋值、删除等操作,不需要在初始化的时候遍历所有属性,多层嵌套时只有访问某一个属性时才会递归转换下一级属性
    • 可以监听动态新增的属性,在Vue2中监听动态属性需要调用Vue.set来处理
    • 可以监听删除的属性
    • 可以监听数组的索引和length属性

编译优化

  • Vue2中通过标记静态根节点,优化diff的过程,但是静态节点还是需要diff算法比对
  • Vue3中标记和提升所有的静态根节点,diff的时候只需要对比动态节点内容
    • Fragments(片段)(升级vetur插件),模板中不再需要创建一个唯一的根节点
    • 静态提升,静态节点被提升到render函数的外层,初始化的时候创建一次,再次调用render的时候不需要再次创建,直接重用
    • Patch flag,标记动态节点执行diff算法时比对的内容(eg:1,只比对文本内容是否变化;9,只比对文本和属性是否变化)
    • 缓存事件处理函数

源码体积的优化

  • Vue3中移除了一些不常用的API,例如:inline-template、filter等
  • Tree-shaking

Vue3响应式原理

  • Proxy对象实现属性监听
  • 多层属性嵌套,在访问属性过程中处理下一级属性
  • 默认监听动态添加的属性
  • 默认监听属性的删除操作
  • 默认监听数组索引和length属性
  • 可以作为单独的模块使用

reactive/ref/toRefs/computed

reactive

  • 接收一个参数,判断这参数是否是对象
  • 创建拦截器对象handler,设置get/set/deleteProperty
  • 返回Proxy对象

reactive vs ref

  • ref可以把基本数据类型数据转换成响应式对象
  • ref返回的对象,重新赋值成对象也是响应式的
  • reactive返回的对象,重新赋值丢失响应式
  • reactive返回的对象不可以解构

toRefs
把reactive返回的对象的每一个属性转换成类ref对象,以便对reactive返回的对象解构

依赖收集过程

目标对象 目标对象的属性名称 effect的函数 key就是目标对象,value就是目标对象的属性名称depsMap(new Map()) targetMap(new WeakMap()): key value, key value, key value key就是目标对象的属性名称,value就是effect的函数集合dep(new Set()) depsMap(new Map()): key value, key value, key value dep(new Set()),value就是effect函数: value, value, value 目标对象 目标对象的属性名称 effect的函数

模拟源码

const isObject = val => val !== null && typeof val === 'object'
const convert = target => isObject(target) ? reactive(target) : target
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target, key) => hasOwnProperty.call(target, key)

export function reactive (target) {
  if (!isObject(target)) return target

  const handler = {
    get (target, key, receiver) {
      // 收集依赖
      track(target, key)
      const result = Reflect.get(target, key, receiver)
      return convert(result)
    },
    set (target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver)
      let result = true
      if (oldValue !== value) {
        result = Reflect.set(target, key, value, receiver)
        // 触发更新
        trigger(target, key)
      }
      return result
    },
    deleteProperty (target, key) {
      const hadKey = hasOwn(target, key)
      const result = Reflect.deleteProperty(target, key)
      if (hadKey && result) {
        // 触发更新
        trigger(target, key)
      }
      return result
    }
  }

  return new Proxy(target, handler)
}

let activeEffect = null

export function effect (callback) {
  activeEffect = callback
  callback() // 访问响应式对象属性,去收集依赖
  activeEffect = null
}

let targetMap = new WeakMap()

export function track (target, key) {
  if (!activeEffect) return
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  dep.add(activeEffect)
}

export function trigger (target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => {
      effect()
    })
  }
}

export function ref (raw) {
  // 判断 raw 是否是ref 创建的对象,如果是的话直接返回
  if (isObject(raw) && raw.__v_isRef) {
    return
  }
  let value = convert(raw)
  const r = {
    __v_isRef: true,
    get value () {
      track(r, 'value')
      return value
    },
    set value (newValue) {
      if (newValue !== value) {
        raw = newValue
        value = convert(raw)
        trigger(r, 'value')
      }
    }
  }
  return r
}

export function toRefs (proxy) {
  const ret = proxy instanceof Array ? new Array(proxy.length) : {}

  for (const key in proxy) {
    ret[key] = toProxyRef(proxy, key)
  }

  return ret
}

function toProxyRef (proxy, key) {
  const r = {
    __v_isRef: true,
    get value () {
      return proxy[key]
    },
    set value (newValue) {
      proxy[key] = newValue
    }
  }
  return r
}

export function computed (getter) {
  const result = ref()

  effect(() => (result.value = getter()))

  return result
}

你可能感兴趣的:(拉勾教育学习笔记,vue.js,前端,javascript)