渲染函数和template区别

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

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

    1. <script src="https://unpkg.com/vue"></script>
    2. <style>
    3. .mt-4 {
    4. margin: 10px;
    5. }
    6. </style>
    7. <div id="app"></div>
    8. <script>
    9. const {h, createApp} = Vue
    10. // 定义 Stack 组件
    11. const Stack = {
    12. props: ['size'],
    13. render() {
    14. // 获取默认插槽
    15. const slot = this.$slots.default
    16. ? this.$slots.default()
    17. : []
    18. // 将插槽中的所有内容套上 div,并且读取 props 上的 size 属性,
    19. // 并构成类名
    20. return h('div', {class: 'stack'}, slot.map(child => {
    21. return h('div', {class: `mt-${this.$props.size}`}, [
    22. child
    23. ])
    24. }))
    25. }
    26. }
    27. // App 组件
    28. const App = {
    29. template: `
    30. <Stack size="4">
    31. <div>hello</div>
    32. <Stack size="4">
    33. <div>hello</div>
    34. <div>hello</div>
    35. </Stack>
    36. </Stack>
    37. `,
    38. components: {
    39. Stack
    40. }
    41. }
    42. // 创建 vue 实例并挂载到 DOM 上
    43. createApp(App).mount('#app')
    44. </script>

    h函数生成虚拟DOM

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

    1. // vnode的格式
    2. {
    3. tag:"div,
    4. props: "",
    5. children: []
    6. }

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

    1. id: "foo",
    2. onClick: this.handleClick

    }, “vue3”) } }

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

mount函数的实现

mount参数:

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

mount实现的操作流程

  1. 根据vnode创建出真实dom
  2. 设置props属性到dom上
  3. 处理children
  4. 将生成的el,append到container上 ```html
    hello
  1. <a name="yrkMU"></a>
  2. ## patch函数,对比节点并进行更新
  3. `patch(oldNode, newNode)`,patch参数:
  4. - 第一个为旧虚拟节点
  5. - 第二个为新的虚拟节点
  6. ![](https://cdn.nlark.com/yuque/0/2022/jpeg/737887/1651156644660-ac02e44d-a0e9-4929-986b-8c3abafc57f4.jpeg)
  7. ```javascript
  8. // 将虚拟dom节点转为真实dom
  9. function createElm(vnode) {
  10. let {
  11. tag,
  12. props,
  13. children
  14. } = vnode;
  15. if (typeof tag === "string") {
  16. vnode.el = document.createElement(tag);
  17. }
  18. if (props) {
  19. for (let key in props) {
  20. let value = props[key];
  21. vnode.el.setAttribute(key, value);
  22. }
  23. }
  24. if (typeof vnode.children === "string") {
  25. vnode.el.textContent = vnode.children;
  26. } else {
  27. children.forEach(child => {
  28. return vnode.el.appendChild(createElm(child));
  29. })
  30. }
  31. return vnode.el;
  32. }
  33. // dom diff过程
  34. function patch(n1, n2) {
  35. if (n1.tag === n2.tag) {
  36. // n1.el,此处的el属性就是在mount方法中,第21行设置的。
  37. const el = n2.el = n1.el; //把n1的el真实dom内容,赋值给n2的el属性和单独的el对象上
  38. const oldProps = n1.props || {};
  39. const newProps = n2.props || {};
  40. for (const key in newProps) {
  41. const oldValue = oldProps[key];
  42. const newValue = newProps[key];
  43. if (newValue !== oldValue) {
  44. el.setAttribute(key, newValue)
  45. }
  46. }
  47. // 处理新节点中不存在的属性,直接将属性移除
  48. for (const key in oldProps) {
  49. if (!(key in newProps)) {
  50. el.removeAttribute(key)
  51. }
  52. }
  53. const oldChildren = n1.children;
  54. const newChildren = n2.children;
  55. if (typeof newChildren === "string") { // 新节点是字符串,直接删除旧节点,并使新接的文本
  56. if (typeof oldChildren === "string") {
  57. if (newChildren !== oldChildren) {
  58. el.textContent = newChildren
  59. }
  60. } else { //旧节点不是字符串,说明包含多个子节点。同样也直接删除
  61. el.textContent = newChildren
  62. }
  63. } else { //
  64. if (typeof oldChildren === "string") { //旧节点是字符串,新节点是多个子元素
  65. el.innerHTML = '';
  66. newChildren.forEach(child => {
  67. mount(child, el)
  68. })
  69. } else { //旧节点多个子元素,新节点多个子元素
  70. // 找出新旧节点最长的共用长度
  71. const commonLength = Math.min(oldChildren.length, newChildren.length);
  72. // 比对公共长度的节点
  73. for(let i = 0; i < commonLength; i++) {
  74. patch(oldChildren[i], newChildren[i]);
  75. }
  76. // 如果新节点长度大于旧节点长度
  77. if(newChildren.length > oldChildren.length){
  78. newChildren.slice(oldChildren.length).forEach(child=>{
  79. mount(child, el)
  80. })
  81. }
  82. // 如果旧节点长度大于新节点长度
  83. if(newChildren.length < oldChildren.length){
  84. oldChildren.slice(newChildren).forEach(child=>{
  85. el.removeChild(child.el)
  86. })
  87. }
  88. }
  89. }
  90. } else {
  91. // 直接替换replace
  92. n1.el.parentNode.replaceChild(createElm(vdom2), n1.el);
  93. }
  94. }
  95. const vdom = h('div', {
  96. class: 'red'
  97. }, [
  98. h('span', null, 'hello')
  99. ])
  100. const vdom2 = h('p', {
  101. class: 'blue'
  102. }, [
  103. h('span', null, 'changed'),
  104. h('p', {class: 'red'}, 'changed1'),
  105. ])
  106. // console.log(createElm(vdom2), 'dom2');
  107. mount(vdom, document.getElementById('app'))

