vue3开发技巧应用

您所在的位置:网站首页 catch三单怎么写 vue3开发技巧应用

vue3开发技巧应用

2023-03-25 17:46| 来源: 网络整理| 查看: 265

渲染函数和template区别h函数生成虚拟DOMmount函数的实现Reactive响应式vue3响应式示例Dep类的实现初步实现Dep类,手动收集依赖和派发更新vue中采用的自动更新Reactive类的实现ES5的defineProperty实现ES6的Proxy实现miniVue基本架构:完整miniVue代码组织和重用 CompositionAPI对比optionAPI和Composite API建立鼠标移动optionAPI逻辑复用mixin使用slot解决复用变量重名问题Composite API逻辑复用Composition API Example

渲染函数和template区别

渲染函数h在vue3中被单独抽离出来,可以单独引入使用。

渲染函数可以处理一些强逻辑性展示的组件,比如渲染嵌套的列表。template多用于简单的纯展示组件,没有强逻辑处理。

渲染函数h用于处理一些无法用template解决的组件。

.mt-4 { margin: 10px; } const {h, createApp} = Vue // 定义 Stack 组件 const Stack = { props: ['size'], render() { // 获取默认插槽 const slot = this.$slots.default ? this.$slots.default() : [] // 将插槽中的所有内容套上 div,并且读取 props 上的 size 属性, // 并构成类名 return h('div', {class: 'stack'}, slot.map(child => { return h('div', {class: `mt-${this.$props.size}`}, [ child ]) })) } } // App 组件 const App = { template: ` hello hello hello `, components: { Stack } } // 创建 vue 实例并挂载到 DOM 上 createApp(App).mount('#app')

h函数生成虚拟DOM

h函数是vue3暴露的一个公开API,可以拥有生成vnode.

// vnode的格式{ tag:"div, props: "", children: []}

```javascript import {h} from “vue” const App = { render() { return h(“div”, {

id: "foo",onClick: this.handleClick

}, “vue3”) } }

## Compiler & Renderer使用[ Vue 3 Template Explorer](https://template-explorer.vuejs.org/)可以看到 Vue 3 把模板template编译成的渲染函数.在处理template到渲染函数的过程,vue3内部进行了很多性能优化设置。给节点标记属性,下次更新时是否进行更新。- 标记出静态节点,下次更新时,不进行diff操作对比。- 标记动态属性,供后边diff使用- 对event进行缓存,在子组件上触发事件时,在patch阶段,不会触发整个子组件的重新渲染。- 引入 block 的概念;动态更新的节点会被添加到 block 上,无论这个节点有多深,v-if 会开启一个新的 block,这个 block 又被其父 block 跟踪;总的来说就是在 diff 的时候不需要在去深度遍历判断,而是从 block 记录的动态节点数组上,去遍历会变化的 vNode```html function h(tag, props, children) {} function mount(vnode, container) {} const vdom = h('div', {class: 'red'}, [ h('span', null, ['hello']) ]) mount(vdom, document.getElementById('app'))

mount函数的实现

mount参数:

VNode:虚拟节点,由h函数生成container:把虚拟节点挂在的dom元素

mount实现的操作流程

