vue3.0 菜鸟初体验 && 响应式原理模拟实现

2020-12-2010:41:39WEB前端开发Comments1,740 views字数 14795阅读模式

创建VUE3 项目

环境搭建

npm install -g @vue/cli # OR yarn global add @vue/cli
vue create todolist-vue3
# select vue 3 preset
复制代码

我在搭建的过程中,由于我本地安装的是vue-cli 版本是 3.x,查看vue-cli 命令:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

#--注意V 是大写--
vue -V
复制代码

因此,先需要全局更新vue-cli版本到最新:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

npm install -g @vue/cli
# OR
yarn global add @vue/cli
复制代码

成功更新,运行 vue -V, 结果版本还是我原先的版本,怎么办呢?网上找了好多方法,一顿折腾后,还是没有解决问题,时间也不早,关机睡觉。 重新开机后,重新运行vue -V 结果终于显示了预期的了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

添加待办事项

vue3.0 菜鸟初体验 && 响应式原理模拟实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

JS 的逻辑代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

import { ref } from 'vue'
import './assets/index.css'
 // 1. 添加代办事件
const useAdd = todos => {
    //  ref 返回一个响应式的对象
    const input = ref('') 
    // 文本框中输入的值添加到待办列表中
    const addTodo = () => {
      // 输入空格,直接返回
      const text = input.value && input.value.trim()
      if (text.length === 0) return 
      // 将输入的待办事件,添加到第一个
      todos.value.unshift({
        text,
        completed: false
      })
      input.value = ''
      console.log(todos.value)
    }
    return {
      input,
      addTodo
    }
  
}
export default {
  name: 'App',
  setup () {
    const todos = ref([])
    return {
     // 这个对象中的数据供模板使用
     todos,
     ...useAdd(todos)
    }
  }
}
复制代码

template 模板代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

<section id="app" class="todoapp">
    <header class="header">
      <h1>todos</h1>
      <input
        class="new-todo"
        placeholder="What needs to be done?"
        autocomplete="off"
        autofocus
        v-model="input"
        @keyup.enter="addTodo"
        >
    </header>
    <section class="main">
      <ul class="todo-list">
        <input id="toggle-all" class="toggle-all" type="checkbox">
        <label for="toggle-all">Mark all as complete</label>
          <li v-for="todo of todos" :key="todo.text">
            {{todo.text}}
          </li>
      </ul>  
     </section> 
  
  
  </section>
复制代码

注意: ref 定义的响应式对象,使用时需要通过 变量名称.value,否则会报undefiend文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

删除待办事项

js 代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

// 2. 定义删除待办事件函数
const useRemove = todos => {
  const remove = todo => {
    // 获取todo的索引
    let index = todos.value.indexOf(todo)
    todos.value.splice(index, 1)
  }
  return {
    remove
  }
}
//setup 中调用

setup () {
   ....
    return {
     ..省略..
     ...useRemove(todos)
    }
  }
复制代码

template 调用删除的代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

<button class="destroy" @click="remove(todo)"></button>
复制代码

编辑待办事件

这个功能比较复杂,我们可以先将要实现的功能列举出来:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

  • 双击待办项,展示编辑文本框
  • 按回车或者编辑文本框失去焦点,修改数据
  • 按esc取消编辑
  • 把编辑文本框清空按回车,删除这一项
  • 显示编辑文本款的时候获取焦点

js代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

// 3. 编辑待办项
const useEdit = remove => {
                                                                                                               
  // 记录原有的数据
  let beforEditText = ''
  // 标识编辑的状态
  const editingToDo = ref(null)
  // 开始编辑,需要记录编辑之前的文本数据
  const editTodo = todo => {
    beforEditText = todo.text
    editingToDo.value = todo
  }
  // 完成编辑
  const doneEdit = todo => {
    if (!editingToDo.value) return

    todo.text = todo.text.trim()
    // 如果文本框的值为空,则删除这一项
    todo.text || remove(todo)
    // 取消状态状态
    editingToDo.value = null
  }
  // 取消编辑
  const cancelEdit = todo => {
    // 取消状态状态
    editingToDo.value = null
    // 恢复文本数据
    todo.text = beforEditText
  } 
  return {
    editingToDo,
    editTodo,
    doneEdit,
    cancelEdit
  }
}