Reactive响应式

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

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

vue3响应式示例

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

Dep类的实现

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

  • depend:用于添加追踪依赖
  • notify:用于更新依赖的属性

    初步实现Dep类,手动收集依赖和派发更新

    1. let activeEffect = null;
    2. class Dep {
    3. constructor(value){
    4. this.subscribers = new Set();
    5. this.value = value;
    6. }
    7. depend() {
    8. if (activeEffect) {
    9. this.subscribers.add(activeEffect);
    10. }
    11. }
    12. notify() {
    13. this.subscribers.forEach((effect) => effect());
    14. }
    15. }
    16. // 和onAChanged作用一样,自动更新依赖属性
    17. function watchEffect(effect) {
    18. activeEffect = effect;
    19. effect(); //首次是 自己执行 触发effect,以后数据发生变化就走notify中触发effect
    20. activeEffect = null;
    21. }
    22. const dep = new Dep('hello');
    23. watchEffect(() => {
    24. dep.depend();
    25. console.log("effect run", dep.value);
    26. });
    27. // 手动执行的更新
    28. dep.value = "world";
    29. dep.notify();

    vue中采用的自动更新

    1. let activeEffect = null;
    2. class Dep {
    3. constructor(value){
    4. this.subscribers = new Set();
    5. this._value = value;
    6. }
    7. get value(){
    8. this.depend(); //自动执行depend,收集依赖
    9. return this._value;
    10. }
    11. set value(newValue){
    12. this._value = newValue;
    13. this.notify(); //自动执行派发更新, notify
    14. }
    15. depend() {
    16. if (activeEffect) {
    17. this.subscribers.add(activeEffect);
    18. }
    19. }
    20. notify() {
    21. this.subscribers.forEach((effect) => effect());
    22. }
    23. }
    24. // 和onAChanged作用一样,自动更新依赖属性
    25. function watchEffect(effect) {
    26. activeEffect = effect;
    27. // 这里就是watchEffect中的监听会默认先执行一次;watch不能执行,必须设置immdiate才能立即执行
    28. effect(); //首次是 自己执行 触发effect,以后数据发生变化就走notify中触发effect
    29. activeEffect = null;
    30. }
    31. const dep = new Dep('hello');
    32. watchEffect(() => {
    33. dep.depend();
    34. console.log("effect run", dep.value);
    35. });
    36. // 手动执行的更新
    37. dep.value = "world";
    38. dep.notify();

    Reactive类的实现

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

    1. let activeEffect = null;
    2. class Dep{
    3. subscribers = new Set();
    4. depend() {
    5. if (activeEffect) {
    6. this.subscribers.add(activeEffect);
    7. }
    8. }
    9. notify() {
    10. this.subscribers.forEach((effect) => effect());
    11. }
    12. }
    13. function watchEffect(effect) {
    14. activeEffect = effect;
    15. effect();
    16. activeEffect = null;
    17. }

    ES5的defineProperty实现

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

    1. <div id="app"></div>
    2. <script>
    3. let activeEffect = null;
    4. class Dep {
    5. subscribers = new Set();
    6. depend() {
    7. if (activeEffect) {
    8. this.subscribers.add(activeEffect);
    9. }
    10. }
    11. notify() {
    12. this.subscribers.forEach((effect) => effect());
    13. }
    14. }
    15. function watchEffect(effect) {
    16. activeEffect = effect;
    17. effect();
    18. activeEffect = null;
    19. }
    20. function reactive(raw) {
    21. // 获取对象key
    22. Object.keys(raw).forEach((key) => {
    23. let value = raw[key];
    24. let dep = new Dep();
    25. Object.defineProperty(raw, key, {
    26. get() {
    27. dep.depend();
    28. return value;
    29. },
    30. set(newValue) {
    31. // 先进行 赋值
    32. value = newValue;
    33. // 再进行更新
    34. dep.notify();
    35. },
    36. });
    37. });
    38. return raw;
    39. }
    40. let state = reactive({
    41. count: 0,
    42. });
    43. watchEffect(() => {
    44. console.log(state.count);
    45. });
    46. state.count++;
    47. </script>

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

    ES6的Proxy实现

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

    1. <div id="app"></div>
    2. <script>
    3. let activeEffect = null;
    4. class Dep {
    5. subscribers = new Set();
    6. depend() {
    7. if (activeEffect) {
    8. this.subscribers.add(activeEffect);
    9. }
    10. }
    11. notify() {
    12. this.subscribers.forEach((effect) => effect());
    13. }
    14. }
    15. function watchEffect(effect) {
    16. activeEffect = effect;
    17. effect();
    18. activeEffect = null;
    19. }
    20. function searchDep(target, key) {
    21. let depsMap = targetMap.get(target);
    22. if (!depsMap) {
    23. targetMap.set(target, (depsMap = new Map()));
    24. }
    25. let dep = depsMap.get(key);
    26. if (!dep) {
    27. depsMap.set(key, (dep = new Dep()));
    28. }
    29. return dep;
    30. }
    31. // 创建一个存放 deps 的弱引用 Map,key 为 target 本身
    32. // 即需要响应式处理的对象本身
    33. // WeakMap 只能用 object 作为 key,并且无法被遍历
    34. // 当 target 不再需要的时候,可以正确地被垃圾处理机制回收
    35. let targetMap = new WeakMap();
    36. function reactive(raw) {
    37. return new Proxy(raw, {
    38. get(target, key, receiver) {
    39. let dep = searchDep(target, key);
    40. dep.depend();
    41. return Reflect.get(target, key, receiver);
    42. },
    43. set(target, key, value, receiver) {
    44. let dep = searchDep(target, key);
    45. let res = Reflect.set(target, key, value, receiver);
    46. dep.notify();
    47. return res;
    48. },
    49. });
    50. }
    51. let state = reactive({
    52. count: 0,
    53. });
    54. watchEffect(() => {
    55. console.log(state.count, "watchEffect");
    56. });
    57. state.count++;
    58. </script>

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

    miniVue

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

    基本架构:

    ```html

  1. <a name="gNeCR"></a>
  2. ### 具体实现
  3. <a name="SM1t0"></a>
  4. #### 修正mount方法,满足处理事件
  5. ```javascript
  6. function mount(vnode, container) {
  7. const tag = vnode.tag;
  8. const el = (vnode.el = document.createElement(tag)); // 在vnode上添加el属性,用来存储原dom结构
  9. // props
  10. if (vnode.props) {
  11. for (let key in vnode.props) {
  12. let value = vnode.props[key];
  13. // 添加对事件属性的处理
  14. + if (key.startsWith("on")) {
  15. + el.addEventListener(key.slice(2).toLowerCase(), value);
  16. + } else {
  17. el.setAttribute(key, value);
  18. + }
  19. }
  20. }
  21. // children
  22. if (vnode.children) {
  23. if (typeof vnode.children === "string") {
  24. el.textContent = vnode.children;
  25. } else {
  26. vnode.children.forEach((child) => {
  27. mount(child, el);
  28. });
  29. }
  30. }
  31. container.appendChild(el);
  32. }