根据vnode创建出真实dom设置props属性到dom上处理children将生成的el,append到container上 ```html hello ## patch函数,对比节点并进行更新`patch(oldNode, newNode)`,patch参数:- 第一个为旧虚拟节点- 第二个为新的虚拟节点![](https://cdn.nlark.com/yuque/0/2022/jpeg/737887/1651156644660-ac02e44d-a0e9-4929-986b-8c3abafc57f4.jpeg)```javascript// 将虚拟dom节点转为真实domfunction createElm(vnode) { let { tag, props, children } = vnode; if (typeof tag === "string") { vnode.el = document.createElement(tag); } if (props) { for (let key in props) { let value = props[key]; vnode.el.setAttribute(key, value); } } if (typeof vnode.children === "string") { vnode.el.textContent = vnode.children; } else { children.forEach(child => { return vnode.el.appendChild(createElm(child)); }) } return vnode.el;}// dom diff过程function patch(n1, n2) { if (n1.tag === n2.tag) { // n1.el,此处的el属性就是在mount方法中,第21行设置的。 const el = n2.el = n1.el; //把n1的el真实dom内容,赋值给n2的el属性和单独的el对象上 const oldProps = n1.props || {}; const newProps = n2.props || {}; for (const key in newProps) { const oldValue = oldProps[key]; const newValue = newProps[key]; if (newValue !== oldValue) { el.setAttribute(key, newValue) } } // 处理新节点中不存在的属性,直接将属性移除 for (const key in oldProps) { if (!(key in newProps)) { el.removeAttribute(key) } } const oldChildren = n1.children; const newChildren = n2.children; if (typeof newChildren === "string") { // 新节点是字符串,直接删除旧节点,并使新接的文本 if (typeof oldChildren === "string") { if (newChildren !== oldChildren) { el.textContent = newChildren } } else { //旧节点不是字符串,说明包含多个子节点。同样也直接删除 el.textContent = newChildren } } else { // if (typeof oldChildren === "string") { //旧节点是字符串,新节点是多个子元素 el.innerHTML = ''; newChildren.forEach(child => { mount(child, el) }) } else { //旧节点多个子元素,新节点多个子元素 // 找出新旧节点最长的共用长度 const commonLength = Math.min(oldChildren.length, newChildren.length); // 比对公共长度的节点 for(let i = 0; i < commonLength; i++) { patch(oldChildren[i], newChildren[i]); } // 如果新节点长度大于旧节点长度 if(newChildren.length > oldChildren.length){ newChildren.slice(oldChildren.length).forEach(child=>{ mount(child, el) }) } // 如果旧节点长度大于新节点长度 if(newChildren.length < oldChildren.length){ oldChildren.slice(newChildren).forEach(child=>{ el.removeChild(child.el) }) } } } } else { // 直接替换replace n1.el.parentNode.replaceChild(createElm(vdom2), n1.el); }}const vdom = h('div', { class: 'red'}, [ h('span', null, 'hello')])const vdom2 = h('p', { class: 'blue'}, [ h('span', null, 'changed'), h('p', {class: 'red'}, 'changed1'),])// console.log(createElm(vdom2), 'dom2');mount(vdom, document.getElementById('app'))

Reactive响应式

一个值发生变化,依赖该值的数据会自动发生变化。

// let a = 10;// let b = a *2;// console.log(b) //20// a = 15; // 如何让b的值变成30;// console.log(b) ?==30onAChanged(() => { b = a *2;})

vue3响应式示例import { ractive, watchEffect } from 'vue'// 创建一个响应式对象const state = reactive({ count: 0})// 会收集所有的依赖,在执行过程,如果某个响应式属性被使用,那么整个函数就会执行// 相当于上面提到的 onAChangedwatchEffect(() => { console.log(state.count)}) // 0state.count++ // 1

Dep类的实现

Dep类的两个方法:depend和notify;

depend:用于添加追踪依赖

notify:用于更新依赖的属性

初步实现Dep类,手动收集依赖和派发更新let activeEffect = null;class Dep { constructor(value){ this.subscribers = new Set(); this.value = value; } depend() { if (activeEffect) { this.subscribers.add(activeEffect); } } notify() { this.subscribers.forEach((effect) => effect()); }}// 和onAChanged作用一样,自动更新依赖属性function watchEffect(effect) { activeEffect = effect; effect(); //首次是 自己执行 触发effect,以后数据发生变化就走notify中触发effect activeEffect = null;}const dep = new Dep('hello');watchEffect(() => { dep.depend(); console.log("effect run", dep.value);});// 手动执行的更新dep.value = "world";dep.notify();