export default {
  name: 'App',
  setup () {
    const todos = ref([
      {
        text: '吃饭',
        completed: false,
      },
      {
        text: '睡觉',
        completed: false,
      },
      {
        text: '打豆豆',
        completed: false,
      },
    ])
    const { remove } = useRemove(todos)
    return {
     // 这个对象中的数据供模板使用
     todos,
     remove,
     ...useAdd(todos),
     ...useEdit(remove)
    }
  },
  directives: {
    editingFocus: (el, binding) => {
      binding.value && el.focus()
    }
  }
}
复制代码

tempalte 代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

<section id="app" class="todoapp">
    <header class="header">
      <h1>todos</h1>
      <input
        class="new-todo"
        placeholder="What needs to be done?"
        autocomplete="off"
        autofocus
        v-model="input"
        @keyup.enter="addTodo"
        >
    </header>
    <section class="main">
      <ul class="todo-list">
        <input id="toggle-all" class="toggle-all" type="checkbox">
        <label for="toggle-all">Mark all as complete</label>
          <li v-for="todo of todos" 
          :key="todo"
          :class="{editing: todo === editingToDo}">
            <div class="view">
           <!-- <input class="toggle" type="checkbox" v-model="todo.completed"> -->
             <label @dblclick="editTodo(todo)">{{ todo.text }}</label>
            <button class="destroy" @click="remove(todo)"></button>
          </div>
          <input
            class="edit"
            type="text"
            v-editing-focus="todo === editingToDo"
            v-model="todo.text"
            @keyup.enter="doneEdit(todo)"
            @blur="doneEdit(todo)"
            @keyup.esc="cancelEdit(todo)"
            >
          </li>

      </ul>  
     </section> 
  
  
  </section>
复制代码

切换待办事项状态

  • 点击checkbox, 改变所有的待办事项的状态
  • All/Active/Completed, 切换查看不同状态的事项
  • 其它
    • 显示未完成待办项个数
    • 移除所有完成的项目
    • 如果没有待办项隐藏main 和 footer

点击checkbox, 改变所有的待办事项的状态

首先我们分析一下实现这一步,需要考虑到两种情况文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

1.点击全选框,所有待办选项都要被勾选文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

  1. 当选项全被选上时,全选框要被勾选

因此,我们可以定义一个计算属性allDone,绑定全选框。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

  • 通过get来获取所有项的状态,当所有的选项状态为已完成时,全选框被选中。
  • 通过 set 来设置将所有的选项状态为 完成 状态

实现代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

const allDone = computed({
   get () {
     // 所有事项都已完成的,返回true
     return !todos.value.filter(todo => !todo.completed).length
   },
   set (value) {
     todos.value.forEach(todo => {
       todo.completed = value
     })
   }
 })
 //---模板中代码---
 <input id="toggle-all" v-model="allDone" class="toggle-all" type="checkbox">
复制代码

实现效果: vue3.0 菜鸟初体验 && 响应式原理模拟实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

All/Active/Completed, 切换查看不同状态的事项

vue3.0 菜鸟初体验 && 响应式原理模拟实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

我们通过点击;页面上这三个链接,来获取相对应状态的事项:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

 <ul class="filters">
        <li><a href="#/all">All</a></li>
        <li><a href="#/active">Active</a></li>
        <li><a href="#/completed">Completed</a></li>
      </ul>
复制代码

在点击链接的时候hash 值会发生变化,因此,我们以这一点作为切入点,首先我们开启hash监听文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

onMounted(() => {
    window.addEventListener('hashchange', onHashChange)
    onHashChange()
  }) 
复制代码

onHashChange, 就是我们要实现功能的核心处理函数,我第一反应就是按照下面的逻辑来实现:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