完整miniVue

  1. <div id="app"></div>
  2. <script>
  3. // vDom
  4. function h(tag, props, children) {
  5. return {
  6. tag,
  7. props,
  8. children,
  9. };
  10. }
  11. function mount(vnode, container) {
  12. const tag = vnode.tag;
  13. // 在vnode上添加el属性,用来存储原dom结构
  14. const el = (vnode.el = document.createElement(tag));
  15. // props
  16. if (vnode.props) {
  17. for (let key in vnode.props) {
  18. let value = vnode.props[key];
  19. // 添加对事件属性的处理
  20. if (key.startsWith("on")) {
  21. el.addEventListener(key.slice(2).toLowerCase(), value);
  22. } else {
  23. el.setAttribute(key, value);
  24. }
  25. }
  26. }
  27. // children
  28. if (vnode.children) {
  29. if (typeof vnode.children === "string") {
  30. el.textContent = vnode.children;
  31. } else {
  32. vnode.children.forEach((child) => {
  33. mount(child, el);
  34. });
  35. }
  36. }
  37. container.appendChild(el);
  38. }
  39. // 将虚拟dom节点转为真实dom
  40. function createElm(vnode) {
  41. let { tag, props, children } = vnode;
  42. if (typeof tag === "string") {
  43. vnode.el = document.createElement(tag);
  44. }
  45. if (props) {
  46. for (let key in props) {
  47. let value = props[key];
  48. vnode.el.setAttribute(key, value);
  49. }
  50. }
  51. if (typeof vnode.children === "string") {
  52. vnode.el.textContent = vnode.children;
  53. } else {
  54. children.forEach((child) => {
  55. return vnode.el.appendChild(createElm(child));
  56. });
  57. }
  58. return vnode.el;
  59. }
  60. // dom diff过程
  61. function patch(n1, n2) {
  62. if (n1.tag === n2.tag) {
  63. //把n1的el真实dom内容,赋值给n2的el属性和单独的el对象上
  64. const el = (n2.el = n1.el);
  65. const oldProps = n1.props || {};
  66. const newProps = n2.props || {};
  67. for (const key in newProps) {
  68. const oldValue = oldProps[key];
  69. const newValue = newProps[key];
  70. if (newValue !== oldValue) {
  71. el.setAttribute(key, newValue);
  72. }
  73. }
  74. // 处理新节点中不存在的属性,直接将属性移除
  75. for (const key in oldProps) {
  76. if (!(key in newProps)) {
  77. el.removeAttribute(key);
  78. }
  79. }
  80. const oldChildren = n1.children;
  81. const newChildren = n2.children;
  82. if (typeof newChildren === "string") {
  83. // 新节点是字符串,直接删除旧节点,并使新接的文本
  84. if (typeof oldChildren === "string") {
  85. if (newChildren !== oldChildren) {
  86. el.textContent = newChildren;
  87. }
  88. } else {
  89. //旧节点不是字符串,说明包含多个子节点。同样也直接删除
  90. el.textContent = newChildren;
  91. }
  92. } else {
  93. if (typeof oldChildren === "string") {
  94. //旧节点是字符串,新节点是多个子元素
  95. el.innerHTML = "";
  96. newChildren.forEach((child) => {
  97. mount(child, el);
  98. });
  99. } else {
  100. //旧节点多个子元素,新节点多个子元素
  101. // 找出新旧节点最长的共用长度
  102. const commonLength=Math.min(oldChildren.length, newChildren.length);
  103. // 比对公共长度的节点
  104. for (let i = 0; i < commonLength; i++) {
  105. patch(oldChildren[i], newChildren[i]);
  106. }
  107. // 如果新节点长度大于旧节点长度
  108. if (newChildren.length > oldChildren.length) {
  109. newChildren.slice(oldChildren.length).forEach((child) => {
  110. mount(child, el);
  111. });
  112. }
  113. // 如果旧节点长度大于新节点长度
  114. if (newChildren.length < oldChildren.length) {
  115. oldChildren.slice(newChildren).forEach((child) => {
  116. el.removeChild(child.el);
  117. });
  118. }
  119. }
  120. }
  121. } else {
  122. // 直接替换replace
  123. n1.el.parentNode.replaceChild(createElm(vdom2), n1.el);
  124. }
  125. }
  126. // reactivity
  127. let activeEffect = null;
  128. class Dep {
  129. subscribers = new Set();
  130. depend() {
  131. if (activeEffect) {
  132. this.subscribers.add(activeEffect);
  133. }
  134. }
  135. notify() {
  136. this.subscribers.forEach((effect) => effect());
  137. }
  138. }
  139. function watchEffect(effect) {
  140. activeEffect = effect;
  141. effect();
  142. activeEffect = null;
  143. }
  144. function searchDep(target, key) {
  145. let depsMap = targetMap.get(target);
  146. if (!depsMap) {
  147. targetMap.set(target, (depsMap = new Map()));
  148. }
  149. let dep = depsMap.get(key);
  150. if (!dep) {
  151. depsMap.set(key, (dep = new Dep()));
  152. }
  153. return dep;
  154. }
  155. let targetMap = new WeakMap();
  156. function reactive(raw) {
  157. return new Proxy(raw, {
  158. get(target, key, receiver) {
  159. let dep = searchDep(target, key);
  160. dep.depend();
  161. return Reflect.get(target, key, receiver);
  162. },
  163. set(target, key, value, receiver) {
  164. let dep = searchDep(target, key);
  165. let res = Reflect.set(target, key, value, receiver);
  166. dep.notify();
  167. return res;
  168. },
  169. });
  170. }
  171. // App 组件
  172. const App = {
  173. data: reactive({
  174. count: 0,
  175. }),
  176. render() {
  177. return h(
  178. "div",
  179. {
  180. // 这里需要在 mount 中添加事件处理
  181. onClick: () => {
  182. this.data.count++;
  183. },
  184. },
  185. String(this.data.count)
  186. ); // 第三个参数这里暂时只支持 String类型
  187. },
  188. };
  189. // 挂载 App
  190. function createApp(component, container) {
  191. let isMounted = false;
  192. let prevVdom;
  193. // component 组件中有响应式对象发生变化,便会执行以下函数
  194. watchEffect(() => {
  195. if (!isMounted) {
  196. // 没有挂载,即初始化
  197. // 记录旧的 vdom
  198. prevVdom = component.render();
  199. // 挂载
  200. mount(prevVdom, container);
  201. isMounted = true;
  202. } else {
  203. // 获取新的 vdom
  204. const newVdom = component.render();
  205. // patch
  206. patch(prevVdom, newVdom);
  207. prevVdom = newVdom;
  208. }
  209. });
  210. }
  211. createApp(App, document.getElementById("app"));
  212. </script>

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

