vue3.0 菜鸟初体验 && 响应式原理模拟实现
创建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 命令:
#--注意V 是大写--
vue -V
复制代码
因此,先需要全局更新vue-cli版本到最新:
npm install -g @vue/cli
# OR
yarn global add @vue/cli
复制代码
成功更新,运行 vue -V, 结果版本还是我原先的版本,怎么办呢?网上找了好多方法,一顿折腾后,还是没有解决问题,时间也不早,关机睡觉。 重新开机后,重新运行vue -V 结果终于显示了预期的了。
添加待办事项
JS 的逻辑代码:
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 模板代码:
<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
删除待办事项
js 代码:
// 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 调用删除的代码:
<button class="destroy" @click="remove(todo)"></button>
复制代码
编辑待办事件
这个功能比较复杂,我们可以先将要实现的功能列举出来:
- 双击待办项,展示编辑文本框
- 按回车或者编辑文本框失去焦点,修改数据
- 按esc取消编辑
- 把编辑文本框清空按回车,删除这一项
- 显示编辑文本款的时候获取焦点
js代码:
// 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 代码:
<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, 改变所有的待办事项的状态
首先我们分析一下实现这一步,需要考虑到两种情况
1.点击全选框,所有待办选项都要被勾选
- 当选项全被选上时,全选框要被勾选
因此,我们可以定义一个计算属性allDone,绑定全选框。
- 通过get来获取所有项的状态,当所有的选项状态为已完成时,全选框被选中。
- 通过 set 来设置将所有的选项状态为 完成 状态
实现代码:
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">
复制代码
实现效果:
All/Active/Completed, 切换查看不同状态的事项
我们通过点击;页面上这三个链接,来获取相对应状态的事项:
<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监听
onMounted(() => {
window.addEventListener('hashchange', onHashChange)
onHashChange()
})
复制代码
onHashChange, 就是我们要实现功能的核心处理函数,我第一反应就是按照下面的逻辑来实现:
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... 逻辑判断的代码,后面我转换思路,优化了这部分逻辑:
// 定义一个对象用于存放获取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
}
}
复制代码
移除事件:
onUnmounted (() => {
window.removeEventListener('hashchange', onHashChange)
})
复制代码
其它功能点
- 显示未完成的待办项个数
// template 代码
<strong>{{remainTodoCount}}</strong> {{ remainTodoCount > 1 ? 'items': 'item' }} left
// JS 代码
// 未完成的事项个数
const remainTodoCount = computed(() => filter.active(todos.value).length)
复制代码
- 移除所有完成的事项
我们将这个方法放到删除待办事件模块中去,统一管理
// 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
}
}
复制代码
添加一个删除全部已完成的待办的按钮,这个按钮只有当未完成的待办的个数 小于 全部待办的个数时才出现这个按钮,否则隐藏
<button class="clear-completed" @click="removeCompleted" v-show="count > remainTodoCount">
Clean completed
</button>
复制代码
存储待办事项
到目前为止,每次添加好待办事项后,刷新页面,发现原本的数据没有了。因此,我们还需要将待办事项存储到localstorage中去。
我们都知道,storage 数据的存取在很多场合下都要使用到,因此,我们需要做一个统一的模块来封装这部分逻辑。 在/src/utils下创建了一个名为localStorage.js:
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 中导入该模块
import useLocalStorage from './utils/localstorage'
复制代码
模块导入完毕之后,记住一定要先调用一下, 我原先直接按照以下的错误方法调用
const key ="TODOSKEY"
useLocalStorage.getItem(key)
复制代码
正确的做法应该是
// 先调用 useLocalStorage
const storage = useLocalStorage()
复制代码
接着创建一个调用storage 中的存储数据的方法:
// 5. localstorage存储待办事项
const useStorage = () => {
const KEY = 'TODOSKEY'
const todos = ref(storage.getItem(KEY) || [])
// 待办事项列表一发生改变就触发set
watchEffect (() => storage.setItem(KEY, todos.value))
return todos
}
复制代码
到此以完成了所有的功能。具体代码可以访问链接
Vue3.0 响应式原理
proxy 使用注意的问题
我们都知道Vue3.0 响应式原理主要借助的是ES6 中的Proxy ,因此,我们首先要对Proxy 对象有个认识,可以参考链接,这里我们不探讨它的用法,我们来介绍使用Proxy 需要注意的两个问题。
Q1: set 和 deleteProperty 中需要返回布尔类型的值在严格模式下,如果返回 false 的话会出现 Type Error 的异常,看下面的具体代码 非严格模式:
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)
复制代码
运行结果:
此时,在非严格模式下,set 和 deleteProperty 并没有 return 语句,正常打印了结果出来。再来看严格模式,抛出typeError 的异常了
可能还是不太理解什么意思,再开看下面代码:
set (target, key, value, receiver) {
// target[key] = value
// Reflect.set(target, key, value, receiver)
return false
},
复制代码
我们可以直接将set,返回一个false,结果呢? 和上面没有return 一样. 再来看下严格模式下的正常情况:
'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)
复制代码
结果:
最后我们可以知道:在严格模式下,Proxy 的set和deleteProperty需要返回一个true,否则会报TypeError
Q2: Proxy 和 Reflect 中使用的 receiver
看代码:
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)
复制代码
运行结果:
分析: 代码在执行到console.log(proxy.foo)时,访问的是obj 中的foo() 此时 this 打印出来的是obj , foo() 中返回的 this.bar ,bar在obj 中未定义,因此 最终foo() 返回undefined
接下来引入receiver :
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)
复制代码
运行结果:
分析:
代码在执行到console.log(proxy.foo)时,访问的是obj 中的foo() 此时 this 打印出来的是Proxy 对象,在foo() 中访问了 Proxy 中的get 方法, 返回了 value-bar ,最终打印出来
因此可以得出:
- Proxy 中 receiver:Proxy 或者继承 Proxy 的对象
- Reflect 中 receiver:如果 target 对象中设置了 getter,getter 中的 this 指向 receiver
Rective 的模拟实现
我们先要明确一下要实现的效果,先看下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>
复制代码
按照代码的执行顺序,我们来分析:
- console.log(Person.name) 结果:这是一个Proxy 代理对象
- console.log(Person.name.firstName) 结果打印: Linh
- 改变age 的值,结果打印: 18
- 删除 heigth, 打印Person 对象:
通过以上,我们要明确reactive 能实现一下几个功能:
- 返回的是一个Proxy 对象
- 需要对嵌套属性实现响应式
- 只有对对象进行响应式处理
- 在调用,修改,删除代理对象的值时,可以拦截数据
接下来看代码。
// 公共工具代码
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)
}
复制代码
运行结果:
从结果可见,我们实现了,上文提到的要实现的几个功能点
effect 模拟实现watchEffect 的实现
首先我们明确下effect 的功能点:
- 传入的回调函数要立即执行
- 将回调函数缓存起来,提供给依赖收集的时候使用
具体代码:
let activeEffect = null
export function effect (callback) {
activeEffect = callback
// 立即执行一次回调函数,访问响应式对象的属性 触发依赖收集
callback()
// 为了防止嵌套属性,造成死递归
activeEffect = null
}
复制代码
调用代码:
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)
复制代码
运行结果:
结果分析:
- 目前的effect 函数,只是具有了访问响应式对象属性的能力
- 对内部依赖的属性,还没有具备监听的能力
依赖收集
在上文中,我们的effect函数,只是实现了基本的回调函数的调用。接下来, 我们来实现一下依赖收集的过程。首先,我们来看一下依赖收集实现的内部数据结构的关系,如下图:
延续上文中的例子,我们现在开始实现依赖收集:
// 依赖收集
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)
}
复制代码
触发更新:
// 触发更新
function trigger (target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
// 遍历触发所有依赖当前属性的effect函数
dep.forEach(effect => {
effect()
});
}
}
复制代码
测试调用:
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>
复制代码
运行结果:
ref 函数模拟实现
ref函数实现功能点分析:
- 接收 一个参数,可以是原始值,也可以是对象
- 参数的情况不同,对应的处理方式:
- 是ref创建的对象,直接返回
- 是普通对象,内部调用reactive
- 原始值,创建一个只有value属性的响应式对像,并返回
- 依赖收集
- 触发更新
具体实现代码:
// 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
}
复制代码
测试代码:
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)
复制代码
运行结果:
" alt="" data-src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c6ec8ab6a1f24b40b3b8b174e71d0b54~tplv-k3u1fbpfcp-watermark.image" data-width="800" data-height="600" />
从结果中可以看出,通过ref, 我们可以将原始值转化为响应式对象,这个值被存放到该对象内部的value属性中,需要使用.value 的方式才能取到这个值,当然Vue3,在模板中直接使用,无须使用这种方式。
toRefs 函数模拟实现
ref函数实现功能点分析:
- 接收一个Reactive 返回的Proxy对象作为参数,否则直接返回
- 把传入对象的所有属性,转换成类似于 ref 函数返回的对象,并把转换后的对象挂载到新的对象中返回
具体实现代码:
// 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 对象
// return new Proxy(target, handler)
return new Proxy(Object.assign(target, {__v__isProxy__: true,}), handler)
复制代码
测试代码:
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)
复制代码
运行结果:
从结果中我们可以看出,toRefs将Reactive的对象中的每一个属性都转换成了一个 ref 的响应式对象,这样我们就可以解构出Reactive 中的属性了。
computed 函数 模拟实现
computed 函数实现的功能分析:
- 接收一个getter 回调函数
- 定义一个ref 对象,ref.value 用于存放getter 函数的返回值
具体代码:
// computed
export function computed (getter) {
// 创建一个value 为 undefined 的对象
const res = ref()
effect(() => {
res.value = getter()
})
return res
}
复制代码
测试代码:
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)
复制代码
运行结果:
注意: computed 返回的实际上是一个ref 对象,因此在使用时要通过 .value 的方式才能获取到值
作者:linh0801