const onHashChange = () => {
    // 获取hash 值
    const hash = window.location.href.replace('#/', '')
    // type.value = hash
    if (hash === 'all') {
	// 此处处理 all 的逻辑
    } else if (hash === 'active') {
	// 此处处理 active 的逻辑
    } else if (hash === 'completed') {
	// 此处处理 completed 的逻辑	
    }
复制代码

这样写也并没有什么问题,就是可能会引入比较多的if...else... 逻辑判断的代码,后面我转换思路,优化了这部分逻辑:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

// 定义一个对象用于存放获取all, active, completed三种不同状态的事项方法
const filter = {
    all: list => list,
    active: list => list.filter(todo => !todo.completed),
    completed: list => list.filter(todo => todo.completed)
  }
// 定义一个存放当前状态的响应对象,默认“显示全部”
  const type = ref('all')
  // 计算属性依赖type , 调用filter对象中对应的处理函数
  const filterTodos = computed(() => filter[type.value](todos.value))
  // 定义一个用于处理hash 变化的处理函数
  const onHashChange = () => {
    // 获取hash 值
    const hash = window.location.hash.replace('#/', '')
    if (filter[hash]) {
       type.value = hash
    }

  }
复制代码

移除事件:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

onUnmounted (() => {
    window.removeEventListener('hashchange', onHashChange)
  })

复制代码

其它功能点

  • 显示未完成的待办项个数
// template 代码
<strong>{{remainTodoCount}}</strong> {{ remainTodoCount > 1 ? 'items': 'item' }} left
// JS 代码
// 未完成的事项个数
  const remainTodoCount = computed(() => filter.active(todos.value).length)
复制代码

vue3.0 菜鸟初体验 && 响应式原理模拟实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

  • 移除所有完成的事项

我们将这个方法放到删除待办事件模块中去,统一管理文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

// 2. 删除待办事件
const useRemove = todos => {
  const remove = todo => {
    // 获取todo的索引
    let index = todos.value.indexOf(todo)
    todos.value.splice(index, 1)
  }
  // 删除已经完成的事项
  const removeCompleted = () => {
    todos.value = todos.value.filter(todo => !todo.completed)
  }
  return {
    remove,
    removeCompleted
  }
}
复制代码

添加一个删除全部已完成的待办的按钮,这个按钮只有当未完成的待办的个数 小于 全部待办的个数时才出现这个按钮,否则隐藏文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

<button class="clear-completed" @click="removeCompleted" v-show="count > remainTodoCount">
         Clean completed
</button>
复制代码

存储待办事项

到目前为止,每次添加好待办事项后,刷新页面,发现原本的数据没有了。因此,我们还需要将待办事项存储到localstorage中去。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

我们都知道,storage 数据的存取在很多场合下都要使用到,因此,我们需要做一个统一的模块来封装这部分逻辑。 在/src/utils下创建了一个名为localStorage.js:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

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

function stringify (obj) {
  let value = null
  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
  }
}
复制代码

在 APP.vue 中导入该模块文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

import useLocalStorage from './utils/localstorage'
复制代码

模块导入完毕之后,记住一定要先调用一下, 我原先直接按照以下的错误方法调用文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

const key ="TODOSKEY"
useLocalStorage.getItem(key)

复制代码

正确的做法应该是文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

// 先调用 useLocalStorage
const storage = useLocalStorage()
复制代码

接着创建一个调用storage 中的存储数据的方法:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

// 5. localstorage存储待办事项
const useStorage = () => {
  const KEY = 'TODOSKEY'
  const todos = ref(storage.getItem(KEY) || [])
  // 待办事项列表一发生改变就触发set
  watchEffect (() => storage.setItem(KEY, todos.value))
  return todos
}
复制代码

到此以完成了所有的功能。具体代码可以访问链接文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

Vue3.0 响应式原理

proxy 使用注意的问题

我们都知道Vue3.0 响应式原理主要借助的是ES6 中的Proxy ,因此,我们首先要对Proxy 对象有个认识,可以参考链接,这里我们不探讨它的用法,我们来介绍使用Proxy 需要注意的两个问题。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

Q1: set 和 deleteProperty 中需要返回布尔类型的值在严格模式下,如果返回 false 的话会出现 Type Error 的异常,看下面的具体代码 非严格模式:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

const target = {
      foo: 'xxx',
      bar: 'yyy'
    }
  
    const proxy = new Proxy(target, {
      get (target, key, receiver) {
        // return target[key]
       return  Reflect.get(target, key, receiver)
      },
      set (target, key, value, receiver) {
        // 
          Reflect.set(target, key, value, receiver)
      },
      deleteProperty (target, key) {
        // delete target[key]
        return Reflect.deleteProperty(target, key)
      }
    })

    proxy.foo = 'zzz'
    console.log(proxy.foo)
    delete proxy.bar
    console.log(proxy.bar) 

复制代码

运行结果:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

vue3.0 菜鸟初体验 && 响应式原理模拟实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

此时,在非严格模式下,set 和 deleteProperty 并没有 return 语句,正常打印了结果出来。再来看严格模式,抛出typeError 的异常了 vue3.0 菜鸟初体验 && 响应式原理模拟实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

可能还是不太理解什么意思,再开看下面代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

 set (target, key, value, receiver) {
        // target[key] = value
        
        // Reflect.set(target, key, value, receiver)
        return false 
      },