代码组织和重用 CompositionAPI

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

  1. function useFeature(){
  2. onMounted(()=> console.log("mounted"))
  3. }
  4. export default{
  5. tempalte: `{{event.count}}`,
  6. props: ["id"],
  7. setup(props){
  8. // 方便复用
  9. useFeature();
  10. return {
  11. event: {count: ref(0)}
  12. }
  13. }
  14. }

对比optionAPI和Composite API建立鼠标移动

  1. <script src="http://unpkg.com/vue"></script>
  2. <div id="app"></div>
  3. <script>
  4. const { createApp } = Vue;
  5. const App = {
  6. template: `{{x}} {{y}}`,
  7. data() {
  8. return {
  9. x: 0,
  10. y: 0,
  11. };
  12. },
  13. methods: {
  14. update(e) {
  15. this.x = e.clientX;
  16. this.y = e.clientY;
  17. },
  18. },
  19. mounted() {
  20. window.addEventListener("mousemove", this.update);
  21. },
  22. unmounted() {
  23. window.addEventListener("mousemove", this.update);
  24. },
  25. };
  26. createApp(App).mount("#app");
  27. </script>

optionAPI逻辑复用mixin

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

  1. <script src="http://unpkg.com/vue"></script>
  2. <div id="app"></div>
  3. <script>
  4. const { createApp } = Vue;
  5. const mouseMoveMixin = {
  6. data() {
  7. return {
  8. x: 0,
  9. y: 0,
  10. };
  11. },
  12. methods: {
  13. update(e) {
  14. this.x = e.clientX;
  15. this.y = e.clientY;
  16. },
  17. },
  18. mounted() {
  19. window.addEventListener("mousemove", this.update);
  20. },
  21. unmounted() {
  22. window.addEventListener("mousemove", this.update);
  23. },
  24. };
  25. const otherMixin = {
  26. data() {
  27. return {
  28. x: 0,
  29. y: 0,
  30. };
  31. },
  32. methods: {
  33. update(e) {
  34. this.x = e.clientX + 'px';
  35. this.y = e.clientY + 'px';
  36. },
  37. },
  38. mounted() {
  39. window.addEventListener("mousemove", this.update);
  40. },
  41. unmounted() {
  42. window.addEventListener("mousemove", this.update);
  43. },
  44. };
  45. const App = {
  46. // 多个mixin同时存在,如果变量名重复出现,后边的会覆盖前面。
  47. template: `{{x}} {{y}}`,
  48. mixins: [mouseMoveMixin, otherMixin],
  49. };
  50. createApp(App).mount("#app");
  51. </script>

