Object.defineProperty => Proxy
重构了虚拟DOM
OptionApi => Composition API
setup
是干啥的?
setup
实际上是一个组件的入口,它运行在组件被实例化时候,props
属性被定义之后,实际上等价于 2 版本的beforeCreate
和 Created
这两个生命周期。
setup
接受两个参数,第一个参数是props
, 另一个参数是context
,
setup(props, ctx) { console.log(props, ctx) } let Child = { template: `<div>{{title}}</div>`, setup(props, context) { console.log(props) } } let App = { template: ` <div class="container"> <Child title="test props"/> </div>`, components: { Child } } Vue.createApp().mount(App, '#app')
reactive
const { reactive, toRefs } = Vue let App = { template: ` <div class="container"> count: {{count}} <button @click="handlerCountAdd"> Click ++ </button> </div>`, setup() { const state = reactive({ count: 0 }) const handlerCountAdd = () => { state.count++ } return { ...toRefs(state), handlerCountAdd } } } Vue.createApp().mount(App, '#app')
toRefs
vue3提供的ref让我们有机会创建单个的响应式的对象,在setup函数中return出去之后,在模板中可直接访问
const App = { template: ` <div class="container"> {{value}} </div>`, setup() { const value = ref(1) return { value } } } Vue.createApp().mount(App, '#app') const App = { template: ` <div class="container"> {{state.value}} </div>`, setup() { const state = reactive({ value: 'reactive' }) return { state } } } Vue.createApp().mount(App, '#app') const App = { template: ` <div class="container"> {{value}} </div>`, setup() { const state = reactive({ value: 'reactive' }) return toRefs(state) } } Vue.createApp().mount(App, '#app')
反转字符串 demo
let App = { template: ` <div class="container"> value: <input v-model="value"/> <br/> rvalue: {{rvalue}} </div>`, setup() { const state = reactive({ value: '', rvalue: computed(() => state.value .split('') .reverse() .join('') ) }) return toRefs(state) } } Vue.createApp().mount(App, '#app')
数据响应式
在Vue3中实现数据响应式的方案由Vue2中的Object.defineProperty
换成了 Proxy
,关于数据响应式的Api上边说到了一些,还剩下effect
和watch
没有提及到,effect
是数据响应式中重要的一部分,watch
和computed
都是基于 effect
的.
let App = { template: ` <div class="container"> count: {{count}} <button @click="handlerCountAdd"> Click ++ </button> </div>`, setup() { const state = reactive({ count: 0, value: 1 }) const handlerCountAdd = () => { state.count++ } watch( () => state.count, val => { console.log('watch', state.count) console.log('watch', state.value) } ) effect(() => { console.log('effect', state.count) console.log('effect', state.value) }) return { ...toRefs(state), handlerCountAdd } } } Vue.createApp().mount(App, '#app')
effect
在响应式数据变化的时候就会执行,执行次数根据响应式数据的个数来决定
let App = { template: ` <div class="container"> <button @click="handlerCountAdd"> Click ++ </button> </div>`, setup() { const r = ref(1) const s = ref(1) const t = ref(1) const handlerCountAdd = () => { r.value *= 1 s.value *= 2 t.value *= 3 } effect(() => { console.log('effect', [r.value, s.value, t.value]) }) return { handlerCountAdd } } } Vue.createApp().mount(App, '#app')
而watch
则点击一次 ,只会触发执行一次
let App = { template: ` <div class="container"> <button @click="handlerCountAdd"> Click ++ </button> </div>`, setup() { const state = reactive({ count: 0, value: 1 }) const r = ref(1) const s = ref(1) const t = ref(1) const handlerCountAdd = () => { r.value *= 1 s.value *= 2 t.value *= 3 } watch([r, s, t], val => { console.log('watch', val) }) return { handlerCountAdd } } } Vue.createApp().mount(App, '#app')
生命周期
beforeCreate => setup(替代) created => setup(替代) beforeMount => onBeforeMount mounted => onMounted beforeUpdate => onBeforeUpdate updated => onUpdated beforeDestroy => onBeforeUnmount destroyed => onUnmounted errorCaptured => onErrorCaptured
全局配置
Vue2.x
创建实例并且挂载DOM
上
import Vue from "vue"; import App from './App.vue' new Vue({ render: (h) => h(App) }).$mount("#app");
Vue3新增api===>createApp
创建实例
createApp 会产生一个 app 实例,该实例拥有全局的可配置上下文
import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app')
component
Vue2.x
【注册或获取全局组件。注册还会自动使用给定的 id 设置组件的名称】
// 注册组件,传入一个选项对象 (自动调用 Vue.extend) Vue.component('my-component', { /* ... */ }) // 获取注册的组件 (始终返回构造器) var MyComponent = Vue.component('my-component')
Vue3
【注册或获取全局组件注册还会自动使用给定的 name
组件 设置组件的名称】全局组件
基本vue2写法一致
import { createApp } from 'vue' const app = createApp({}) // 注册组件,传入一个选项对象 app.component('my-component', { /* ... */ }) // 获取注册的组件 (始终返回构造器) const MyComponent = app.component('my-component', {})
globalProperties 【新增属性】
app.config.globalProperties.foo = 'bar' app.component('child-component', { mounted() { console.log(this.foo) // 'bar' } })
添加可在程序内的任何组件实例中访问的全局属性。当存在键冲突时,组件属性将优先
替代掉Vue2.x
的 Vue.prototype
属性放到原型上的写法
// Vue2.x Vue.prototype.$http = () => {} // Vue3 const app = Vue.createApp({}) app.config.globalProperties.$http = () => {}
isCustomElement
【新增属性】
替代掉Vue2.x
的ignoredElements
- Vue.config.ignoredElements = [ // 用一个 `RegExp` 忽略所有“ion-”开头的元素 // 仅在 2.5+ 支持 /^ion-/ ] // 一些组件以'ion-'开头将会被解析为自定义组件 + app.config.isCustomElement = tag => tag.startsWith('ion-')
指定一个方法来识别在Vue之外定义的自定义组件(例如,使用Web Component API
)。如果组件符合这个条件,它就不需要本地或全局注册,Vue也不会抛出关于Unknown custom element
的警告
注意,这个函数中不需要匹配所有原生HTML和SVG标记—Vue
解析器会自动执行此检查
optionMergeStrategies
const app = Vue.createApp({ mounted() { console.log(this.$options.hello) } }) app.config.optionMergeStrategies.hello = (parent, child, vm) => { return `Hello, ${child}` } app.mixin({ hello: 'Vue' }) // 'Hello, Vue
定义自定义选项的合并策略。
合并策略接收在父实例options
和∗∗
子实例∗∗options
和子实例options和∗∗子实例∗∗options,分别作为第一个和第二个参数。上下文Vue实例作为第三个参数传递
【自定义选项合并策略】mixin
const app = Vue.createApp({ custom: 'hello!' }) app.config.optionMergeStrategies.custom = (toVal, fromVal) => { console.log(fromVal, toVal) // => "goodbye!", undefined // => "hello!", "goodbye!" return fromVal || toVal } app.mixin({ custom: 'goodbye!', created() { console.log(this.$options.custom) // => "hello!" } })
optionMergeStrategies
先获取到子实例的$options
的mixin而没有父实例
【custom第一次改变从undefined
到goodbye--->
打印"goodbye!", undefined
】
父实例的options替换掉子实例的options替换掉子实例的options替换掉子实例的options
【custom第二次从goodbye到hello!--->打印了"hello", "goodbye!"】
最后在打印`app.config.optionMergeStrategies.custom`返回的父实例的`$options`
无论如何this.options.custom最后会返回合并策略的return的值【使用场景利用父子组件的options.custom最后会返回合并策略的return的值【使用场景利用父子组件的options.custom最后会返回合并策略的return的值【使用场景利用父子组件的options,然后返回计算等操作得到所需要的值】optionMergeStrategies合并$options变化
directive
import { createApp } from 'vue' const app = createApp({}) // 注册 app.directive('my-directive', { // 指令的生命周期 // 在绑定元素的父组件被挂载之前调用 beforeMount(el, binding, vnode) {}, // 在挂载绑定元素的父组件时调用 mounted(el, binding, vnode) {}, // 在更新包含组件的VNode之前调用 beforeUpdate(el, binding, vnode, prevNode) {}, // 组件的VNode及其子组件的VNode更新之后调用 updated(el, binding, vnode, prevNode) {}, // 在卸载绑定元素的父组件之前调用 beforeUnmount(el, binding, vnode) {}, // 在卸载绑定元素的父组件时调用 unmounted(el, binding, vnode) {} }) // 注册 (指令函数) app.directive('my-directive', (el, binding, vnode, prevNode) => { // 这里将会被 `mounted` 和 `updated` 调用 }) // getter,返回已注册的指令 const myDirective = app.directive('my-directive') el
指令绑定到的元素。这可以用来直接操作DOM。
binding【包含下列属性的对象】
instance:使用指令的组件的实例
value:指令的绑定值,例如:v-my-directive="1 + 1"中,绑定值为 2
oldValue:指令绑定的前一个值,仅在 beforeUpdate 和 updated 钩子中可用。无论值是否改变都可用
arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
dir:一个对象,在注册指令时作为参数传递; 举个例子,看下面指令
app.directive('focus', { mounted(el) { el.focus() } })
dir就是下面的对象
{ mounted(el) { el.focus() } } vnode
编译生成的虚拟节点
prevNode
前一个虚拟节点,仅在beforeUpdate和updated钩子中可用
tips:除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行
mount
【类似Vue2.x】
在所提供的DOM
元素上挂载应用程序实例的根组件
import { createApp } from 'vue' const app = createApp({}) // 做一些准备 app.mount('#my-app')
provide/inject
【Vue2.x一致】
该选项与inject一起使用,允许一个祖先组件作为其所有后代的依赖注入器,无论组件层次结构有多深,只要它们位于同一父链中就可以
provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property。在该对象中你可以使用 ES2015Symbols 作为 key,但是只在原生支持 Symbol 和 Reflect.ownKeys 的环境下可工作。
如果在组件中两者都只能在当前活动组件实例的 setup() 中调用,详细请看依赖注入部分
import { createApp } from 'vue' const app = createApp({ provide: { user: 'John Doe' } }) app.component('user-card', { inject: ['user'], template: ` <div> {{ user }} </div> ` })
unmount【新增属性】
在所提供的DOM元素上卸载应用程序实例的根组件
import { createApp } from 'vue' const app = createApp({}) // 做一些必要的准备 app.mount('#my-app') // 应用程序将在挂载后5秒被卸载 setTimeout(() => app.unmount('#my-app'), 5000)
use【Vue2.x一致】
安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。
当 install 方法被同一个插件多次调用,插件将只会被安装一次。
setup
setup 函数是一个新的组件选项。作为在组件内使用 CompositionAPI 的入口点
注意 setup 返回的 ref 在模板中会自动解开,不需要写 .value【setup 内部需要.value】
调用时机
创建组件实例,然后初始化 props ,紧接着就调用setup 函数。从生命周期钩子的视角来看,它会在 beforeCreate 钩子之前被调用
如果 setup 返回一个对象,则对象的属性将会被合并到组件模板的渲染上下文
参数
props 作为其第一个参数
注意 props 对象是响应式的,watchEffect 或 watch 会观察和响应 props 的更新
不要解构 props 对象,那样会使其失去响应性
export default { props: { name: String, }, setup(props) { console.log(props.name) watchEffect(() => { console.log(`name is: ` + props.name) }) }, }
第二个参数提供了一个上下文对象【从原来 2.x 中 this 选择性地暴露了一些 property(attrs/emit/slots)】
attrs 和 slots 都是内部组件实例上对应项的代理,可以确保在更新后仍然是最新值。所以可以解构,无需担心后面访问到过期的值
为什么props作为第一个参数?
组件使用 props 的场景更多,有时候甚至只使用 props
将 props 独立出来作为第一个参数,可以让 TypeScript 对 props 单独做类型推导,不会和上下文中的其他属性相混淆。这也使得 setup 、 render 和其他使用了 TSX 的函数式组件的签名保持一致
this 在 setup() 中不可用。由于 setup() 在解析 2.x 选项前被调用,setup() 中的 this 将与 2.x 选项中的 this 完全不同。同时在 setup() 和 2.x 选项中使用 this 时将造成混乱
setup(props, { attrs }) { // 一个可能之后回调用的签名 function onClick() { console.log(attrs.foo) // 一定是最新的引用,没有丢失响应性 } }
响应式系统 API
reactive
desc: 接收一个普通对象然后返回该普通对象的响应式代理【等同于 2.x 的 Vue.observable()】
tips:Proxy对象是目标对象的一个代理器,任何对目标对象的操作(实例化,添加/删除/修改属性等等),都必须通过该代理器。因此我们可以把来自外界的所有操作进行拦截和过滤或者修改等操作
响应式转换是“深层的”:会影响对象内部所有嵌套的属性。基于 ES2015 的 Proxy 实现,返回的代理对象不等于原始对象。建议仅使用代理对象而避免依赖原始对象
reactive 类的 api 主要提供了将复杂类型的数据处理成响应式数据的能力,其实这个复杂类型是要在object array map set weakmap weakset 这五种之中【如下源码,他会判断是否是五类以及是否被冻结】
因为是组合函数【对象】,所以必须始终保持对这个所返回对象的引用以保持响应性【不能解构该对象或者展开】例如 const { x, y } = useMousePosition()或者return { ...useMousePosition() }
function useMousePosition() { const pos = reactive({ x: 0, y: 0, }) return pos }
toRefs API 用来提供解决此约束的办法——它将响应式对象的每个 property 都转成了相应的 ref【把对象转成了ref】。
function useMousePosition() { const pos = reactive({ x: 0, y: 0, }) return toRefs(pos) } // x & y 现在是 ref 形式了! const { x, y } = useMousePosition()
ref
接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 .value
const count = ref(0) console.log(count.value) // 0
如果传入 ref 的是一个对象,将调用 reactive 方法进行深层响应转换
陷阱
setup 中return返回会自动解套【在模板中不需要.value】
ref 作为 reactive 对象的 property 被访问或修改时,也将自动解套 .value
const count = ref(0) /*当做reactive的对象属性----解套*/ const state = reactive({ count, }) /* 不需要.value*/ console.log(state.count) // 0 /*修改reactive的值*/ state.count = 1 /*修改了ref的值*/ console.log(count.value) // 1
注意如果将一个新的 ref 分配给现有的 ref, 将替换旧的 ref
/*创建一个新的ref*/ const otherCount = ref(2) /*赋值给reactive的旧的ref,旧的会被替换掉*/ state.count = otherCount /*修改reactive会修改otherCount*/ console.log(state.count) // 2 /*修改reactive会count没有被修改 */ console.log(count.value) // 1
嵌套在 reactive Object 中时,ref 才会解套。从 Array 或者 Map 等原生集合类中访问 ref 时,不会自动解套【自由数据类型是Object才会解套,array map set weakmap weakset集合类 访问 ref 时,不会自动解套】
const arr = reactive([ref(0)]) // 这里需要 .value console.log(arr[0].value) const map = reactive(new Map([['foo', ref(0)]])) // 这里需要 .value console.log(map.get('foo').value)
心智负担上 ref vs reactive
在普通 JavaScript 中区别声明基础类型变量与对象变量时一样区别使用 ref 和 reactive
所有的地方都用 reactive,然后记得在组合函数返回响应式对象时使用 toRefs。这降低了一些关于 ref 的心智负担
readonly
传入一个对象(响应式或普通)或 ref,返回一个原始对象的只读代理。一个只读的代理是“深层的”,对象内部任何嵌套的属性也都是只读的【返回一个永远不会变的只读代理】【场景可以参数比对等】
const original = reactive({ count: 0 }) const copy = readonly(original) watchEffect(() => { // 依赖追踪 console.log(copy.count) }) // original 上的修改会触发 copy 上的侦听 original.count++ // 无法修改 copy 并会被警告 copy.count++ // warning! reactive响应式系统工具集 isProxy
检查一个对象是否是由 reactive 或者 readonly 方法创建的代理
isReactive
检查一个对象是否是由 reactive 创建的响应式代理
import { reactive, isReactive } from 'vue' const state = reactive({ name: 'John' }) console.log(isReactive(state)) // -> true
如果这个代理是由 readonly 创建的,但是又被 reactive 创建的另一个代理包裹了一层,那么同样也会返回 true
import { reactive, isReactive, readonly } from 'vue' const state = reactive({ name: 'John' }) // 用readonly创建一个只读响应式对象plain const plain = readonly({ name: 'Mary' }) //readonly创建的,所以isReactive为false console.log(isReactive(plain)) // -> false // reactive创建的响应式代理对象包裹一层readonly,isReactive也是true,isReadonly也是true const stateCopy = readonly(state) console.log(isReactive(stateCopy)) // -> true isReadonly 检查一个对象是否是由 readonly 创建的只读代理 reactive高级响应式系统API toRaw 返回由 reactive 或 readonly 方法转换成响应式代理的普通对象。这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发更改。不建议一直持有原始对象的引用【**不建议赋值给任何变量**】。请谨慎使用
import { reactive, isReactive, readonly } from 'vue' const state = reactive({ name: 'John' }) // 用readonly创建一个只读响应式对象plain const plain = readonly({ name: 'Mary' }) //readonly创建的,所以isReactive为false console.log(isReactive(plain)) // -> false // reactive创建的响应式代理对象包裹一层readonly,isReactive也是true,isReadonly也是true const stateCopy = readonly(state) console.log(isReactive(stateCopy)) // -> true isReadonly
检查一个对象是否是由 readonly 创建的只读代理
reactive高级响应式系统APItoRaw
返回由 reactive 或 readonly 方法转换成响应式代理的普通对象。这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发更改。不建议一直持有原始对象的引用【**不建议赋值给任何变量**】。请谨慎使用
被toRaw之后的对象是没有被代理/跟踪的的普通对象
const foo = {} const reactiveFoo = reactive(foo) console.log(toRaw(reactiveFoo) === foo) // true console.log(toRaw(reactiveFoo) !== reactiveFoo) // true markRaw
显式标记一个对象为“永远不会转为响应式代理”,函数返回这个对象本身。
【markRaw传入对象,返回的值是永远不会被转为响应式代理的】 const foo = markRaw({ name: 'Mary' }) console.log(isReactive(reactive(foo))) // false 被 markRaw 标记了,即使在响应式对象中作属性,也依然不是响应式的 const bar = reactive({ foo }) console.log(isReactive(bar.foo)) // false