复制代码

我们可以直接将set,返回一个false,结果呢? vue3.0 菜鸟初体验 && 响应式原理模拟实现 和上面没有return 一样. 再来看下严格模式下的正常情况:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

 'use strict'
    const target = {
      foo: 'xxx',
      bar: 'yyy'
    }
    // Reflect.getPrototypeOf()
    // Object.getPrototypeOf()
    const proxy = new Proxy(target, {
      get (target, key, receiver) {
        // return target[key]
       return  Reflect.get(target, key, receiver)
      },
      set (target, key, value, receiver) {
        // target[key] = value
        
        return Reflect.set(target, key, value, receiver)
      },
      deleteProperty (target, key) {
        // delete target[key]
         return Reflect.deleteProperty(target, key)
      }
    })

    proxy.foo = 'zzz'
    console.log(proxy.foo)
    delete proxy.bar
    console.log(proxy.bar) 

复制代码

结果: vue3.0 菜鸟初体验 && 响应式原理模拟实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

最后我们可以知道:在严格模式下,Proxy 的set和deleteProperty需要返回一个true,否则会报TypeError文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

Q2: Proxy 和 Reflect 中使用的 receiver文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

看代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

 const obj = {
      get foo() {
        console.log(this)
        return this.bar
      }
    }

    const proxy = new Proxy(obj, {
      get (target, key) {
        if (key === 'bar') { 
          return 'value - bar' 
        }
        return Reflect.get(target, key)
      }
    })
    console.log(proxy.foo)

复制代码

运行结果:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

vue3.0 菜鸟初体验 && 响应式原理模拟实现 分析: 代码在执行到console.log(proxy.foo)时,访问的是obj 中的foo() 此时 this 打印出来的是obj , foo() 中返回的 this.bar ,bar在obj 中未定义,因此 最终foo() 返回undefined文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

接下来引入receiver :文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

 const obj = {
      get foo() {
        console.log(this)
        return this.bar
      }
    }

    const proxy = new Proxy(obj, {
      get (target, key, receiver) {
        if (key === 'bar') { 
          return 'value - bar' 
        }
        return Reflect.get(target, key, receiver)
      }
    })
    console.log(proxy.foo)
复制代码

运行结果:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

vue3.0 菜鸟初体验 && 响应式原理模拟实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

分析:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

代码在执行到console.log(proxy.foo)时,访问的是obj 中的foo() 此时 this 打印出来的是Proxy 对象,在foo() 中访问了 Proxy 中的get 方法, 返回了 value-bar ,最终打印出来文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

因此可以得出:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

  • Proxy 中 receiver:Proxy 或者继承 Proxy 的对象
  • Reflect 中 receiver:如果 target 对象中设置了 getter,getter 中的 this 指向 receiver

Rective 的模拟实现

我们先要明确一下要实现的效果,先看下html 页面的代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script type="module">
    import reactive from './reactivity/index.js'
    const Person = reactive({
      name: {
        firstName: 'Lin',
        lastName: 'Huan'
      },
      age: 29,
      height: 165,
      tel: 18649713338
      })
    
      console.log(Person.name)
      console.log(Person.name.firstName)
      
      Person.age = 18
      console.log(Person.age)

      delete Person.height
      console.log(Person)
  </script>
  
</body>
</html>
复制代码

按照代码的执行顺序,我们来分析:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

  1. console.log(Person.name) 结果:这是一个Proxy 代理对象

vue3.0 菜鸟初体验 && 响应式原理模拟实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

  1. console.log(Person.name.firstName) 结果打印: Linh
  2. 改变age 的值,结果打印: 18
  3. 删除 heigth, 打印Person 对象:

vue3.0 菜鸟初体验 && 响应式原理模拟实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

通过以上,我们要明确reactive 能实现一下几个功能:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

  • 返回的是一个Proxy 对象
  • 需要对嵌套属性实现响应式
  • 只有对对象进行响应式处理
  • 在调用,修改,删除代理对象的值时,可以拦截数据

接下来看代码。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

// 公共工具代码
const isObject = value => value !== null && typeof value === 'object' 
const convert = target => isObject(target) ? reactive(target): target
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target, key) => hasOwnProperty.call(target, key)