vue中采用的自动更新let activeEffect = null;class Dep { constructor(value){ this.subscribers = new Set(); this._value = value; } get value(){ this.depend(); //自动执行depend,收集依赖 return this._value; } set value(newValue){ this._value = newValue; this.notify(); //自动执行派发更新, notify } depend() { if (activeEffect) { this.subscribers.add(activeEffect); } } notify() { this.subscribers.forEach((effect) => effect()); }}// 和onAChanged作用一样,自动更新依赖属性function watchEffect(effect) { activeEffect = effect; // 这里就是watchEffect中的监听会默认先执行一次;watch不能执行,必须设置immdiate才能立即执行 effect(); //首次是 自己执行 触发effect,以后数据发生变化就走notify中触发effect activeEffect = null;}const dep = new Dep('hello');watchEffect(() => { dep.depend(); console.log("effect run", dep.value);});// 手动执行的更新dep.value = "world";dep.notify();

Reactive类的实现

利用上面实现的Dep类,完成数据响应式的方法函数reactive。响应式数据的值是数据本身,所以创建的Dep类可以简化,不需要value值。

let activeEffect = null;class Dep{subscribers = new Set();depend() { if (activeEffect) { this.subscribers.add(activeEffect); }}notify() { this.subscribers.forEach((effect) => effect());}}function watchEffect(effect) {activeEffect = effect;effect();activeEffect = null;}

ES5的defineProperty实现

在Object.defineProperty的get/set中 进行依赖收集/派发更新。

let activeEffect = null;class Dep { subscribers = new Set(); depend() { if (activeEffect) { this.subscribers.add(activeEffect); } } notify() { this.subscribers.forEach((effect) => effect()); }}function watchEffect(effect) { activeEffect = effect; effect(); activeEffect = null;}function reactive(raw) { // 获取对象key Object.keys(raw).forEach((key) => { let value = raw[key]; let dep = new Dep(); Object.defineProperty(raw, key, { get() { dep.depend(); return value; }, set(newValue) { // 先进行 赋值 value = newValue; // 再进行更新 dep.notify(); }, }); }); return raw;}let state = reactive({ count: 0,});watchEffect(() => { console.log(state.count);});state.count++;

defineProperty的缺陷,无法拦截新增属性,和数组的push、pop、shift、unshfit、sort、reverse、splice方法。

ES6的Proxy实现

使用Proxy对象进行代理,代理的是整个对象的值,可以解决defineProperty的缺陷。

let activeEffect = null;class Dep { subscribers = new Set(); depend() { if (activeEffect) { this.subscribers.add(activeEffect); } } notify() { this.subscribers.forEach((effect) => effect()); }}function watchEffect(effect) { activeEffect = effect; effect(); activeEffect = null;}function searchDep(target, key) { 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 Dep())); } return dep;}// 创建一个存放 deps 的弱引用 Map,key 为 target 本身// 即需要响应式处理的对象本身// WeakMap 只能用 object 作为 key,并且无法被遍历// 当 target 不再需要的时候,可以正确地被垃圾处理机制回收let targetMap = new WeakMap();function reactive(raw) { return new Proxy(raw, { get(target, key, receiver) { let dep = searchDep(target, key); dep.depend(); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { let dep = searchDep(target, key); let res = Reflect.set(target, key, value, receiver); dep.notify(); return res; }, });}let state = reactive({ count: 0,});watchEffect(() => { console.log(state.count, "watchEffect");});state.count++;

这样就可以监听到新增属性的数据变化。

miniVue

实现一个Vue,主要采用上面已经实现的方法。

基本架构:

```html

### 具体实现#### 修正mount方法,满足处理事件```javascriptfunction mount(vnode, container) { const tag = vnode.tag; const el = (vnode.el = document.createElement(tag)); // 在vnode上添加el属性,用来存储原dom结构 // props if (vnode.props) { for (let key in vnode.props) { let value = vnode.props[key]; // 添加对事件属性的处理+ if (key.startsWith("on")) {+ el.addEventListener(key.slice(2).toLowerCase(), value);+ } else { el.setAttribute(key, value);+ } } } // children if (vnode.children) { if (typeof vnode.children === "string") { el.textContent = vnode.children; } else { vnode.children.forEach((child) => { mount(child, el); }); } } container.appendChild(el);}

完整miniVue // vDom function h(tag, props, children) { return { tag, props, children, }; } function mount(vnode, container) { const tag = vnode.tag; // 在vnode上添加el属性,用来存储原dom结构 const el = (vnode.el = document.createElement(tag)); // props if (vnode.props) { for (let key in vnode.props) { let value = vnode.props[key]; // 添加对事件属性的处理 if (key.startsWith("on")) { el.addEventListener(key.slice(2).toLowerCase(), value); } else { el.setAttribute(key, value); } } } // children if (vnode.children) { if (typeof vnode.children === "string") { el.textContent = vnode.children; } else { vnode.children.forEach((child) => { mount(child, el); }); } } container.appendChild(el); } // 将虚拟dom节点转为真实dom function createElm(vnode) { let { tag, props, children } = vnode; if (typeof tag === "string") { vnode.el = document.createElement(tag); } if (props) { for (let key in props) { let value = props[key]; vnode.el.setAttribute(key, value); } } if (typeof vnode.children === "string") { vnode.el.textContent = vnode.children; } else { children.forEach((child) => { return vnode.el.appendChild(createElm(child)); }); } return vnode.el; } // dom diff过程 function patch(n1, n2) { if (n1.tag === n2.tag) { //把n1的el真实dom内容,赋值给n2的el属性和单独的el对象上 const el = (n2.el = n1.el); const oldProps = n1.props || {}; const newProps = n2.props || {}; for (const key in newProps) { const oldValue = oldProps[key]; const newValue = newProps[key]; if (newValue !== oldValue) { el.setAttribute(key, newValue); } } // 处理新节点中不存在的属性,直接将属性移除 for (const key in oldProps) { if (!(key in newProps)) { el.removeAttribute(key); } } const oldChildren = n1.children; const newChildren = n2.children; if (typeof newChildren === "string") { // 新节点是字符串,直接删除旧节点,并使新接的文本 if (typeof oldChildren === "string") { if (newChildren !== oldChildren) { el.textContent = newChildren; } } else { //旧节点不是字符串,说明包含多个子节点。同样也直接删除 el.textContent = newChildren; } } else { if (typeof oldChildren === "string") { //旧节点是字符串,新节点是多个子元素 el.innerHTML = ""; newChildren.forEach((child) => { mount(child, el); }); } else { //旧节点多个子元素,新节点多个子元素 // 找出新旧节点最长的共用长度 const commonLength=Math.min(oldChildren.length, newChildren.length); // 比对公共长度的节点 for (let i = 0; i oldChildren.length) { newChildren.slice(oldChildren.length).forEach((child) => { mount(child, el); }); } // 如果旧节点长度大于新节点长度 if (newChildren.length { el.removeChild(child.el); }); } } } } else { // 直接替换replace n1.el.parentNode.replaceChild(createElm(vdom2), n1.el); } } // reactivity let activeEffect = null; class Dep { subscribers = new Set(); depend() { if (activeEffect) { this.subscribers.add(activeEffect); } } notify() { this.subscribers.forEach((effect) => effect()); } } function watchEffect(effect) { activeEffect = effect; effect(); activeEffect = null; } function searchDep(target, key) { 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 Dep())); } return dep; } let targetMap = new WeakMap(); function reactive(raw) { return new Proxy(raw, { get(target, key, receiver) { let dep = searchDep(target, key); dep.depend(); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { let dep = searchDep(target, key); let res = Reflect.set(target, key, value, receiver); dep.notify(); return res; }, }); } // App 组件 const App = { data: reactive({ count: 0, }), render() { return h( "div", { // 这里需要在 mount 中添加事件处理 onClick: () => { this.data.count++; }, }, String(this.data.count) ); // 第三个参数这里暂时只支持 String类型 }, }; // 挂载 App function createApp(component, container) { let isMounted = false; let prevVdom; // component 组件中有响应式对象发生变化,便会执行以下函数 watchEffect(() => { if (!isMounted) { // 没有挂载,即初始化 // 记录旧的 vdom prevVdom = component.render(); // 挂载 mount(prevVdom, container); isMounted = true; } else { // 获取新的 vdom const newVdom = component.render(); // patch patch(prevVdom, newVdom); prevVdom = newVdom; } }); }createApp(App, document.getElementById("app"));

新增App组件,和createApp的方法。App组件包含:data数据,render方法。createApp:创建并挂载实例

代码组织和重用 CompositionAPI

composition组合式API,可以任意复用,实质上就是函数的调用。

function useFeature(){ onMounted(()=> console.log("mounted"))}export default{ tempalte: `{{event.count}}`, props: ["id"], setup(props){ // 方便复用 useFeature(); return { event: {count: ref(0)} } }}

对比optionAPI和Composite API建立鼠标移动 const { createApp } = Vue; const App = { template: `{{x}} {{y}}`, data() { return { x: 0, y: 0, }; }, methods: { update(e) { this.x = e.clientX; this.y = e.clientY; }, }, mounted() { window.addEventListener("mousemove", this.update); }, unmounted() { window.addEventListener("mousemove", this.update); }, }; createApp(App).mount("#app");

optionAPI逻辑复用mixin

为了实现逻辑复用,可以使用mixin,但是当存在多个mixin引入使用时,没法区分变量来自哪个mixin。

const { createApp } = Vue; const mouseMoveMixin = { data() { return { x: 0, y: 0, }; }, methods: { update(e) { this.x = e.clientX; this.y = e.clientY; }, }, mounted() { window.addEventListener("mousemove", this.update); }, unmounted() { window.addEventListener("mousemove", this.update); }, }; const otherMixin = { data() { return { x: 0, y: 0, }; }, methods: { update(e) { this.x = e.clientX + 'px'; this.y = e.clientY + 'px'; }, }, mounted() { window.addEventListener("mousemove", this.update); }, unmounted() { window.addEventListener("mousemove", this.update); }, }; const App = { // 多个mixin同时存在,如果变量名重复出现,后边的会覆盖前面。 template: `{{x}} {{y}}`, mixins: [mouseMoveMixin, otherMixin], }; createApp(App).mount("#app");

使用slot解决复用变量重名问题 const { createApp } = Vue; const Mouse = { data() { return { x: 0, y: 0, }; }, methods: { update(e) { this.x = e.clientX; this.y = e.clientY; }, }, mounted() { window.addEventListener("mousemove", this.update); }, unmounted() { window.addEventListener("mousemove", this.update); }, render() { return ( this.$slots.default && this.$slots.default({ x: this.x, y: this.y, }) ); }, }; const Other = { data() { return { x: 0, y: 0, }; }, methods: { update(e) { this.x = e.clientX + "px"; this.y = e.clientY + "px"; }, }, mounted() { window.addEventListener("mousemove", this.update); }, unmounted() { window.addEventListener("mousemove", this.update); }, render() { return ( this.$slots.default && this.$slots.default({ x: this.x, y: this.y, }) ); }, }; const App = { components: { Mouse, Other }, template: ` {{x}} {{y}} --- {{otherX}} {{otherY}} `, }; createApp(App).mount("#app");

Composite API逻辑复用 const { createApp, ref, onUnmounted, onMounted } = Vue; const useMouse = () => { const x = ref(0); const y = ref(0); const update = (e) => { x.value = e.clientX; y.value = e.clientY; }; onMounted(() => { window.addEventListener("mousemove", update); }); onUnmounted(() => { window.removeEventListener("mousemove", update); }); return { x, y }; }; const App = { template: `{{x}} {{y}}`, setup() { // 可以在此处修改引入的变量名 let { x, y } = useMouse(); return { x, y }; }, }; createApp(App).mount("#app");

Composition API Example const { createApp, ref, watchEffect } = Vue; // 进一步简化在组件中的 use function usePost(getId) { return useFetch( () => `https://jsonplaceholder.typicode.com/todos/${getId()}` ); } // 抽出 fetch,并且你可以在的 useFetch 中使用 watchEffect 来监听传进来的值的变化 function useFetch(getUrl) { const data = ref(null); const error = ref(null); const isPending = ref(true); watchEffect(() => { // reset data.value = null; error.value = null; isPending.value = true; // fetch fetch(getUrl()) .then((res) => res.json()) .then((_data) => { data.value = _data; }) .catch((err) => { error.value = err; }) .finally(() => { isPending.value = false; }); }); return { data, error, isPending, }; } const Post = { template: ` loading {{ data }} Something went Wrong: {{ error.message }} `, props: ["id"], setup(props) { // prop.id 被传到了 useFetch 的 watchEffect 中 // 所以 prop.id 变化,即可重新 fetch const { data, error, isPending } = usePost(() => props.id); return { data, error, isPending, }; }, }; const App = { components: { Post }, data() { return { id: 1, }; }, template: ` change ID `, }; createApp(App).mount("#app");

代码地址视频参考



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3