使用slot解决复用变量重名问题

  1. <script src="http://unpkg.com/vue"></script>
  2. <div id="app"></div>
  3. <script>
  4. const { createApp } = Vue;
  5. const Mouse = {
  6. data() {
  7. return {
  8. x: 0,
  9. y: 0,
  10. };
  11. },
  12. methods: {
  13. update(e) {
  14. this.x = e.clientX;
  15. this.y = e.clientY;
  16. },
  17. },
  18. mounted() {
  19. window.addEventListener("mousemove", this.update);
  20. },
  21. unmounted() {
  22. window.addEventListener("mousemove", this.update);
  23. },
  24. render() {
  25. return (
  26. this.$slots.default &&
  27. this.$slots.default({
  28. x: this.x,
  29. y: this.y,
  30. })
  31. );
  32. },
  33. };
  34. const Other = {
  35. data() {
  36. return {
  37. x: 0,
  38. y: 0,
  39. };
  40. },
  41. methods: {
  42. update(e) {
  43. this.x = e.clientX + "px";
  44. this.y = e.clientY + "px";
  45. },
  46. },
  47. mounted() {
  48. window.addEventListener("mousemove", this.update);
  49. },
  50. unmounted() {
  51. window.addEventListener("mousemove", this.update);
  52. },
  53. render() {
  54. return (
  55. this.$slots.default &&
  56. this.$slots.default({
  57. x: this.x,
  58. y: this.y,
  59. })
  60. );
  61. },
  62. };
  63. const App = {
  64. components: { Mouse, Other },
  65. template: `<Mouse v-slot='{x, y}'>
  66. <Other v-slot='{x: otherX, y: otherY}'>
  67. {{x}} {{y}} --- {{otherX}} {{otherY}}
  68. </Other>
  69. </Mouse>`,
  70. };
  71. createApp(App).mount("#app");
  72. </script>