export default function reactive (target) {
  // 如果target不是对象,原样返回
  if (!isObject(target)) return target

  // 定义一个处理器
  const handler = {
    get (target, key, receiver) {
      // 依赖收集在这里进行
      console.log(`get:${key}`)
      const result = Reflect.get(target, key, receiver)
      // 是对象的回归调用reactivity()
      return convert(result)
    },
    set (target, key, value, receiver) {
      let result = true
      // target 中是否存在该属性
      // 注意坑:此处不需要判断属性是否存在 error:Uncaught ReferenceError: Cannot access 	  //	'hasOwn' before initialization
      // const hasOwn = hasOwn(target, key)
      // 获取旧值,用于判断新旧值是否一样,不一样重新复值
      const oldValue = Reflect.get(target, key)
      if (hasOwn && value !== oldValue) {
        console.log(`set: ${key}-${value}`)
        result = Reflect.set(target, key, value, receiver)
        // 触发更新在这里进行
      }
      return result
    },
    deleteProperty (target, key) {
      let result = true
      const hasKey = hasOwn(target, key)
      if (hasKey) {
         result = Reflect.deleteProperty(target, key)
        //  触发更新
      }
      return result
    }
  }
  return new Proxy (target, handler)
}

复制代码

运行结果:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

vue3.0 菜鸟初体验 && 响应式原理模拟实现 从结果可见,我们实现了,上文提到的要实现的几个功能点文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

effect 模拟实现watchEffect 的实现

首先我们明确下effect 的功能点:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

  • 传入的回调函数要立即执行
  • 将回调函数缓存起来,提供给依赖收集的时候使用

具体代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

let activeEffect = null
export function effect (callback) {
  activeEffect = callback
  // 立即执行一次回调函数,访问响应式对象的属性 触发依赖收集
  callback()
  // 为了防止嵌套属性,造成死递归
  activeEffect = null
}

复制代码

调用代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

        import {reactive, effect} from './reactivity/index.js'
    const Person = reactive({
      name: {
        firstName: 'Lin',
        lastName: 'Huan'
      },
      age: 29,
      height: 165,
      tel: 18649666638
      })
      let name = null
      effect (() =>{
        name = Person.name.firstName + '-' + Person.name.lastName
      })
      console.log('effect调用:')
      console.log(name)
     
      Person.name.firstName = 'zhang'
      console.log('修改后')
      console.log( 'fistName:',Person.name.firstName)
      console.log("name:", name)
复制代码

运行结果:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

vue3.0 菜鸟初体验 && 响应式原理模拟实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

结果分析:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

  • 目前的effect 函数,只是具有了访问响应式对象属性的能力
  • 对内部依赖的属性,还没有具备监听的能力

依赖收集

在上文中,我们的effect函数,只是实现了基本的回调函数的调用。接下来, 我们来实现一下依赖收集的过程。首先,我们来看一下依赖收集实现的内部数据结构的关系,如下图: vue3.0 菜鸟初体验 && 响应式原理模拟实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

延续上文中的例子,我们现在开始实现依赖收集:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

//  依赖收集
let targetMap = new WeakMap()
function track (target, key) {
  debugger
  // 当前没有需要收集的依赖,直接返回
  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)
}

复制代码

触发更新:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

// 触发更新
function trigger (target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) {
    // 遍历触发所有依赖当前属性的effect函数
    dep.forEach(effect => {
      effect()
    });
  } 
}

复制代码

测试调用:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

import {reactive, effect} from './reactivity/index.js'
    const Person = reactive({
      name: {
        firstName: 'Lin',
        lastName: 'Huan'
      },
      age: 90,
      height: 165,
      tel: 18649666638
      })
      let name = null
      effect (() =>{
        name = Person.name.firstName + '-' + Person.name.lastName
      })
      console.log('effect调用:')
      console.log(name)
     
      Person.name.firstName = 'zhang'
      console.log('修改后')
      console.log( 'fistName:',Person.name.firstName)
      console.log("name:", name)
      
  </script>

复制代码

运行结果:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

vue3.0 菜鸟初体验 && 响应式原理模拟实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

ref 函数模拟实现

ref函数实现功能点分析:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

  • 接收 一个参数,可以是原始值,也可以是对象
  • 参数的情况不同,对应的处理方式:
    • 是ref创建的对象,直接返回
    • 是普通对象,内部调用reactive
    • 原始值,创建一个只有value属性的响应式对像,并返回
  • 依赖收集
  • 触发更新

具体实现代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

// ref
export function ref (raw) {
  // ref创建的对象直接返回
   if (isObject(raw) && raw.__v__isRef__) return
  //  普通对象转化成响应式对象
  let value = convert(raw)
  const r = {
    __v__isRef__: true, // 标记是否是ref 创建的对象
    get value () {
      // 依赖收集
      track(r, 'value')
      return value
    },
    set value (newValue) {
      if (newValue !== value) {
        raw = newValue
        value = convert(raw)
        trigger(r, 'value')
      }
    }
  }
  return r
}  
复制代码

测试代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

 import { reactive, effect, ref } from './reactivity/index.js'

    const price = ref(5000)
    const count = ref(3)
    
    let total = 0 
    effect(() => {
      total = price.value * count.value
    })
    console.log(total)

    price.value = 4000
    console.log(total)

    count.value = 1
    console.log(total)
复制代码

运行结果:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

vue3.0 菜鸟初体验 && 响应式原理模拟实现" alt="" data-data-original="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c6ec8ab6a1f24b40b3b8b174e71d0b54~tplv-k3u1fbpfcp-watermark.image" src="https://www.cainiaoxueyuan.com/wp-content/themes/begin/img/loading.png"height="20" data-width="800" data-height="600" />文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

从结果中可以看出,通过ref, 我们可以将原始值转化为响应式对象,这个值被存放到该对象内部的value属性中,需要使用.value 的方式才能取到这个值,当然Vue3,在模板中直接使用,无须使用这种方式。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

toRefs 函数模拟实现

ref函数实现功能点分析:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

  • 接收一个Reactive 返回的Proxy对象作为参数,否则直接返回
  • 把传入对象的所有属性,转换成类似于 ref 函数返回的对象,并把转换后的对象挂载到新的对象中返回

具体实现代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

// toRefs
export function toRefs (proxy) {
  if ( isObject(proxy) && !proxy.__v__isProxy__ ) {
    console.log('需要传入一个响应式对象')
    return
  }   
  const ret = Array.isArray(proxy) ? new Array(proxy.length) : {}
  // 遍历所有的属性,转换成ref 对象
  for (var key in proxy) {
    ret[key] = toProxyRefs(proxy, key)
  }
  return ret
}
function toProxyRefs (proxy, key) {
  const r = {
    __v__isRef__: true,
    get value () {
      // 此处可以不用做依赖收集,因为proxy是个响应式的对象

      return proxy[key] 
    },
    set value (newValue) {
      if (newValue !== proxy[key]) {
        proxy[key] = newValue  
      }
    }
  }
  return r
}

复制代码

需要修改reactive 返回的Proxy 对象文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

// return new Proxy(target, handler)
return new Proxy(Object.assign(target, {__v__isProxy__: true,}), handler)
复制代码

测试代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

import { reactive, effect, toRefs } from './reactivity/index.js'
    toRefs({})
    function useProduct () {
      const product = reactive({
        name: 'iPhone',
        price: 5000,
        count: 3
      })
      
      return toRefs(product)
    }
    console.dir(useProduct())
    const { price, count } = useProduct()


    let total = 0 
    effect(() => {
      total = price.value * count.value
    })
    console.log(total)

    price.value = 4000
    console.log(total)

    count.value = 1
    console.log(total)

复制代码

运行结果: vue3.0 菜鸟初体验 && 响应式原理模拟实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

从结果中我们可以看出,toRefs将Reactive的对象中的每一个属性都转换成了一个 ref 的响应式对象,这样我们就可以解构出Reactive 中的属性了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

computed 函数 模拟实现

computed 函数实现的功能分析:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

  • 接收一个getter 回调函数
  • 定义一个ref 对象,ref.value 用于存放getter 函数的返回值

具体代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

// computed 
export function computed (getter) {
  // 创建一个value 为 undefined 的对象
  const res = ref()
  effect(() => {
    res.value = getter()
  })
  return res
}

复制代码

测试代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

import { reactive, effect, computed } from './reactivity/index.js'

    const product = reactive({
      name: 'iPhone',
      price: 5000,
      count: 3
    })
    let total = computed(() => {
      return product.price * product.count
    })
    console.log(total.value)
 
    product.price = 4000
    console.log(total.value)

    product.count = 1
    console.log(total.value)
复制代码

运行结果: vue3.0 菜鸟初体验 && 响应式原理模拟实现文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

注意: computed 返回的实际上是一个ref 对象,因此在使用时要通过 .value 的方式才能获取到值文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

作者:linh0801文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html

文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/20757.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/gcs/20757.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定