Composite API逻辑复用

  1. <script src="http://unpkg.com/vue"></script>
  2. <div id="app"></div>
  3. <script>
  4. const { createApp, ref, onUnmounted, onMounted } = Vue;
  5. const useMouse = () => {
  6. const x = ref(0);
  7. const y = ref(0);
  8. const update = (e) => {
  9. x.value = e.clientX;
  10. y.value = e.clientY;
  11. };
  12. onMounted(() => {
  13. window.addEventListener("mousemove", update);
  14. });
  15. onUnmounted(() => {
  16. window.removeEventListener("mousemove", update);
  17. });
  18. return { x, y };
  19. };
  20. const App = {
  21. template: `{{x}} {{y}}`,
  22. setup() {
  23. // 可以在此处修改引入的变量名
  24. let { x, y } = useMouse();
  25. return { x, y };
  26. },
  27. };
  28. createApp(App).mount("#app");
  29. </script>

Composition API Example

  1. <script src="https://unpkg.com/vue"></script>
  2. <div id="app"></div>
  3. <script>
  4. const { createApp, ref, watchEffect } = Vue;
  5. // 进一步简化在组件中的 use
  6. function usePost(getId) {
  7. return useFetch(
  8. () => `https://jsonplaceholder.typicode.com/todos/${getId()}`
  9. );
  10. }
  11. // 抽出 fetch,并且你可以在的 useFetch 中使用 watchEffect 来监听传进来的值的变化
  12. function useFetch(getUrl) {
  13. const data = ref(null);
  14. const error = ref(null);
  15. const isPending = ref(true);
  16. watchEffect(() => {
  17. // reset
  18. data.value = null;
  19. error.value = null;
  20. isPending.value = true;
  21. // fetch
  22. fetch(getUrl())
  23. .then((res) => res.json())
  24. .then((_data) => {
  25. data.value = _data;
  26. })
  27. .catch((err) => {
  28. error.value = err;
  29. })
  30. .finally(() => {
  31. isPending.value = false;
  32. });
  33. });
  34. return {
  35. data,
  36. error,
  37. isPending,
  38. };
  39. }
  40. const Post = {
  41. template: `
  42. <div v-if="isPending">loading</div>
  43. <div v-else-if="data">{{ data }}</div>
  44. <div v-else-if="error">Something went Wrong: {{ error.message }}</div>
  45. `,
  46. props: ["id"],
  47. setup(props) {
  48. // prop.id 被传到了 useFetch 的 watchEffect 中
  49. // 所以 prop.id 变化,即可重新 fetch
  50. const { data, error, isPending } = usePost(() => props.id);
  51. return {
  52. data,
  53. error,
  54. isPending,
  55. };
  56. },
  57. };
  58. const App = {
  59. components: { Post },
  60. data() {
  61. return {
  62. id: 1,
  63. };
  64. },
  65. template: `
  66. <button @click="id++">change ID</button>
  67. <Post :id="id"></Post>
  68. `,
  69. };
  70. createApp(App).mount("#app");
  71. </script>

代码地址
视频参考