1.什么是Vue.js #

Vue.js是一款渐进式 JavaScript 框架,用于构建用户界面。与其它大型框架不同,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。

这个框架的主要目标是通过尽量简单的 API 提供高效的数据绑定和灵活的组件系统。Vue.js 适用于从单页面应用(SPA)到复杂的前端应用的开发。

Vue 的一些主要特点包括:

  1. 声明式渲染:使得 DOM 的构建直接映射到底层数据。
  2. 组件系统:用于构建可复用的 UI 组件。
  3. 客户端路由(可选):与 vue-router 集成。
  4. 状态管理(可选):与 Vuex 集成。
  5. 虚拟 DOM:提供了一个在内存中计算和渲染元素改动的机制,这通常比直接操作真实 DOM 更快。
  6. 指令:用于对 DOM 元素进行底层操作。
  7. 模板或 JSX:你可以使用模板语法或 JSX,还可以直接写渲染函数。
  8. 响应式数据绑定:允许自动更新视图,当模型改变时。

由于 Vue.js 有一个相对简单和灵活的 API,以及丰富的文档和社区支持,因此它很受欢迎,并且得到了大量的开发者和公司的采用。

2.如何使用Vue.js #

使用 Vue.js 2 开发应用程序涉及几个基础步骤,下面是一个简单的指南。

  1. 通过 CDN 引入
    在 HTML 文件中添加以下脚本标签:

     <script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
    
  2. 使用 npm 安装
    在终端中运行以下命令:

     npm install vue@2
    

3.MVVM #

MVVM(Model-View-ViewModel)是一种用于构建用户界面的设计模式,特别适用于复杂的前端应用。下面我们将通过一个简单的 JavaScript 示例来解释 MVVM 的基本概念。

HTML(View)

首先,我们来定义视图(View)。视图是用户与其交互的界面元素。

<!DOCTYPE html>
<html>
<head>
  <title>MVVM Demo</title>
</head>
<body>
  <button id="increment-btn">点击增加</button>
  <p id="counter-text">计数:0</p>
  <script src="app.js"></script>
</body>
</html>

JavaScript(Model 和 ViewModel)

然后,我们定义数据模型(Model)和视图模型(ViewModel)。

// Model(模型)
const model = {
  count: 0
};

// ViewModel(视图模型)
const viewModel = {
  incrementCount: function() {
    model.count += 1;
    this.updateView();
  },
  updateView: function() {
    document.getElementById('counter-text').innerText = `计数:${model.count}`;
  }
};

// 初始化
viewModel.updateView();

// DOM 监听(连接 View 和 ViewModel)
document.getElementById('increment-btn').addEventListener('click', function() {
  viewModel.incrementCount();
});

原理解析

这样,我们就实现了 Model, View 和 ViewModel 的解耦,每个部分有其自己的职责,而 ViewModel 起到了桥梁的作用。这就是 MVVM 设计模式的基本原理。

4.声明式渲染 #

在 Vue.js 中,声明式渲染是一种表达界面应该如何展示的高级方式,而不是描述如何进行 DOM 操作来达到目的。换句话说,你只需要声明你想要什么,而 Vue.js 会负责如何去做。

4.1 vue.html #

vue.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>
    <div id="app"></div>
    <script src="vue.js"></script>
    <script>
        new Vue({
            el: '#app',
            data: {
                message: 'hello'
            },
            template:'<span>{{message}}</span>'
        });
    </script>
</body>
</html>

4.2 my-vue.js #

my-vue.js

class Vue {
    constructor(options) {
        this.$options = options;
        this.$el = document.querySelector(options.el);
        this.$data = options.data;
        this.init();
    }
    init() {
        this.proxyData();
        this.compile();
    }
    proxyData() {
        for (let key in this.$data) {
            Object.defineProperty(this, key, {
                get() {
                    return this.$data[key];
                },
                set(newVal) {
                    this.$data[key] = newVal;
                }
            });
        }
    }
    compile() {
        const { template } = this.$options;
        const html = template.replace(/\{\{(.+?)\}\}/g, (_, key) => {
            return this[key];
        });
        const tempContainer = document.createElement('div');
        tempContainer.innerHTML = html;
        const newEl = tempContainer.firstChild;
        document.body.replaceChild(newEl, this.$el);
    }
}

5.响应式数据 #

在 Vue.js 中,响应式数据是其中最核心的概念之一。简单来说,响应式意味着当数据变化时,与该数据相关的所有事物都会自动更新。

5.1 vue.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>
    <div id="app"></div>
    <script src="vue.js"></script>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                message: 'hello'
            },
            template:'<span>{{message}}</span>'
        });
+       vm.message = 'world';
    </script>
</body>
</html>

5.2 my-vue.js #

my-vue.js

class Vue {
    constructor(options) {
        this.$options = options;
        this.$el = document.querySelector(options.el);
        this.$data = options.data;
        this.init();
    }
    init() {
        this.proxyData();
+       this.observe(this.$data);
        this.compile();
    }
+   observe(obj) {
+       for (let key in obj) {
+           let value = obj[key];
+           Object.defineProperty(obj, key, {
+               get() {
+                   return value;
+               },
+               set: (newValue) => {
+                   if (newValue !== value) {
+                       value = newValue;
+                       this.compile();
+                   }
+               }
+           });
+       }
+   }
    proxyData() {
        for (let key in this.$data) {
            Object.defineProperty(this, key, {
                get() {
                    return this.$data[key];
                },
                set(newVal) {
                    this.$data[key] = newVal;
                }
            });
        }
    }
    compile() {
        const { template } = this.$options;
        const html = template.replace(/\{\{(.+?)\}\}/g, (_, key) => {
            return this[key];
        });
        const tempContainer = document.createElement('div');
        tempContainer.innerHTML = html;
        const newEl = tempContainer.firstChild;
        document.body.replaceChild(newEl, this.$el);
        this.$el=newEl;
    }
}

6.v-text #

v-text 是 Vue.js 中用于更新 DOM 元素文本内容的一个指令。这个指令会把指定元素(通常是一个文本节点)的 textContent 属性设置为跟表达式的值一致。

下面是一个简单的例子:

<!-- Vue 组件或者实例的模板 -->
<div v-text="message"></div>

<!-- JavaScript 部分 -->
<script>
  new Vue({
    el: '#app',
    data: {
      message: 'Hello, world!'
    }
  });
</script>

在这个例子中,<div> 标签的文本内容会被设置为 data 对象里 message 的值,即 "Hello, world!"。

这与直接使用插值表达式(即双大括号)基本是等价的:

<div>{{ message }}</div>

然而,v-text 会直接改写元素的 textContent,因此这个元素内部的其他文本或标记会被覆盖。另外,v-text 更新 DOM 的方式可能比插值表达式更高效,尤其是当你需要更新大量文本节点时。

请注意,v-text 指令本质上不会解析 HTML 标签,只会以纯文本的形式插入。如果你需要解析 HTML,你可以使用 v-html 指令,但要注意这可能会导致跨站脚本攻击(XSS)。

总结:

6.1 v-text.html #

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue</title>
</head>
<body>
    <div id="app">
    </div>
    <script src="my-vue.js"></script>
    <script>
        var vm = new Vue({
            el:'#app',
            data:{msg:'hello',content:'world'},
+           template:'<p><span v-text="msg"></span><span v-text="content"></span></p>'
        });
    </script>
</body>
</html>

6.2 my-vue.js #

class Vue {
    constructor(options) {
        this.$options = options;
        this.$el = document.querySelector(options.el);
        this.$data = options.data;
        this.init();
    }
    init() {
        this.proxyData();
        this.observe(this.$data);
        this.compile();
    }
    observe(obj) {
        for (let key in obj) {
            let value = obj[key];
            Object.defineProperty(obj, key, {
                get() {
                    return value;
                },
                set: (newValue) => {
                    if (newValue !== value) {
                        value = newValue;
                        this.compile();
                    }
                }
            });
        }
    }
    proxyData() {
        for (let key in this.$data) {
            Object.defineProperty(this, key, {
                get() {
                    return this.$data[key];
                },
                set(newVal) {
                    this.$data[key] = newVal;
                }
            });
        }
    }
    compile() {
        const { template } = this.$options;
        const html = template.replace(/\{\{(.+?)\}\}/g, (_, key) => {
            return this[key];
        });
        const tempContainer = document.createElement('div');
        tempContainer.innerHTML = html;
+       const hasVTexts = tempContainer.querySelectorAll('[v-text]');
+       if (hasVTexts && hasVTexts.length > 0) {
+           for (const hasVText of hasVTexts) {
+               let textBinding = hasVText.getAttribute('v-text');
+               hasVText.textContent = this[textBinding];
+           }
+       }
        const newEl = tempContainer.firstChild;
        document.body.replaceChild(newEl, this.$el);
        this.$el = newEl;
    }
}

7.v-html #

v-html 是 Vue.js 框架中的一个指令,用于将 HTML 字符串插入到 DOM(文档对象模型)中。该指令用于绑定一段 HTML 代码到一个 DOM 元素,使该段代码作为元素的 innerHTML 渲染出来。

下面是一个简单的例子:

<template>
  <div>
    <div v-html="htmlString"></div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      htmlString: "<h1>Hello World</h1>"
    };
  }
};
</script>

在这个例子中,<h1>Hello World</h1> 会被插入到 div 元素内,并显示为一个 H1 大小的 "Hello World" 文字。

注意事项:

  1. 安全性问题:由于 v-html 直接将 HTML 字符串插入到 DOM,因此存在 XSS(跨站脚本攻击)的风险。你应当只对可信的内容使用 v-html,并且尽量避免插入用户生成的内容。

  2. 替代方案:如果你只是想插入文本而不是 HTML,可以使用 {{}} 插值表达式。

  3. 不保留状态:使用 v-html 插入的 HTML 不会保留任何 Vue 组件状态。这意味着如果你通过 v-html 插入了包含 Vue 组件的 HTML,这些组件将不会被实例化。

  4. 限制v-html 只影响元素的 innerHTML,不能用来绑定 Vue 模板表达式到整个模板。

  5. 不建议频繁使用:频繁使用 v-html 可能会使得应用更难以维护和理解,因为它分散了应用逻辑。

使用 v-html 指令时,务必小心,并确保你了解其潜在的风险和限制。

7.1 v-html.html #

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue</title>
</head>
<body>
    <div id="app">
    </div>
    <script src="my-vue.js"></script>
    <script>
        var vm = new Vue({
            el:'#app',
            data:{msg:'<strong>hello</strong>'},
            template:'<span v-html="msg"></span>'
        });
    </script>
</body>
</html>

7.2 my-vue.js #

class Vue {
    constructor(options) {
        this.$options = options;
        this.$el = document.querySelector(options.el);
        this.$data = options.data;
        this.init();
    }
    init() {
        this.proxyData();
        this.observe(this.$data);
        this.compile();
    }
    observe(obj) {
        for (let key in obj) {
            let value = obj[key];
            Object.defineProperty(obj, key, {
                get() {
                    return value;
                },
                set: (newValue) => {
                    if (newValue !== value) {
                        value = newValue;
                        this.compile();
                    }
                }
            });
        }
    }
    proxyData() {
        for (let key in this.$data) {
            Object.defineProperty(this, key, {
                get() {
                    return this.$data[key];
                },
                set(newVal) {
                    this.$data[key] = newVal;
                }
            });
        }
    }
    compile() {
        const { template } = this.$options;
        const html = template.replace(/\{\{(.+?)\}\}/g, (_, key) => {
            return this[key];
        });
        const tempContainer = document.createElement('div');
        tempContainer.innerHTML = html;
        const hasVTexts = tempContainer.querySelectorAll('[v-text]');
        if (hasVTexts && hasVTexts.length > 0) {
            for (const hasVText of hasVTexts) {
                let textBinding = hasVText.getAttribute('v-text');
                hasVText.textContent = this[textBinding];
            }
        }
+       const hasVHTMLs = tempContainer.querySelectorAll('[v-html]');
+       if (hasVHTMLs && hasVHTMLs.length > 0) {
+           for (const hasVHTML of hasVHTMLs) {
+               let htmlBinding = hasVHTML.getAttribute('v-html');
+               hasVHTML.innerHTML = this[htmlBinding];
+           }
+       }
        const newEl = tempContainer.firstChild;
        document.body.replaceChild(newEl, this.$el);
        this.$el = newEl;
    }
}

8.v-cloak #

v-cloak 是一个用于 Vue.js 的特殊指令,其主要目的是在 Vue 实例编译结束、数据绑定完成后才显示该元素。在这之前,元素和其子元素将保持隐藏。这个指令用于防止未编译的 "Mustache 标签"(双大括号语法,如 {{ message }})在页面加载时短暂出现。

一般来说,你可能会将 v-cloak 和 CSS 规则结合使用,以在 Vue 完成初始化之前隐藏相关元素:

/* CSS */
[v-cloak] {
  display: none;
}

然后,在你的 HTML 中:

<div v-cloak>
  {{ message }}
</div>

在这个例子中,只要存在 v-cloak 指令,Vue 还未完成编译,该 div 就会被隐藏。当 Vue 完成初始化和编译后,v-cloak 指令会被自动移除,并且相关的 CSS 规则也不再适用,因此元素将变为可见。

这种机制尤其有用在大型应用或网络状况不佳的情况下,用户可能会看到 "闪烁" 的 {{ message }} 标签,直到 Vue 完成初始化。通过使用 v-cloak,你可以提供更流畅、更专业的用户体验。

8.1 v-cloak.html #

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue</title>
    <style>
        [v-cloak]{
            display: none;
        }
    </style>
</head>
<body>
+   <div id="app" v-cloak>
+       <span>{{msg}}</span>
    </div>
    <script src="my-vue.js"></script>
    <script>
        var vm = new Vue({
            el:'#app',
            data:{msg:'hello'}
        });
    </script>
</body>
</html>

8.2 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Vue {
    constructor(options) {
        this.$options = options;
        this.$el = document.querySelector(options.el);
        this.$data = options.data;
        this.init();
    }
    init() {
        this.proxyData();
        this.observe(this.$data);
+       this.compile(this.$el);
    }
    observe(obj) {
        for (let key in obj) {
            let value = obj[key];
            Object.defineProperty(obj, key, {
                get() {
                    return value;
                },
                set: (newValue) => {
                    if (newValue !== value) {
                        value = newValue;
+                       this.compile(this.$el);
                    }
                }
            });
        }
    }
    proxyData() {
        for (let key in this.$data) {
            Object.defineProperty(this, key, {
                get() {
                    return this.$data[key];
                },
                set(newVal) {
                    this.$data[key] = newVal;
                }
            });
        }
    }
    compile(el) {
+       const { childNodes } = el;
+       Array.from(childNodes).forEach(node => {
+           if (node.nodeType === ELEMENT_NODE) {
+               const attrs = node.attributes;
+               Array.from(attrs).forEach(attr => {
+                   if (attr.name === 'v-text') {
+                       const expr = attr.value;
+                       node.textContent = this[expr];
+                   } else if (attr.name == 'v-html') {
+                       const expr = attr.value;
+                       node.innerHTML = this[expr];
+                   }
+               });
+               this.compile(node);
+           } else if (node.nodeType === TEXT_NODE) {
+               const { textContent } = node;
+               node.textContent = textContent.replace(/\{\{(.+?)\}\}/g, (_, key) => {
+                   return this[key];
+               });
+           }
+       });
+       el.removeAttribute('v-cloak');
    }
}

9.v-pre #

v-pre 是一个 Vue.js 的指令,用于跳过当前元素和所有子元素的编译过程。这样可以用于显示原始的 Mustache 标签。当 Vue.js 渲染元素和组件时,会解析其中的 Vue 指令和插值表达式(例如,{{ message }})。有时,你可能希望 Vue 忽略某个元素及其子元素,不对其进行编译。这是 v-pre 的用途。

例如,考虑以下 HTML:

<div v-pre>
  <p>{{ this will not be compiled }}</p>
</div>

由于 v-pre 指令的存在,<div> 元素和其所有子元素都不会被 Vue 编译。因此,页面上会原样显示文本 {{ this will not be compiled }}

v-pre 主要用于两种场景:

  1. 性能优化:如果你有确定不需要编译的大块内容,使用 v-pre 可以减少编译时间。
  2. 显示原始 Mustache 标签:当你需要在应用中原样显示某些代码或标签时,可以使用 v-pre

需要注意的是,v-pre 会跳过元素及其所有子元素的编译,包括其中的 Vue 指令。所以,请谨慎使用这个指令。

9.1 v-pre.html #

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue</title>
</head>
<body>
    <div id="app" >
        <span>{{msg}} {{age}}</span>
        <span v-pre>{{msg}}</span>
        <p v-pre>
            <span>{{age}}</span>
        </p>
    </div>
    <script src="my-vue.js"></script>
    <script>
        var vm = new Vue({
            el:'#app',
            data:{msg:'hello',age:18}
        });
    </script>
</body>
</html>

9.2 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Vue {
    constructor(options) {
        this.$options = options;
        this.$el = document.querySelector(options.el);
        this.$data = options.data;
        this.init();
    }
    init() {
        this.proxyData();
        this.observe(this.$data);
        this.compile(this.$el);
    }
    observe(obj) {
        for (let key in obj) {
            let value = obj[key];
            Object.defineProperty(obj, key, {
                get() {
                    return value;
                },
                set: (newValue) => {
                    if (newValue !== value) {
                        value = newValue;
                        this.compile(this.$el);
                    }
                }
            });
        }
    }
    proxyData() {
        for (let key in this.$data) {
            Object.defineProperty(this, key, {
                get() {
                    return this.$data[key];
                },
                set(newVal) {
                    this.$data[key] = newVal;
                }
            });
        }
    }
    compile(el) {
        const { childNodes } = el;
        Array.from(childNodes).forEach(node => {
            if (node.nodeType === ELEMENT_NODE) {
+               if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
+                   return;
+               }
                const attrs = node.attributes;
                Array.from(attrs).forEach(attr => {
                    if (attr.name === 'v-text') {
                        const expr = attr.value;
                        node.textContent = this[expr];
                    } else if (attr.name == 'v-html') {
                        const expr = attr.value;
                        node.innerHTML = this[expr];
                    }
                });
                this.compile(node);
            } else if (node.nodeType === TEXT_NODE) {
                const { textContent } = node;
                node.textContent = textContent.replace(/\{\{(.+?)\}\}/g, (_, key) => {
                    return this[key];
                });
            }
        });
        el.removeAttribute('v-cloak');
    }
}

10.v-bind #

v-bind 是 Vue.js 中用于绑定属性或表达式到元素的一个指令。当你需要动态设置 HTML 元素的某个属性值时,你可以使用 v-bind。它使得数据与视图可以保持同步。

基本语法:

<!-- 绑定一个属性 -->
<a v-bind:href="url">Link</a>

<!-- 使用表达式 -->
<div v-bind:style="{ color: active ? 'red' : 'gray' }"></div>

在这些例子中,urlactive 是 Vue 实例中的数据或计算属性。

常见用法:

  1. 绑定图片链接

     <img v-bind:src="imageSrc">
    

    这里,imageSrc 是一个变量,它的值会被设置为 img 元素的 src 属性。

  2. 动态类名

     <div v-bind:class="{ active: isActive }"></div>
    

    isActivetrue 时,这个 div 的类名会包含 active

  3. 动态样式

     <div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
    

    这里,activeColorfontSize 是变量,它们的值会分别被设置为 divcolorfontSize 样式。

  4. 绑定动态属性

     <button v-bind:[key]="value"></button>
    

    在这个例子中,keyvalue 是变量。这允许你动态地设置任何属性。

  5. 简写

    v-bind 有一个简写,可以直接用 : 表示:

     <!-- 完整语法 -->
     <a v-bind:href="url">Link</a>
    
     <!-- 简写 -->
     <a :href="url">Link</a>
    

使用 v-bind 指令,你可以创建更加动态和响应式的 web 应用。

10.1 v-bind.html #

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue</title>
</head>

<body>
    <div id="app">
        <div v-bind:title="title">title内容</div>
    </div>
    <script src="my-vue.js"></script>
    <script>
        new Vue({
            el: '#app',
            data: {
                title: 'title值'
            }
        })
    </script>
</body>

</html>

10.2 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Vue {
    constructor(options) {
        this.$options = options;
        this.$el = document.querySelector(options.el);
        this.$data = options.data;
        this.init();
    }
    init() {
        this.proxyData();
        this.observe(this.$data);
        this.compile(this.$el);
    }
    observe(obj) {
        for (let key in obj) {
            let value = obj[key];
            Object.defineProperty(obj, key, {
                get() {
                    return value;
                },
                set: (newValue) => {
                    if (newValue !== value) {
                        value = newValue;
                        this.compile(this.$el);
                    }
                }
            });
        }
    }
    proxyData() {
        for (let key in this.$data) {
            Object.defineProperty(this, key, {
                get() {
                    return this.$data[key];
                },
                set(newVal) {
                    this.$data[key] = newVal;
                }
            });
        }
    }
    compile(el) {
        const { childNodes } = el;
        Array.from(childNodes).forEach(node => {
            if (node.nodeType === ELEMENT_NODE) {
                if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
                    return;
                }
                const attrs = node.attributes;
                Array.from(attrs).forEach(attr => {
                    if (attr.name === 'v-text') {
                        const expr = attr.value;
                        node.textContent = this[expr];
                    } else if (attr.name == 'v-html') {
                        const expr = attr.value;
                        node.innerHTML = this[expr];
+                   }else if (attr.name.startsWith('v-bind:')) {
+                       const attrName = attr.name.slice(7);
+                       const key = attr.value;
+                       node.setAttribute(attrName, this[key]);
+                   }
                });
                this.compile(node);
            } else if (node.nodeType === TEXT_NODE) {
                const { textContent } = node;
                node.textContent = textContent.replace(/\{\{(.+?)\}\}/g, (_, key) => {
                    return this[key];
                });
            }
        });
        el.removeAttribute('v-cloak');
    }
}

11.v-on #

v-on 是 Vue.js 中用于监听 DOM 事件并在触发时执行一些 JavaScript 逻辑的一个指令。通过这个指令,你可以将一个方法或者一个内联语句与 DOM 事件绑定。

基本用法

这里有一些例子,以便你更好地理解这个指令:

<!-- 绑定一个方法到 click 事件 -->
<button v-on:click="doSomething">Click me</button>

<!-- 你也可以直接绑定一个内联语句 -->
<button v-on:click="count += 1">Increment</button>

<!-- 使用事件对象 -->
<button v-on:click="showEvent($event)">Show Event</button>
new Vue({
  el: '#app',
  data: {
    count: 0
  },
  methods: {
    doSomething() {
      alert('Button was clicked!');
    },
    showEvent(event) {
      console.log(event);
    }
  }
});

缩写

在 Vue 中,v-on: 可以简写为 @,让模板看起来更简洁:

<!-- 完整语法 -->
<button v-on:click="doSomething">Click me</button>

<!-- 缩写 -->
<button @click="doSomething">Click me</button>

传递参数

v-on 指令也支持传递参数:

<button @click="say('hi')">Say Hi</button>
<button @click="say('hello')">Say Hello</button>
new Vue({
  el: '#app',
  methods: {
    say(message) {
      alert(message);
    }
  }
});

动态事件名

你也可以绑定动态的事件名:

<button v-on:[eventName]="doSomething">Click me</button>
new Vue({
  el: '#app',
  data: {
    eventName: 'click'
  },
  methods: {
    doSomething() {
      alert('Button was clicked!');
    }
  }
});

11.1 v-on #

在 Vue.js 2 中,v-on 指令用于监听 DOM 事件,并在触发时执行某些 JavaScript 代码。这是一种声明式的方式来处理浏览器事件,如点击 (click)、输入 (input)、提交 (submit) 等。

11.2 methods #

methods 是 Vue 实例的一个选项,用于定义与该实例关联的方法。这些方法通常用于模板中的事件处理,或者在 Vue 实例中用于计算或操作数据。

11.3 v-on.html #

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue</title>
</head>

<body>
    <div id="app">
        <button v-on:click="sayHello">Click me</button>
    </div>
    <script src="my-vue.js"></script>
    <script>
        new Vue({
            el: '#app',
            data: {
                msg: 'hello'
            },
            methods: {
                sayHello: function () {
                    console.log(this.msg);
                }
            }
        })
    </script>
</body>

</html>

11.4 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Vue {
    constructor(options) {
        this.$options = options;
        this.$el = document.querySelector(options.el);
        this.$data = options.data;
+       this.$methods = options.methods;
        this.init();
    }
    init() {
        this.proxyData();
+       this.proxyMethods();
        this.observe(this.$data);
        this.compile(this.$el);
    }
+   proxyMethods() {
+       for (let key in this.$methods) {
+           this[key] = this.$methods[key].bind(this);
+       }
+   }
    observe(obj) {
        for (let key in obj) {
            let value = obj[key];
            Object.defineProperty(obj, key, {
                get() {
                    return value;
                },
                set: (newValue) => {
                    if (newValue !== value) {
                        value = newValue;
                        this.compile(this.$el);
                    }
                }
            });
        }
    }
    proxyData() {
        for (let key in this.$data) {
            Object.defineProperty(this, key, {
                get() {
                    return this.$data[key];
                },
                set(newVal) {
                    this.$data[key] = newVal;
                }
            });
        }
    }
    compile(el) {
        const { childNodes } = el;
        Array.from(childNodes).forEach(node => {
            if (node.nodeType === ELEMENT_NODE) {
                if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
                    return;
                }
                const attrs = node.attributes;
                Array.from(attrs).forEach(attr => {
                    if (attr.name === 'v-text') {
                        const expr = attr.value;
                        node.textContent = this[expr];
                    } else if (attr.name == 'v-html') {
                        const expr = attr.value;
                        node.innerHTML = this[expr];
                    } else if (attr.name.startsWith('v-bind:')) {
                        const attrName = attr.name.slice(7);
                        const key = attr.value;
                        node.setAttribute(attrName, this[key]);
+                   } else if (attr.name.startsWith('v-on:')) {
+                       const eventName = attr.name.slice(5);
+                       const methodName = attr.value;
+                       node.addEventListener(eventName, this[methodName]);
+                   }
                });
                this.compile(node);
            } else if (node.nodeType === TEXT_NODE) {
                const { textContent } = node;
                node.textContent = textContent.replace(/\{\{(.+?)\}\}/g, (_, key) => {
                    return this[key];
                });
            }
        });
        el.removeAttribute('v-cloak');
    }
}

12.$event #

在 Vue.js 中,v-on 指令用于监听 DOM 事件,并在触发这些事件时执行一些 JavaScript 代码。$event 是一个特殊的变量,它用于访问原生的 DOM 事件对象。

例如,考虑一个按钮,点击该按钮时触发一个 click 事件:

<!-- HTML模板 -->
<button v-on:click="handleClick($event)">Click Me</button>
// Vue 实例
new Vue({
  el: '#app',
  methods: {
    handleClick(event) {
      console.log('Button clicked!', event);
    }
  }
});

在这个例子中,当用户点击按钮时,handleClick 方法会被调用,并且接收到原生的 click 事件对象作为参数。这个原生的事件对象存储在 $event 变量中。

$event 对象提供了关于该事件的各种信息,包括:

有时,你可能想要同时传递 $event 和其他参数。在这种情况下,你可以这样做:

<!-- HTML模板 -->
<button v-on:click="handleClick('someValue', $event)">Click Me</button>
// Vue 实例
new Vue({
  el: '#app',
  methods: {
    handleClick(value, event) {
      console.log('Button clicked!', value, event);
    }
  }
});

在这个例子中,handleClick 方法接收两个参数:一个是字符串 'someValue',另一个是原生的 click 事件对象。这样你就可以在方法内部同时访问这两个值。

$event 是非常有用的,特别是当你需要访问事件对象以进行更复杂的操作时,例如阻止事件的默认行为或停止事件的传播。

12.1 v-on.html #

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue</title>
</head>
<body>
    <div id="app">
+       <button v-on:click="sayHello(1,'a',$event,msg)">Click me</button>
    </div>
    <script src="my-vue.js"></script>
    <script>
        new Vue({
            el: '#app',
            data: {
                msg: 'hello'
            },
            methods: {
+               sayHello (number,string,event,variable) {
+                   console.log(number,string,event,variable);
+               }
            }
        })
    </script>
</body>
</html>

12.2 my-vue.js #

my-vue.js

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Vue {
    constructor(options) {
        this.$options = options;
        this.$el = document.querySelector(options.el);
        this.$data = options.data;
        this.$methods = options.methods;
        this.init();
    }
    init() {
        this.proxyData();
        this.proxyMethods();
        this.observe(this.$data);
        this.compile(this.$el);
    }
    proxyMethods() {
        for (let key in this.$methods) {
            this[key] = this.$methods[key].bind(this);
        }
    }
    observe(obj) {
        for (let key in obj) {
            let value = obj[key];
            Object.defineProperty(obj, key, {
                get() {
                    return value;
                },
                set: (newValue) => {
                    if (newValue !== value) {
                        value = newValue;
                        this.compile(this.$el);
                    }
                }
            });
        }
    }
    proxyData() {
        for (let key in this.$data) {
            Object.defineProperty(this, key, {
                get() {
                    return this.$data[key];
                },
                set(newVal) {
                    this.$data[key] = newVal;
                }
            });
        }
    }
    compile(el) {
        const { childNodes } = el;
        Array.from(childNodes).forEach(node => {
            if (node.nodeType === ELEMENT_NODE) {
                if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
                    return;
                }
                const attrs = node.attributes;
                Array.from(attrs).forEach(attr => {
                    if (attr.name === 'v-text') {
                        const expr = attr.value;
                        node.textContent = this[expr];
                    } else if (attr.name == 'v-html') {
                        const expr = attr.value;
                        node.innerHTML = this[expr];
                    } else if (attr.name.startsWith('v-bind:')) {
                        const attrName = attr.name.slice(7);
                        const key = attr.value;
                        node.setAttribute(attrName, this[key]);
                    } else if (attr.name.startsWith('v-on:')) {
+                        const eventName = attr.name.slice(5);
+                        const methodExpression = attr.value.trim();
+                        let methodName = methodExpression;
+                        let args = [];
+                        const parenIndex = methodExpression.indexOf('(');
+                        if (parenIndex > -1) {
+                            methodName = methodExpression.substring(0, parenIndex);
+                            const argsString = methodExpression.substring(
+                                parenIndex + 1, 
+                                methodExpression.length - 1
+                            ).trim();
+                            args = argsString.split(',').map(arg => arg.trim());
+                        }
+                        node.addEventListener(eventName, (event) => {
+                            const actualArgs = args.map(arg => {
+                                if (arg === '$event') return event;
+                                if (!isNaN(arg)) return Number(arg);
+                                if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
+                                return this[arg];
+                            });
+                            this[methodName](...actualArgs);
+                        });
                    }
                });
                this.compile(node);
            } else if (node.nodeType === TEXT_NODE) {
                const { textContent } = node;
                node.textContent = textContent.replace(/\{\{(.+?)\}\}/g, (_, key) => {
                    return this[key];
                });
            }
        });
        el.removeAttribute('v-cloak');
    }
}

13.修饰符 #

13.1 修饰符 #

13.1.1 事件修饰符 #

13.1.1.1 .stop #

.stop 事件修饰符用于阻止事件冒泡。当你在元素上使用 .stop 修饰符时,如果此元素的事件被触发,那么这个事件不会向上冒泡至父元素。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue</title>
</head>
<body>
    <div id="app">
      <div id="parent" v-on:click="parentClicked">
        <button id="child" v-on:click.stop="childClicked">Click me</button>
      </div>
    </div>
   <script src="my-vue.js"></script>
    <script>
      new Vue({
        el: '#app',
        methods: {
          parentClicked() {
            console.log('Parent clicked!');
          },
          childClicked() {
            console.log('Child clicked!');
          }
        }
      });
    </script>
  </body>
</html>
13.1.1.2 .prevent #

.prevent 是一个事件修饰符,用于告诉 Vue 的事件系统,我们希望阻止原生事件的默认行为。这对应于在 JavaScript 中调用 event.preventDefault()

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue</title>
</head>
<body>
  <div id="app" >
    <form v-on:submit.prevent="handleSubmit">
      <label>用户名</label>
      <input/>
      <input type="submit"/>
    </form>
  </div>
  <script src="vue.js"></script>
  <script>
      var vm = new Vue({
          el:'#app',
          data:{msg:'hello'},
          methods:{
              handleSubmit(event){
                  console.log('handleSubmit');
              }
          }
      });
  </script>
</body>
</html>
13.1.1.3 .capture #

.capture是一种事件修饰符,它使事件监听器在事件捕获模式下触发,而非默认的冒泡模式。

在解释.capture修饰符之前,我们首先需要了解事件的冒泡和捕获模式。

事件冒泡和捕获:

.capture修饰符就是使Vue在捕获模式下监听DOM事件。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue</title>
</head>

<body>
  <div id="app">
    <div id="parent" v-on:click="parentClicked" v-on:click.capture="parentCaptureClicked">
      <button id="child" v-on:click="childClicked" v-on:click.capture="childCaptureClicked">Click me</button>
    </div>
  </div>
  <script src="my-vue.js"></script>
  <script>
    new Vue({
      el: '#app',
      methods: {
        parentClicked() {
          console.log('Parent clicked!');
        },
        childClicked() {
          console.log('Child clicked!');
        },
        parentCaptureClicked() {
          console.log('parentCaptureClicked');
        },
        childCaptureClicked() {
          console.log('childCaptureClicked');
        }
      }
    });
  </script>
</body>

</html>
13.1.1.4 .self #

在 Vue2 中,.self 是一个事件修饰符,用于确保只有当事件在该元素本身(而不是子元素)触发时,才会触发事件回调函数。这对应于 JavaScript 中对事件的目标元素(event.target)和当前元素(event.currentTarget)的比较。

这个修饰符常用在你希望事件只在特定元素上触发,而不是在其子元素上触发时。

以下是一个简单的例子:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue</title>
</head>

<body>
  <div id="app">
    <div id="parent" v-on:click.self="parentClicked" v-on:click.capture="parentCaptureClicked">
      Parent
      <button id="child" v-on:click="childClicked" v-on:click.capture="childCaptureClicked">Click me</button>
    </div>
  </div>
  <script src="my-vue.js"></script>
  <script>
    new Vue({
      el: '#app',
      methods: {
        parentClicked() {
          console.log('Parent clicked!');
        },
        childClicked() {
          console.log('Child clicked!');
        },
        parentCaptureClicked() {
          console.log('parentCaptureClicked');
        },
        childCaptureClicked() {
          console.log('childCaptureClicked');
        }
      }
    });
  </script>
</body>

</html>
13.1.1.5 .once #

.once修饰符用于指定事件监听器在触发一次之后就自动解除绑定,即这个监听器只会被触发一次。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue</title>
</head>

<body>
  <div id="app">
    <p>{{counter}}</p>
    <button v-on:click.once="handleClick">Click me</button>
  </div>
  <script src="my-vue2.js"></script>
  <script>
    new Vue({
      el: '#app',
      data: {
        counter: 0
      },
      methods: {
        handleClick () {
          this.counter=this.counter+1;
        }
      }
    });
  </script>
</body>
</html>
13.1.1.6 .passive #

.passive 是一个事件修饰符,用于增强性能,特别是在移动端。这对应于在 JavaScript 中对事件调用 addEventListener 方法时使用 { passive: true } 选项。

当你添加了 .passive 修饰符后,你就告诉浏览器你不打算阻止事件的默认行为。这样,浏览器就可以在事件处理程序运行的同时做一些性能优化。这对于一些高频触发的事件(例如滚动或触摸事件)特别有用。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Vue Scrolling</title>
    <style>
        /* 添加样式以确保 div 具有滚动条 */
        div {
            width: 300px;
            height: 200px;
            overflow: auto;
        }
        /* 添加足够的内容以触发滚动 */
        .content {
            height: 1000px;
        }
    </style>
</head>
<body>
    <div id="app">
        <div v-on:scroll.passive="onScroll" class="scroll-area">
            <div class="content">内容</div>
        </div>
    </div>
    <script src="my-vue.js"></script>
    <script>
        new Vue({
            el: '#app',
            methods: {
                onScroll(event) {
                    console.log('Scrolled!');
                }
            }
        });
    </script>
</body>
</html>

13.1.2 按键修饰符 #

为了处理键盘事件,Vue 提供了一些按键修饰符。这些修饰符允许你在事件触发时更加精确地处理不同的按键。这些修饰符可以在 v-on 或其缩写 @ 后使用。

以下是一些常用的按键修饰符:

  1. .enter:只有在按下 Enter 键时才触发事件处理函数
  2. .tab:只有在按下 Tab 键时才触发事件处理函数
  3. .delete:只有在按下 Delete 或 Backspace 键时才触发事件处理函数
  4. .esc:只有在按下 Esc 键时才触发事件处理函数
  5. .space:只有在按下 Space 键时才触发事件处理函数
  6. .up:只有在按下 Up 键时才触发事件处理函数
  7. .down:只有在按下 Down 键时才触发事件处理函数
  8. .left:只有在按下 Left 键时才触发事件处理函数
  9. .right:只有在按下 Right 键时才触发事件处理函数

以下是一个使用 .enter 按键修饰符的示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Vue</title>
</head>
<body>
    <div id="app">
      <input v-on:keyup.enter="submit" />
    </div>
    <script src="my-vue.js"></script>
    <script>
      new Vue({
        el: '#app',
        methods: {
          submit() {
            console.log('Enter key was pressed');
          }
        }
      })
    </script>
  </body>
</html>

在这个例子中,当在输入框中按下 Enter 键时,submit 方法会被调用。

你也可以使用按键的 ASCII 码作为修饰符,例如 `@keyup.13="submit",其中 13 是 Enter 键的 ASCII 码。但是,使用名字(如.enter`)通常更容易理解。

请注意,Vue2 也支持系统修饰符(如 .ctrl.alt.shift.meta),可以与按键修饰符一起使用,例如 `@keydown.ctrl.enter="submit",这样只有在同时按下 Ctrl 键和 Enter 键时,submit` 方法才会被调用。

13.1.3 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
+class Dep {
+  constructor() {
+    this.subs = [];
+  }
+  addSub(sub) {
+    this.subs.push(sub);
+  }
+  notify() {
+    this.subs.forEach(sub => sub.update());
+  }
+}
+class Watcher {
+  constructor(vm, expr, cb) {
+    this.vm = vm;
+    this.expr = expr;
+    this.cb = cb;
+    Dep.target = this;
+    this.oldValue = this.vm[this.expr];
+    Dep.target = null;
+  }
+  update() {
+    const newValue = this.vm[this.expr];
+    if (this.oldValue !== newValue) {
+      this.cb(newValue);
+      this.oldValue = newValue;
+    }
+  }
+}
class Vue {
  constructor(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
    this.init();
  }
  init() {
    this.proxyData();
    this.proxyMethods();
+   this.observe(this.$data);
    this.compile(this.$el);
  }
  proxyMethods() {
    for (let key in this.$methods) {
      this[key] = this.$methods[key].bind(this);
    }
  }
+  observe(obj) {
+    for (let key in obj) {
+      let value = obj[key];
+      const dep = new Dep();
+      Object.defineProperty(obj, key, {
+        get() {
+          if (Dep.target) {
+            dep.addSub(Dep.target);
+          }
+          return value;
+        },
+        set: (newValue) => {
+          if (newValue !== value) {
+            value = newValue;
+            dep.notify();
+          }
+        }
+      });
+    }
+  }
  proxyData() {
    for (let key in this.$data) {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newVal) {
          this.$data[key] = newVal;
        }
      });
    }
  }
  compile(el) {
    const vm = this;
    const { childNodes } = el;
    Array.from(childNodes).forEach(node => {
      if (node.nodeType === ELEMENT_NODE) {
        if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
          return;
        }
        const attrs = node.attributes;
        Array.from(attrs).forEach(attr => {
          if (attr.name === 'v-text') {
            const expr = attr.value;
+           new Watcher(vm, expr, newValue => {
+             node.textContent = newValue;
+           });
            node.textContent = vm[expr];
          } else if (attr.name === 'v-html') {
            const expr = attr.value;
+           new Watcher(vm, expr, newValue => {
+             node.innerHTML = newValue;
+           });
            node.innerHTML = vm[expr];
          } else if (attr.name.startsWith('v-bind:')) {
            const attrName = attr.name.slice(7);
            const key = attr.value;
+           new Watcher(vm, key, newValue => {
+             node.setAttribute(attrName, newValue);
+           });
            node.setAttribute(attrName, vm[key]);
          } else if (attr.name.startsWith('v-on:')) {
            let [eventName, ...modifiers] = attr.name.slice(5).split('.');
            const methodExpression = attr.value.trim();
            let methodName = methodExpression;
            let args = [];
            const parenIndex = methodExpression.indexOf('(');
            if (parenIndex > -1) {
              methodName = methodExpression.substring(0, parenIndex);
              const argsString = methodExpression.substring(
                parenIndex + 1,
                methodExpression.length - 1
              ).trim();
              args = argsString.split(',').map(arg => arg.trim());
            }
            const options = {
              capture: modifiers.includes('capture'),
              once: modifiers.includes('once'),
              passive: modifiers.includes('passive')
            };
            node.addEventListener(eventName, function handler(event) {
+             if (modifiers.includes('stop')) event.stopPropagation();
+             if (modifiers.includes('prevent')) event.preventDefault();
+             if (modifiers.includes('self') && event.target !== event.currentTarget) return;
+             const keyCodes = {
+               enter: 13,
+               tab: 9,
+               delete: [8, 46],
+               esc: 27,
+               space: 32,
+               up: 38,
+               down: 40,
+               left: 37,
+               right: 39
+             };
+             const keyCode = keyCodes[modifiers.find(mod => keyCodes.hasOwnProperty(mod))];
+             if (keyCode) {
+               if (Array.isArray(keyCode)) {
+                 if (!keyCode.includes(event.keyCode)) return;
+               } else if (event.keyCode !== keyCode) {
+                 return;
+               }
+             }
+             const actualArgs = args.map(arg => {
+               if (arg === '$event') return event;
+               if (!isNaN(arg)) return Number(arg);
+               if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
+               return vm[arg];
+             });
+             vm[methodName](...actualArgs);
+           }, options);
          }
        });
        this.compile(node);
      } else if (node.nodeType === TEXT_NODE) {
+       const originalTextContent = node.textContent; 
+       const updateTextContent = () => {
+           let text = originalTextContent; 
+           text = text.replace(/\{\{(.+?)\}\}/g, (_, key) => {
+               return vm[key.trim()];
+           });
+           node.textContent = text;
+       };
+       updateTextContent();
+       let match;
+       const reg = /\{\{(.+?)\}\}/g;
+       while ((match = reg.exec(originalTextContent)) !== null) {
+           const key = match[1].trim();
+           new Watcher(vm, key, updateTextContent);
+       }
      }
    });
  }
}

14. 嵌套对象 #

14.1 deep.html #

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <div id="app" v-cloak>
             {{user.name}}
        </div>
        <script src="v2.js"></script>
        <script>
        var vm = new Vue({
            el:'#app',
            data:{//data的属性都是依赖
              user:{
                name:'beijing'
              }
            },
            methods:{

            }
        });
        vm.user.name='guangzhou'
    </script>
    </body>
</html>

14.2 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;

class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}

class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    Dep.target = this;
    this.oldValue = this.getVal(this.vm, this.expr);
    Dep.target = null;
  }
  update() {
    const newValue = this.getVal(this.vm, this.expr);
    if (this.oldValue !== newValue) {
      this.cb(this.oldValue, newValue);
      this.oldValue = newValue;
    }
  }
  getVal(vm, expr) {
    const keys = expr.split('.');
    return keys.reduce((prev, next) => {
      return prev[next];
    }, vm);
  }
}

class Vue {
  constructor(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
    this.init();
  }
  init() {
    this.proxyData();
    this.proxyMethods();
    this.observe(this.$data);
    this.compile(this.$el);
  }
  observe(obj) {
    if (!obj || typeof obj !== 'object') {
      return;
    }
    Object.keys(obj).forEach(key => {
      this.defineReactive(obj, key, obj[key]);
    });
  }
  defineReactive(obj, key, val) {
    this.observe(val);
    const dep = new Dep();
    const vm = this; 
    Object.defineProperty(obj, key, {
      get() {
        if (Dep.target) {
          dep.addSub(Dep.target);
        }
        return val;
      },
      set(newVal) {
        if (newVal === val) {
          return;
        }
        val = newVal;
        vm.observe(newVal);
        dep.notify();
      }
    });
  }  
  proxyMethods() {
    for (let key in this.$methods) {
      this[key] = this.$methods[key].bind(this);
    }
  }
  proxyData() {
    for (let key in this.$data) {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newVal) {
          this.$data[key] = newVal;
        }
      });
    }
  }
  compile(el) {
    const vm = this;
    const { childNodes } = el;
    [...childNodes].forEach(node => {
      if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
        return;
      }
      if (node.nodeType === ELEMENT_NODE) {
        // compileAttributes([...node.attributes], node, vm);  // 如果需要,编译属性
        this.compile(node);
      } else if (node.nodeType === TEXT_NODE) {
        let { originalTextContent } = node;
        if (!originalTextContent) {
          originalTextContent = node.textContent;
          node.originalTextContent = originalTextContent;
        }
        let reg = /\{\{(.+?)\}\}/g;
        const updateTextContent = () => {
          node.textContent = originalTextContent.replace(reg, (_, key) => {
            debugger
            return this.getVal(vm, key.trim()); // 注意这里使用了 getVal 方法
          });
        };
        updateTextContent();
        let match;
        while ((match = reg.exec(originalTextContent)) !== null) {
          const key = match[1].trim();
          new Watcher(vm, key, updateTextContent);
        }
      }
    });
    el.removeAttribute('v-cloak');
  }
  getVal(vm, expr) {
    const keys = expr.split('.');
    return keys.reduce((prev, next) => {
      return prev[next];
    }, vm);
  }
}
function compileAttributes(attributes, node, vm) {
  attributes.forEach(attr => {
    if (attr.name === 'v-text') {
      handleVText(node, attr, vm);
    } else if (attr.name === 'v-html') {
      handleVHtml(node, attr, vm);
    } else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
      handleVBind(node, attr, vm);
    } else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
      handleEvent(node, attr, vm);
    }
  });
}
function handleVText(node, attr, vm) {
  const expr = attr.value;
  node.textContent = vm[expr];
}
function handleVHtml(node, attr, vm) {
  const expr = attr.value;
  node.innerHTML = vm[expr];
}
function handleVBind(node, attr, vm) {
  let attrName = attr.name;
  if (attrName.startsWith(':')) {
    attrName = `v-bind${attrName}`;
  }
  attrName = attrName.slice(7);
  const key = attr.value;
  node.setAttribute(attrName, vm[key]);
}
function handleEvent(node, attr, vm) {
  let attrName = attr.name;
  if (attrName.startsWith('@')) {
    attrName = `v-on:${attrName.slice(1)}`;
  }
  const [eventName, ...modifiers] = attrName.slice(5).split('.');
  const methodExpression = attr.value.trim();
  let methodName = methodExpression;
  let args = [];
  let parenIndex = methodExpression.indexOf('(');
  if (parenIndex !== -1) {
    methodName = methodExpression.substring(0, parenIndex);
    const argsString = methodExpression.substring(parenIndex + 1, methodExpression.length - 1).trim();
    args = argsString.split(',').map(arg => arg.trim());
  }
  let options = {
    capture: modifiers.includes('capture'),
    once: modifiers.includes('once')
  };
  node.addEventListener(eventName, event => {
    if (modifiers.includes('stop')) event.stopPropagation();
    if (modifiers.includes('prevent')) event.preventDefault();
    if (modifiers.includes('self')) {
      if (event.target !== event.currentTarget) {
        return;
      }
    }
    const actualArgs = args.map(arg => {
      if (!isNaN(arg)) return arg;
      if (arg === '$event') return event;
      if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
      return vm[arg];
    });
    vm[methodName](...actualArgs);
  }, options);
}

15. 支持数组 #

15.1 array.html #

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <div id="app" v-cloak>
             {{users[0].name}}
             {{users.length}}
        </div>
        <script src="v3.js"></script>
        <script>
        var vm = new Vue({
            el:'#app',
            data:{//data的属性都是依赖
              users:[
                {id:1,name:'zhangsan'}
              ]
            }
        });
        vm.users[0].name='lisi'
        setTimeout(()=>{
          vm.users.push({id:2,name:'wangwu'});
        },3000);
    </script>
    </body>
</html>

15.2 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    Dep.target = this;
    this.oldValue = this.getVal(this.vm, this.expr);
    Dep.target = null;
  }
  update() {
    const newValue = this.getVal(this.vm, this.expr);
    if (this.oldValue !== newValue) {
      this.cb(this.oldValue, newValue);
      this.oldValue = newValue;
    }
  }
  getVal(vm, expr) {
    const keys = expr.split(/[\.\[\]]/).filter(Boolean);
    return keys.reduce((prev, next) => {
      if (next.match(/^\d+$/)) { // 检查是否为数字
        return prev[parseInt(next, 10)];
      }
      return prev[next];
    }, vm);
  }
}
class Vue {
  constructor(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
    this.init();
  }
  init() {
    this.proxyData();
    this.proxyMethods();
    this.observe(this.$data);
    this.compile(this.$el);
  }
  observe(obj) {
    if (Array.isArray(obj)) {
      this.observeArray(obj);
      return;
    }
    if (!obj || typeof obj !== 'object') {
      return;
    }
    Object.keys(obj).forEach(key => {
      this.defineReactive(obj, key, obj[key]);
    });
  }
  defineReactive(obj, key, val) {
    this.observe(val);
    const dep = new Dep();
    const _this = this;
    Object.defineProperty(obj, key, {
      get() {
        if (Dep.target) {
          dep.addSub(Dep.target);
          if (Array.isArray(val)) {
            val.__dep__ = dep;  // 如果 val 是数组,将它的 __dep__ 属性设置为这个依赖
          }
        }
        return val;
      },
      set(newVal) {
        if (newVal === val) {
          return;
        }
        val = newVal;
        _this.observe(newVal);
        dep.notify();
      }
    });
  }  
  observeArray(arr) {
    arr.__proto__ = this.arrayMethods();
    arr.__dep__ = new Dep();  // 添加这行,初始化数组的依赖
    for(let i = 0; i < arr.length; i++) {
      this.observe(arr[i]);
    }
  }
  arrayMethods() {
    let arrProto = Array.prototype;
    let arrayMethods = Object.create(arrProto);
    let _this = this;
    [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ].forEach(method => {
      arrayMethods[method] = function () {
        const result = arrProto[method].apply(this, arguments);
        let inserted;
        switch (method) {
          case 'push':
          case 'unshift':
            inserted = arguments;
            break;
          case 'splice':
            inserted = arguments.slice(2);
            break;
        }
        if (inserted) _this.observeArray(inserted);
        debugger
        if (this.__dep__) {
          this.__dep__.notify();
        }
        return result;
      };
    });
    return arrayMethods;
  }
  proxyMethods() {
    for (let key in this.$methods) {
      this[key] = this.$methods[key].bind(this);
    }
  }
  proxyData() {
    for (let key in this.$data) {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newVal) {
          this.$data[key] = newVal;
        }
      });
    }
  }
  compile(el) {
    const vm = this;
    const {
      childNodes
    } = el;
    [...childNodes].forEach(node => {
      if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
        return;
      }
      if (node.nodeType === ELEMENT_NODE) {
        this.compile(node);
      } else if (node.nodeType === TEXT_NODE) {
        let {
          originalTextContent
        } = node;
        if (!originalTextContent) {
          originalTextContent = node.textContent;
          node.originalTextContent = originalTextContent;
        }
        let reg = /\{\{(.+?)\}\}/g;
        const updateTextContent = () => {
          node.textContent = originalTextContent.replace(reg, (_, key) => {
            return this.getVal(vm, key.trim());
          });
        };
        updateTextContent();
        let match;
        while ((match = reg.exec(originalTextContent)) !== null) {
          const key = match[1].trim();
          new Watcher(vm, key, updateTextContent);
        }
      }
    });
    el.removeAttribute('v-cloak');
  }
  getVal(vm, expr) {
    const keys = expr.split(/[\.\[\]]/).filter(Boolean);
    return keys.reduce((prev, next) => {
      if (next.match(/^\d+$/)) { // 检查是否为数字
        return prev[parseInt(next, 10)];
      }
      return prev[next];
    }, vm);
  }
}
function compileAttributes(attributes, node, vm) {
  attributes.forEach(attr => {
    if (attr.name === 'v-text') {
      handleVText(node, attr, vm);
    } else if (attr.name === 'v-html') {
      handleVHtml(node, attr, vm);
    } else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
      handleVBind(node, attr, vm);
    } else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
      handleEvent(node, attr, vm);
    }
  });
}
function handleVText(node, attr, vm) {
  const expr = attr.value;
  node.textContent = vm[expr];
}
function handleVHtml(node, attr, vm) {
  const expr = attr.value;
  node.innerHTML = vm[expr];
}
function handleVBind(node, attr, vm) {
  let attrName = attr.name;
  if (attrName.startsWith(':')) {
    attrName = `v-bind${attrName}`;
  }
  attrName = attrName.slice(7);
  const key = attr.value;
  node.setAttribute(attrName, vm[key]);
}
function handleEvent(node, attr, vm) {
  let attrName = attr.name;
  if (attrName.startsWith('@')) {
    attrName = `v-on:${attrName.slice(1)}`;
  }
  const [eventName, ...modifiers] = attrName.slice(5).split('.');
  const methodExpression = attr.value.trim();
  let methodName = methodExpression;
  let args = [];
  let parenIndex = methodExpression.indexOf('(');
  if (parenIndex !== -1) {
    methodName = methodExpression.substring(0, parenIndex);
    const argsString = methodExpression.substring(parenIndex + 1, methodExpression.length - 1).trim();
    args = argsString.split(',').map(arg => arg.trim());
  }
  let options = {
    capture: modifiers.includes('capture'),
    once: modifiers.includes('once')
  };
  node.addEventListener(eventName, event => {
    if (modifiers.includes('stop')) event.stopPropagation();
    if (modifiers.includes('prevent')) event.preventDefault();
    if (modifiers.includes('self')) {
      if (event.target !== event.currentTarget) {
        return;
      }
    }
    const actualArgs = args.map(arg => {
      if (!isNaN(arg)) return arg;
      if (arg === '$event') return event;
      if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
      return vm[arg];
    });
    vm[methodName](...actualArgs);
  }, options);
}

11.v-model #

v-model 是 Vue.js 中一个非常重要的指令,用于实现数据的双向绑定。这意味着,该指令能让你将输入控件(如 <input><textarea><select>)与数据对象的一个属性进行绑定,当其中一方变化时,另一方也会相应地变化。

这是一个基础的使用 v-model 的例子:

<template>
  <div>
    <input v-model="message">
    <p>你输入的内容是:{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: ''
    };
  }
}
</script>

在这个例子中,<input> 元素与 message 数据属性双向绑定。也就是说,当用户在输入框中输入文本时,message 数据属性会自动更新;反过来,如果 message 数据属性变化,输入框中的内容也会跟着变。

如何工作的?

实际上,v-modelv-bindv-on 的语法糖。上述的例子等效于:

<template>
  <div>
    <input :value="message" @input="message = $event.target.value">
    <p>你输入的内容是:{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: ''
    };
  }
}
</script>

在这个等效的例子中,:value="message" 使输入框的值与 message 数据属性绑定(单向绑定),而 @input="message = $event.target.value" 监听输入框的 input 事件,并在事件发生时更新 message 数据属性。

应用场景

  1. 表单控件:用于 <input><textarea><select> 等。
  2. 组件:你也可以在自定义组件上使用 v-model

v-model 是 Vue.js 中一个非常实用的指令,它简化了表单和组件与数据之间双向通信的逻辑。

11.1 v-model.html #

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Vue</title>
</head>

<body>
  <div id="app">
    <input v-model="msg">
    <p>msg is: {{ msg }}</p>
  </div>
  <script src="my-vue.js"></script>
  <script>
    new Vue({
      el: '#app',
      data: {
        msg: 'Hello'
      }
    })
  </script>
</body>

</html>

11.2 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    Dep.target = this;
    this.oldValue = this.vm[this.expr];
    Dep.target = null;
  }
  update() {
    const newValue = this.vm[this.expr];
    if (this.oldValue !== newValue) {
      this.cb(newValue);
      this.oldValue = newValue;
    }
  }
}
class Vue {
  constructor(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
    this.init();
  }
  init() {
    this.proxyData();
    this.proxyMethods();
    this.observe(this.$data);
    this.compile(this.$el);
  }
  proxyMethods() {
    for (let key in this.$methods) {
      this[key] = this.$methods[key].bind(this);
    }
  }
  observe(obj) {
    for (let key in obj) {
      let value = obj[key];
      const dep = new Dep();
      Object.defineProperty(obj, key, {
        get() {
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return value;
        },
        set: (newValue) => {
          if (newValue !== value) {
            value = newValue;
            dep.notify();
          }
        }
      });
    }
  }
  proxyData() {
    for (let key in this.$data) {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newVal) {
          this.$data[key] = newVal;
        }
      });
    }
  }
  compile(el) {
    const vm = this;
    const { childNodes } = el;
    Array.from(childNodes).forEach(node => {
      if (node.nodeType === ELEMENT_NODE) {
        if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
          return;
        }
        const attrs = node.attributes;
        Array.from(attrs).forEach(attr => {
          if (attr.name === 'v-text') {
            handleVText(node, attr, vm);
          } else if (attr.name === 'v-html') {
            handleVHtml(node, attr, vm);
          } else if (attr.name.startsWith('v-bind:')) {
            handleVBind(node, attr, vm);
+         }else if (attr.name.startsWith('@')) {
+           handleEvent(node, attr, vm);
+         }else if (attr.name.startsWith('v-on:')) {
+           handleEvent(node, attr, vm);
+         }else if (attr.name === 'v-model') {
+           handleVModel(node, attr, vm);
+         }
        });
        this.compile(node);
      } else if (node.nodeType === TEXT_NODE) {
        const originalTextContent = node.textContent;
        const updateTextContent = () => {
          let text = originalTextContent;
          text = text.replace(/\{\{(.+?)\}\}/g, (_, key) => {
            return vm[key.trim()];
          });
          node.textContent = text;
        };
        updateTextContent();
        let match;
        const reg = /\{\{(.+?)\}\}/g;
        while ((match = reg.exec(originalTextContent)) !== null) {
          const key = match[1].trim();
          new Watcher(vm, key, updateTextContent);
        }
      }
    });
  }
}
+function handleVModel(node, attr, vm) {
+  const key = attr.value;
+  node.value = vm[key];
+  new Watcher(vm, key, function() {
+    node.value = vm[key];
+  });
+  node.addEventListener('input', function() {
+    vm[key] = node.value;
+  });
+}
function handleVText(node, attr, vm) {
  const expr = attr.value;
  new Watcher(vm, expr, newValue => {
    node.textContent = newValue;
  });
  node.textContent = vm[expr];
}

function handleVHtml(node, attr, vm) {
  const expr = attr.value;
  new Watcher(vm, expr, newValue => {
    node.innerHTML = newValue;
  });
  node.innerHTML = vm[expr];
}

function handleVBind(node, attr, vm) {
  const attrName = attr.name.slice(7);
  const key = attr.value;
  new Watcher(vm, key, newValue => {
    node.setAttribute(attrName, newValue);
  });
  node.setAttribute(attrName, vm[key]);
}

function handleEvent(node, attr, vm) {
  let attrName = attr.name;
  if(attrName.startsWith('@')){
    attrName=`v-on:${attr.name.slice(1)}`;
  }
  let [eventName, ...modifiers] = attrName.slice(5).split('.');
  const methodExpression = attr.value.trim();
  let methodName = methodExpression;
  let args = [];

  const parenIndex = methodExpression.indexOf('(');
  if (parenIndex > -1) {
    methodName = methodExpression.substring(0, parenIndex);
    const argsString = methodExpression.substring(
      parenIndex + 1,
      methodExpression.length - 1
    ).trim();
    args = argsString.split(',').map(arg => arg.trim());
  }

  const options = {
    capture: modifiers.includes('capture'),
    once: modifiers.includes('once'),
    passive: modifiers.includes('passive')
  };

  node.addEventListener(eventName, function handler(event) {
    if (modifiers.includes('stop')) event.stopPropagation();
    if (modifiers.includes('prevent')) event.preventDefault();
    if (modifiers.includes('self') && event.target !== event.currentTarget) return;
    const keyCodes = {
      enter: 13,
      tab: 9,
      delete: [8, 46],
      esc: 27,
      space: 32,
      up: 38,
      down: 40,
      left: 37,
      right: 39
    };
    const keyCode = keyCodes[modifiers.find(mod => keyCodes.hasOwnProperty(mod))];
    if (keyCode) {
      if (Array.isArray(keyCode)) {
        if (!keyCode.includes(event.keyCode)) return;
      } else if (event.keyCode !== keyCode) {
        return;
      }
    }
    const actualArgs = args.map(arg => {
      if (arg === '$event') return event;
      if (!isNaN(arg)) return Number(arg);
      if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
      return vm[arg];
    });
    vm[methodName](...actualArgs);
    if (modifiers.includes('once')) {
      node.removeEventListener(eventName, handler);
    }
  }, options);
}

12.v-once #

基本用法

<div v-once>
  {{ message }}
</div>

在这个例子中,不论 message 如何改变,该 <div> 元素及其内部的内容只会渲染一次。

组件中的使用

您也可以在自定义组件上使用 v-once

<my-component v-once></my-component>

使用 v-once 的组件仅会初始化和渲染一次,之后即使传入的 props 改变了,也不会再次渲染。

结合 v-for 使用

如果您有一个不会改变的列表,您也可以结合 v-for 使用 v-once

<ul>
  <li v-for="item in items" v-once>{{ item }}</li>
</ul>

请注意,在这里使用 v-once 能显著提高列表的渲染性能,因为列表项不会重新渲染。

使用场景

虽然 v-once 在某些性能优化场景中很有用,但请注意不要过度使用它。过度使用可能导致应用逻辑变得难以理解和维护。只有在确定某个元素或组件确实不需要重新渲染时,才应使用 v-once

12.1 v-once.html #

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Vue</title>
</head>

<body>
  <div id="app">
    <span v-once>永不改变: {{msg}}</span>
  </div>
  <script src="my-vue.js"></script>
  <script>
    var vm = new Vue({
      el: '#app',
      data: {
        msg: 'hello'
      }
    })
    vm.msg = 'world'
  </script>
</body>

</html>

12.2 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    Dep.target = this;
    this.oldValue = this.vm[this.expr];
    Dep.target = null;
  }
  update() {
    const newValue = this.vm[this.expr];
    if (this.oldValue !== newValue) {
      this.cb(newValue);
      this.oldValue = newValue;
    }
  }
}
class Vue {
  constructor(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
    this.init();
  }
  init() {
    this.proxyData();
    this.proxyMethods();
    this.observe(this.$data);
    this.compile(this.$el);
  }
  proxyMethods() {
    for (let key in this.$methods) {
      this[key] = this.$methods[key].bind(this);
    }
  }
  observe(obj) {
    for (let key in obj) {
      let value = obj[key];
      const dep = new Dep();
      Object.defineProperty(obj, key, {
        get() {
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return value;
        },
        set: (newValue) => {
          if (newValue !== value) {
            value = newValue;
            dep.notify();
          }
        }
      });
    }
  }
  proxyData() {
    for (let key in this.$data) {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newVal) {
          this.$data[key] = newVal;
        }
      });
    }
  }
+ compile(el,once) {
    const vm = this;
    const { childNodes } = el;
    Array.from(childNodes).forEach(node => {
      if (node.nodeType === ELEMENT_NODE) {
        if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
          return;
        }
        const attrs = node.attributes;
        Array.from(attrs).forEach(attr => {
          if (attr.name === 'v-text') {
            handleVText(node, attr, vm);
          } else if (attr.name === 'v-html') {
            handleVHtml(node, attr, vm);
          } else if (attr.name.startsWith('v-bind:')) {
            handleVBind(node, attr, vm);
          } else if (attr.name.startsWith('v-on:')) {
            handleVOn(node, attr, vm);
          } else if (attr.name === 'v-model') {
            handleVModel(node, attr, vm);
          }
        });
+       if (node.hasAttribute('v-once')) {
+         this.compile(node,true);
+       }else{
+         this.compile(node);
        }
      } else if (node.nodeType === TEXT_NODE) {
        const originalTextContent = node.textContent;
        const updateTextContent = () => {
          let text = originalTextContent;
          text = text.replace(/\{\{(.+?)\}\}/g, (_, key) => {
            return vm[key.trim()];
          });
          node.textContent = text;
        };
        updateTextContent();
+       if (!once) {
          let match;
          const reg = /\{\{(.+?)\}\}/g;
          while ((match = reg.exec(originalTextContent)) !== null) {
            const key = match[1].trim();
            new Watcher(vm, key, updateTextContent);
          }
+       }
      }
    });
  }
}
function handleVModel(node, attr, vm) {
  const key = attr.value;
  node.value = vm[key];
  new Watcher(vm, key, function () {
    node.value = vm[key];
  });
  node.addEventListener('input', function () {
    vm[key] = node.value;
  });
}
function handleVText(node, attr, vm) {
  const expr = attr.value;
  new Watcher(vm, expr, newValue => {
    node.textContent = newValue;
  });
  node.textContent = vm[expr];
}

function handleVHtml(node, attr, vm) {
  const expr = attr.value;
  new Watcher(vm, expr, newValue => {
    node.innerHTML = newValue;
  });
  node.innerHTML = vm[expr];
}

function handleVBind(node, attr, vm) {
  const attrName = attr.name.slice(7);
  const key = attr.value;
  new Watcher(vm, key, newValue => {
    node.setAttribute(attrName, newValue);
  });
  node.setAttribute(attrName, vm[key]);
}

function handleVOn(node, attr, vm) {
  let [eventName, ...modifiers] = attr.name.slice(5).split('.');
  const methodExpression = attr.value.trim();
  let methodName = methodExpression;
  let args = [];

  const parenIndex = methodExpression.indexOf('(');
  if (parenIndex > -1) {
    methodName = methodExpression.substring(0, parenIndex);
    const argsString = methodExpression.substring(
      parenIndex + 1,
      methodExpression.length - 1
    ).trim();
    args = argsString.split(',').map(arg => arg.trim());
  }

  const options = {
    capture: modifiers.includes('capture'),
    once: modifiers.includes('once'),
    passive: modifiers.includes('passive')
  };

  node.addEventListener(eventName, function handler(event) {
    if (modifiers.includes('stop')) event.stopPropagation();
    if (modifiers.includes('prevent')) event.preventDefault();
    if (modifiers.includes('self') && event.target !== event.currentTarget) return;
    const keyCodes = {
      enter: 13,
      tab: 9,
      delete: [8, 46],
      esc: 27,
      space: 32,
      up: 38,
      down: 40,
      left: 37,
      right: 39
    };
    const keyCode = keyCodes[modifiers.find(mod => keyCodes.hasOwnProperty(mod))];
    if (keyCode) {
      if (Array.isArray(keyCode)) {
        if (!keyCode.includes(event.keyCode)) return;
      } else if (event.keyCode !== keyCode) {
        return;
      }
    }
    const actualArgs = args.map(arg => {
      if (arg === '$event') return event;
      if (!isNaN(arg)) return Number(arg);
      if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
      return vm[arg];
    });
    vm[methodName](...actualArgs);
    if (modifiers.includes('once')) {
      node.removeEventListener(eventName, handler);
    }
  }, options);
}

12.v-show #

v-show 是 Vue.js 中用于条件渲染的一个指令。使用 v-show 指令可以基于一个表达式的真假值来显示或隐藏一个元素。与 v-if 类似,v-show 也可以用于条件性地显示或隐藏内容,但两者的工作机制有所不同。

基础用法

<!-- isVisible 是一个布尔值 -->
<div v-show="isVisible">我是可见的</div>

在这个例子中,如果 isVisible 的值为 true,那么这个 <div> 元素会显示在页面上。如果 isVisible 的值为 false,这个 <div> 元素会被隐藏。

工作机制v-if 不同的是,v-show 不会从 DOM 中添加或删除元素。而是通过改变 CSS 的 display 属性来控制元素的可见性。当条件为 false 时,v-show 会给元素添加一个 display: none 的样式。

优点

缺点

v-show 与 v-if 的选择

注意事项

12.1 v-show.html #

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue</title>
</head>
<body>
  <div id="app">
    <div v-show="isShowing">Hello</div>
    <button v-on:click="toggle">toggle</button>
  </div>
  <script src="my-vue.js"></script>
  <script>
    new Vue({
      el: '#app',
      data: {
        isShowing: true
      },
      methods: {
        toggle: function () {
          this.isShowing = !this.isShowing;
        }
      }
    })
  </script>
</body>
</html>

12.2 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm;
    this.expr = expr;
    this.cb = cb;
    Dep.target = this;
    this.oldValue = this.vm[this.expr];
    Dep.target = null;
  }
  update() {
    const newValue = this.vm[this.expr];
    if (this.oldValue !== newValue) {
      this.cb(newValue);
      this.oldValue = newValue;
    }
  }
}
class Vue {
  constructor(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
    this.init();
  }
  init() {
    this.proxyData();
    this.proxyMethods();
    this.observe(this.$data);
    this.compile(this.$el);
  }
  proxyMethods() {
    for (let key in this.$methods) {
      this[key] = this.$methods[key].bind(this);
    }
  }
  observe(obj) {
    for (let key in obj) {
      let value = obj[key];
      const dep = new Dep();
      Object.defineProperty(obj, key, {
        get() {
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return value;
        },
        set: (newValue) => {
          if (newValue !== value) {
            value = newValue;
            dep.notify();
          }
        }
      });
    }
  }
  proxyData() {
    for (let key in this.$data) {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newVal) {
          this.$data[key] = newVal;
        }
      });
    }
  }
  compile(el,once) {
    const vm = this;
    const { childNodes } = el;
    Array.from(childNodes).forEach(node => {
      if (node.nodeType === ELEMENT_NODE) {
        if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
          return;
        }
        const attrs = node.attributes;
        Array.from(attrs).forEach(attr => {
          if (attr.name === 'v-text') {
            handleVText(node, attr, vm);
          } else if (attr.name === 'v-html') {
            handleVHtml(node, attr, vm);
          } else if (attr.name.startsWith('v-bind:')) {
            handleVBind(node, attr, vm);
          } else if (attr.name.startsWith('v-on:')) {
            handleVOn(node, attr, vm);
          } else if (attr.name === 'v-model') {
            handleVModel(node, attr, vm);
+         }else if (attr.name === 'v-show') {
+           handleVShow(node, attr, vm);
+         }
        });
        if (node.hasAttribute('v-once')) {
          this.compile(node,true);
        }else{
          this.compile(node);
        }
      } else if (node.nodeType === TEXT_NODE) {
        const originalTextContent = node.textContent;
        const updateTextContent = () => {
          let text = originalTextContent;
          text = text.replace(/\{\{(.+?)\}\}/g, (_, key) => {
            return vm[key.trim()];
          });
          node.textContent = text;
        };
        updateTextContent();
        if (!once) {
          let match;
          const reg = /\{\{(.+?)\}\}/g;
          while ((match = reg.exec(originalTextContent)) !== null) {
            const key = match[1].trim();
            new Watcher(vm, key, updateTextContent);
          }
        }
      }
    });
  }
}
+function handleVShow(node, attr, vm) {
+  const expr = attr.value;
+  const update = () => {
+    node.style.display = vm[expr] ? '' : 'none';
+  };
+  update();
+  new Watcher(vm, expr, update);
+}
function handleVModel(node, attr, vm) {
  const key = attr.value;
  node.value = vm[key];
  new Watcher(vm, key, function () {
    node.value = vm[key];
  });
  node.addEventListener('input', function () {
    vm[key] = node.value;
  });
}
function handleVText(node, attr, vm) {
  const expr = attr.value;
  new Watcher(vm, expr, newValue => {
    node.textContent = newValue;
  });
  node.textContent = vm[expr];
}

function handleVHtml(node, attr, vm) {
  const expr = attr.value;
  new Watcher(vm, expr, newValue => {
    node.innerHTML = newValue;
  });
  node.innerHTML = vm[expr];
}

function handleVBind(node, attr, vm) {
  const attrName = attr.name.slice(7);
  const key = attr.value;
  new Watcher(vm, key, newValue => {
    node.setAttribute(attrName, newValue);
  });
  node.setAttribute(attrName, vm[key]);
}

function handleVOn(node, attr, vm) {
  let [eventName, ...modifiers] = attr.name.slice(5).split('.');
  const methodExpression = attr.value.trim();
  let methodName = methodExpression;
  let args = [];

  const parenIndex = methodExpression.indexOf('(');
  if (parenIndex > -1) {
    methodName = methodExpression.substring(0, parenIndex);
    const argsString = methodExpression.substring(
      parenIndex + 1,
      methodExpression.length - 1
    ).trim();
    args = argsString.split(',').map(arg => arg.trim());
  }

  const options = {
    capture: modifiers.includes('capture'),
    once: modifiers.includes('once'),
    passive: modifiers.includes('passive')
  };

  node.addEventListener(eventName, function handler(event) {
    if (modifiers.includes('stop')) event.stopPropagation();
    if (modifiers.includes('prevent')) event.preventDefault();
    if (modifiers.includes('self') && event.target !== event.currentTarget) return;
    const keyCodes = {
      enter: 13,
      tab: 9,
      delete: [8, 46],
      esc: 27,
      space: 32,
      up: 38,
      down: 40,
      left: 37,
      right: 39
    };
    const keyCode = keyCodes[modifiers.find(mod => keyCodes.hasOwnProperty(mod))];
    if (keyCode) {
      if (Array.isArray(keyCode)) {
        if (!keyCode.includes(event.keyCode)) return;
      } else if (event.keyCode !== keyCode) {
        return;
      }
    }
    const actualArgs = args.map(arg => {
      if (arg === '$event') return event;
      if (!isNaN(arg)) return Number(arg);
      if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
      return vm[arg];
    });
    vm[methodName](...actualArgs);
    if (modifiers.includes('once')) {
      node.removeEventListener(eventName, handler);
    }
  }, options);
}

13.v-if #

v-if, v-else-if, 和 v-else 是 Vue.js 中用于条件渲染的指令。这些指令允许你根据一些条件动态地添加或移除 DOM 元素。

v-if

v-if 用于条件性地渲染一个块。该块只会在指令的表达式返回 true 时被渲染。

<div v-if="showMessage">
  Hello, World!
</div>
new Vue({
  data: {
    showMessage: true
  }
});

在这个例子中,当 showMessage 的值为 true 时,<div> 会被添加到 DOM 中。当 showMessage 的值变为 false 时,该 <div> 会从 DOM 中移除。

v-else-if

v-else-if 用于表示一个“否则,如果”的块,它必须紧跟在一个 v-if 或另一个 v-else-if 块之后。

<div v-if="type === 'A'">
  A 类型
</div>
<div v-else-if="type === 'B'">
  B 类型
</div>

v-else

v-else 用于表示一个“否则”的块,它必须紧跟在一个 v-ifv-else-if 块之后。

<div v-if="type === 'A'">
  A 类型
</div>
<div v-else>
  非 A 类型
</div>

组合使用

你可以将这三个指令组合在一起,创建一个完整的条件块链。

<div v-if="type === 'A'">
  A 类型
</div>
<div v-else-if="type === 'B'">
  B 类型
</div>
<div v-else>
  其他类型
</div>

注意事项

13.1 v-if.html #

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Vue</title>
</head>

<body>
  <div id="app">
    <div v-if="value === 'A'">A</div>
    <div v-else-if="value === 'B'">B</div>
    <div v-else>C</div>
    <button v-on:click="changeValue">changeValue</button>
  </div>
  <script src="my-vue.js"></script>
  <script>
    new Vue({
      el: '#app',
      data: {
        value: 'A'
      },
      methods: {
        changeValue: function () {
          if (this.value === 'A') {
            this.value = 'B';
          } else if (this.value === 'B') {
            this.value = 'C';
          }else {
            this.value = 'A';
          }
        }
      }
    })
  </script>
</body>

</html>

13.2 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
class Watcher {
+ constructor(vm, exprOrFn, cb) {
    this.vm = vm;
    this.cb = cb;
+   if (typeof exprOrFn === 'function') {
+     this.getter = exprOrFn;
+   } else {
+     this.getter = () => {
+       return vm[exprOrFn];
+     };
+   }
    Dep.target = this;
+   this.oldValue = this.get();
    Dep.target = null;
  }
+ get() {
+   return this.getter.call(this.vm);
+ }
  update() {
+   const newValue = this.get();
    if (this.oldValue !== newValue) {
      this.cb(newValue);
      this.oldValue = newValue;
    }
  }
}

class Vue {
  constructor(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
    this.init();
  }
  init() {
    this.proxyData();
    this.proxyMethods();
    this.observe(this.$data);
    this.compile(this.$el);
  }
  proxyMethods() {
    for (let key in this.$methods) {
      this[key] = this.$methods[key].bind(this);
    }
  }
  observe(obj) {
    for (let key in obj) {
      let value = obj[key];
      const dep = new Dep();
      Object.defineProperty(obj, key, {
        get() {
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return value;
        },
        set: (newValue) => {
          if (newValue !== value) {
            value = newValue;
            dep.notify();
          }
        }
      });
    }
  }
  proxyData() {
    for (let key in this.$data) {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newVal) {
          this.$data[key] = newVal;
        }
      });
    }
  }
  compile(el, once) {
    const vm = this;
    const { childNodes } = el;
+   const conditionalElements = [];
    Array.from(childNodes).forEach(node => {
      if (node.nodeType === ELEMENT_NODE) {
+        if (
+           node.hasAttribute('v-if') ||
+           node.hasAttribute('v-else-if') ||
+           node.hasAttribute('v-else')
+         ) {
+           conditionalElements.push(node);
+         }
        if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
          return;
        }
        const attrs = node.attributes;
        Array.from(attrs).forEach(attr => {
          if (attr.name === 'v-text') {
            handleVText(node, attr, vm);
          } else if (attr.name === 'v-html') {
            handleVHtml(node, attr, vm);
          } else if (attr.name.startsWith('v-bind:')) {
            handleVBind(node, attr, vm);
          } else if (attr.name.startsWith('v-on:')) {
            handleVOn(node, attr, vm);
          } else if (attr.name === 'v-model') {
            handleVModel(node, attr, vm);
          }
        });
        if (node.hasAttribute('v-once')) {
          this.compile(node, true);
        } else {
          this.compile(node);
        }
      } else if (node.nodeType === TEXT_NODE) {
        const originalTextContent = node.textContent;
        const updateTextContent = () => {
          let text = originalTextContent;
          text = text.replace(/\{\{(.+?)\}\}/g, (_, key) => {
            return vm[key.trim()];
          });
          node.textContent = text;
        };
        updateTextContent();
        if (!once) {
          let match;
          const reg = /\{\{(.+?)\}\}/g;
          while ((match = reg.exec(originalTextContent)) !== null) {
            const key = match[1].trim();
            new Watcher(vm, key, updateTextContent);
          }
        }
      }
    });
+   if (conditionalElements.length > 0)
+     handleConditionals(conditionalElements, vm);
  }
}
+function handleConditionals(conditionalElements, vm) {
+    const elementsData = conditionalElements.map(element => {
+        const placeholder = document.createComment('placeholder');
+        element.parentNode.insertBefore(placeholder, element);
+        element.parentNode.removeChild(element);
+        return {
+            placeholder,
+            element,
+            evaluate: getEvaluateFn(element.getAttribute('v-if') || element.getAttribute('v-else-if') || 'true', vm)
+        };
+    });
+    const updateVisibility = () => {
+        let firstVisibleElementInserted = false;
+        elementsData.forEach(({
+            placeholder,
+            element,
+            evaluate
+        }) => {
+            const {
+                parentNode
+            } = placeholder;
+            const shouldDisplay = evaluate();
+            const isDisplayed = parentNode.contains(element);
+            if (shouldDisplay && !firstVisibleElementInserted) {
+                if (!isDisplayed) {
+                    parentNode.insertBefore(element, placeholder);
+                }
+                firstVisibleElementInserted = true;
+            } else if (isDisplayed) {
+                parentNode.removeChild(element);
+            }
+            if (shouldDisplay && !firstVisibleElementInserted) {
+                parentNode.insertBefore(element, placeholder);
+                firstVisibleElementInserted = evaluate();
+            }
+        });
+    };
+    updateVisibility();
+    new Watcher(vm,() => elementsData.map(({ evaluate }) => evaluate()),updateVisibility);
+}
+function getEvaluateFn(expr, vm) {
+    return new Function('with(this) { return ' + expr + ' }').bind(vm.$data);
+}

function handleVModel(node, attr, vm) {
  const key = attr.value;
  node.value = vm[key];
  new Watcher(vm, key, function () {
    node.value = vm[key];
  });
  node.addEventListener('input', function () {
    vm[key] = node.value;
  });
}
function handleVText(node, attr, vm) {
  const expr = attr.value;
  new Watcher(vm, expr, newValue => {
    node.textContent = newValue;
  });
  node.textContent = vm[expr];
}

function handleVHtml(node, attr, vm) {
  const expr = attr.value;
  new Watcher(vm, expr, newValue => {
    node.innerHTML = newValue;
  });
  node.innerHTML = vm[expr];
}

function handleVBind(node, attr, vm) {
  const attrName = attr.name.slice(7);
  const key = attr.value;
  new Watcher(vm, key, newValue => {
    node.setAttribute(attrName, newValue);
  });
  node.setAttribute(attrName, vm[key]);
}

function handleVOn(node, attr, vm) {
  let [eventName, ...modifiers] = attr.name.slice(5).split('.');
  const methodExpression = attr.value.trim();
  let methodName = methodExpression;
  let args = [];

  const parenIndex = methodExpression.indexOf('(');
  if (parenIndex > -1) {
    methodName = methodExpression.substring(0, parenIndex);
    const argsString = methodExpression.substring(
      parenIndex + 1,
      methodExpression.length - 1
    ).trim();
    args = argsString.split(',').map(arg => arg.trim());
  }

  const options = {
    capture: modifiers.includes('capture'),
    once: modifiers.includes('once'),
    passive: modifiers.includes('passive')
  };

  node.addEventListener(eventName, function handler(event) {
    if (modifiers.includes('stop')) event.stopPropagation();
    if (modifiers.includes('prevent')) event.preventDefault();
    if (modifiers.includes('self') && event.target !== event.currentTarget) return;
    const keyCodes = {
      enter: 13,
      tab: 9,
      delete: [8, 46],
      esc: 27,
      space: 32,
      up: 38,
      down: 40,
      left: 37,
      right: 39
    };
    const keyCode = keyCodes[modifiers.find(mod => keyCodes.hasOwnProperty(mod))];
    if (keyCode) {
      if (Array.isArray(keyCode)) {
        if (!keyCode.includes(event.keyCode)) return;
      } else if (event.keyCode !== keyCode) {
        return;
      }
    }
    const actualArgs = args.map(arg => {
      if (arg === '$event') return event;
      if (!isNaN(arg)) return Number(arg);
      if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
      return vm[arg];
    });
    vm[methodName](...actualArgs);
    if (modifiers.includes('once')) {
      node.removeEventListener(eventName, handler);
    }
  }, options);
}

14.v-for #

v-for 是 Vue.js 中用于渲染列表的指令。这个指令允许你遍历数组或对象,并为每个项生成一个子模板。

基础用法

最常见的用法是遍历一个数组:

<ul>
  <li v-for="item in items">{{ item.text }}</li>
</ul>

对应的 Vue 实例中的 data 对象可能是这样的:

new Vue({
  el: '#app',
  data: {
    items: [
      { text: 'Item 1' },
      { text: 'Item 2' },
      { text: 'Item 3' }
    ]
  }
});

这会生成一个包含三个列表项 (li) 的无序列表 (ul)。

遍历对象

除了数组,你也可以使用 v-for 来遍历对象的属性:

<ul>
  <li v-for="(value, key) in object">
    {{ key }}: {{ value }}
  </li>
</ul>

对应的 Vue 实例:

new Vue({
  el: '#app',
  data: {
    object: {
      firstName: 'John',
      lastName: 'Doe',
      age: 30
    }
  }
});

索引和键

当遍历数组时,你也可以访问当前项的索引:

<ul>
  <li v-for="(item, index) in items">{{ index }}: {{ item.text }}</li>
</ul>

当遍历对象时,除了属性值和键,还可以访问当前属性的索引:

<ul>
  <li v-for="(value, key, index) in object">
    {{ index }}. {{ key }}: {{ value }}
  </li>
</ul>

key 属性

当使用 v-for 与 Vue 组件一起使用时,推荐为每个循环项添加一个唯一的 key 属性。这有助于 Vue 更有效地更新 DOM:

<my-component
  v-for="(item, index) in items"
  :key="item.id"
  :item="item"
></my-component>

在范围内遍历

v-for 也可以用于重复一个模板多次(但没有绑定数据):

<span v-for="n in 5">{{ n }} </span>

这将输出 "1 2 3 4 5 "。

注意事项

14.1 v-for.html #

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue</title>
</head>
<body>
  <div id="app">
    <ul >
      <li v-for="item in items">
        {{ item.message }}
      </li>
    </ul>
  </div>
  <script src="my-vue.js"></script>
  <script>
    var vm = new Vue({
      el: '#app',
      data: {
        items: [
          { message: 'hello' },
          { message: 'world' }
        ]
      }
    })
    vm.items = [
      { message: 'aaa' },
      { message: 'bbb' }
    ];
  </script>
</body>
</html>

14.2 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
class Watcher {
  constructor(vm, exprOrFn, cb) {
    this.vm = vm;
    this.cb = cb;
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn;
    } else {
      this.getter = () => {
        return vm[exprOrFn];
      };
    }
    Dep.target = this;
    this.oldValue = this.get();
    Dep.target = null;
  }
  get() {
    return this.getter.call(this.vm);
  }
  update() {
    const newValue = this.get();
    if (this.oldValue !== newValue) {
      this.cb(newValue);
      this.oldValue = newValue;
    }
  }
}

class Vue {
  constructor(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
    this.init();
  }
  init() {
    this.proxyData();
    this.proxyMethods();
    this.observe(this.$data);
    this.compile(this.$el);
  }
  proxyMethods() {
    for (let key in this.$methods) {
      this[key] = this.$methods[key].bind(this);
    }
  }
  observe(obj) {
    for (let key in obj) {
      let value = obj[key];
      const dep = new Dep();
      Object.defineProperty(obj, key, {
        get() {
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return value;
        },
        set: (newValue) => {
          if (newValue !== value) {
            value = newValue;
            dep.notify();
          }
        }
      });
    }
  }
  proxyData() {
    for (let key in this.$data) {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newVal) {
          this.$data[key] = newVal;
        }
      });
    }
  }
+ compile(el, once, scope = this) {
+   const vm = scope;
    const { childNodes } = el;
    const conditionalElements = [];
    Array.from(childNodes).forEach(node => {
      if (node.nodeType === ELEMENT_NODE) {
        if (
          node.hasAttribute('v-if') ||
          node.hasAttribute('v-else-if') ||
          node.hasAttribute('v-else')
        ) {
          conditionalElements.push(node);
        }
        if (node.nodeType === 1 && node.hasAttribute('v-pre')) {
          return;
        }
        const attrs = node.attributes;
        Array.from(attrs).forEach(attr => {
          if (attr.name === 'v-text') {
            handleVText(node, attr, vm);
          } else if (attr.name === 'v-html') {
            handleVHtml(node, attr, vm);
          } else if (attr.name.startsWith('v-bind:')) {
            handleVBind(node, attr, vm);
          } else if (attr.name.startsWith('v-on:')) {
            handleVOn(node, attr, vm);
          } else if (attr.name === 'v-model') {
            handleVModel(node, attr, vm);
+         } else if (attr.name === 'v-for') {
+           handleVFor(node, attr, vm,once);
          }
        });
        if (node.hasAttribute('v-once')) {
          this.compile(node, true);
        } else {
          this.compile(node);
        }
      } else if (node.nodeType === TEXT_NODE) {
        const originalTextContent = node.textContent;
        const updateTextContent = () => {
          let text = originalTextContent;
          text = text.replace(/\{\{(.+?)\}\}/g, (_, key) => {
+           return resolveData(vm, key.trim());
          });          
          node.textContent = text;
        };
        updateTextContent();
        if (!once) {
          let match;
          const reg = /\{\{(.+?)\}\}/g;
          while ((match = reg.exec(originalTextContent)) !== null) {
            const key = match[1].trim();
            new Watcher(vm, key, updateTextContent);
          }
        }
      }
    });
    if (conditionalElements.length > 0)
      handleConditionals(conditionalElements, vm);
  }
}
+function resolveData(obj, key) {
+  const keys = key.split('.');
+  let value = obj;
+  for (const k of keys) {
+    if (value == null) return;
+    value = value[k];
+  }
+  return value;
+}

+function handleVFor(node, attr, vm,once) {
+  const originalNode = node.cloneNode(true); 
+  const parentNode = node.parentNode;
+  const placeholder = document.createComment('vue-placeholder');
+  parentNode.replaceChild(placeholder, node);
+  const expression = attr.value;
+  const [itemVar, , listKey] = expression.split(' ');
+  const update = () => {
+    while (placeholder.nextSibling) {
+      parentNode.removeChild(placeholder.nextSibling);
+    }
+    const newScope = Object.create(vm);
+    vm[listKey].forEach((itemData, index) => {
+      const clone = originalNode.cloneNode(true);
+      parentNode.appendChild(clone);
+      newScope[itemVar] = itemData;
+      newScope.index = index;
+      vm.compile(clone,once,newScope);
+    });
+  };
+  new Watcher(vm, listKey, update);
+  update();
+}

function handleConditionals(conditionalElements, vm) {
  const elementsData = conditionalElements.map(element => {
    const placeholder = document.createComment('vue-placeholder');
    element.parentNode.insertBefore(placeholder, element);
    element.parentNode.removeChild(element);
    return {
      evaluateCondition: createConditionEvaluator(element, vm),
      placeholder,
      element,
    };
  });
  const updateVisibility = () => {
    let firstVisibleElementInserted = false;
    elementsData.forEach(({ evaluateCondition, element, placeholder }) => {
      const parentNode = placeholder.parentNode;
      const shouldDisplay = evaluateCondition();
      const isDisplayed = parentNode.contains(element);
      if (shouldDisplay && !firstVisibleElementInserted) {
        if (!isDisplayed) {
          parentNode.insertBefore(element, placeholder);
        }
        firstVisibleElementInserted = true;
      } else if (isDisplayed) {
        parentNode.removeChild(element);
      }
    });
  };
  updateVisibility();
  new Watcher(vm, () => elementsData.map(({ evaluateCondition }) => evaluateCondition()).join(','), updateVisibility);
}

function createConditionEvaluator(element, vm) {
  const vIf = element.getAttribute('v-if');
  const vElseIf = element.getAttribute('v-else-if');
  const expression = vIf || vElseIf || 'true';
  return getEvaluateExpressionFn(expression, vm);
}

function getEvaluateExpressionFn(expr, vm) {
  return new Function('with(this) { return ' + expr + ' }').bind(vm.$data);
}
function handleVModel(node, attr, vm) {
  const key = attr.value;
  node.value = vm[key];
  new Watcher(vm, key, function () {
    node.value = vm[key];
  });
  node.addEventListener('input', function () {
    vm[key] = node.value;
  });
}
function handleVText(node, attr, vm) {
  const expr = attr.value;
  new Watcher(vm, expr, newValue => {
    node.textContent = newValue;
  });
  node.textContent = vm[expr];
}

function handleVHtml(node, attr, vm) {
  const expr = attr.value;
  new Watcher(vm, expr, newValue => {
    node.innerHTML = newValue;
  });
  node.innerHTML = vm[expr];
}

function handleVBind(node, attr, vm) {
  const attrName = attr.name.slice(7);
  const key = attr.value;
  new Watcher(vm, key, newValue => {
    node.setAttribute(attrName, newValue);
  });
  node.setAttribute(attrName, vm[key]);
}

function handleVOn(node, attr, vm) {
  let [eventName, ...modifiers] = attr.name.slice(5).split('.');
  const methodExpression = attr.value.trim();
  let methodName = methodExpression;
  let args = [];

  const parenIndex = methodExpression.indexOf('(');
  if (parenIndex > -1) {
    methodName = methodExpression.substring(0, parenIndex);
    const argsString = methodExpression.substring(
      parenIndex + 1,
      methodExpression.length - 1
    ).trim();
    args = argsString.split(',').map(arg => arg.trim());
  }

  const options = {
    capture: modifiers.includes('capture'),
    once: modifiers.includes('once'),
    passive: modifiers.includes('passive')
  };

  node.addEventListener(eventName, function handler(event) {
    if (modifiers.includes('stop')) event.stopPropagation();
    if (modifiers.includes('prevent')) event.preventDefault();
    if (modifiers.includes('self') && event.target !== event.currentTarget) return;
    const keyCodes = {
      enter: 13,
      tab: 9,
      delete: [8, 46],
      esc: 27,
      space: 32,
      up: 38,
      down: 40,
      left: 37,
      right: 39
    };
    const keyCode = keyCodes[modifiers.find(mod => keyCodes.hasOwnProperty(mod))];
    if (keyCode) {
      if (Array.isArray(keyCode)) {
        if (!keyCode.includes(event.keyCode)) return;
      } else if (event.keyCode !== keyCode) {
        return;
      }
    }
    const actualArgs = args.map(arg => {
      if (arg === '$event') return event;
      if (!isNaN(arg)) return Number(arg);
      if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
      return vm[arg];
    });
    vm[methodName](...actualArgs);
    if (modifiers.includes('once')) {
      node.removeEventListener(eventName, handler);
    }
  }, options);
}

15.计算属性 #

在 Vue.js 中,计算属性(Computed Properties)是一种特殊类型的属性,可以用于处理复杂逻辑和计算。计算属性是基于依赖属性的值进行计算的,并且其结果会被缓存。这意味着,只有当依赖的属性值改变时,计算属性才会重新计算。

基础用法

下面是一个简单的例子,展示了如何使用计算属性:

<template>
  <div>
    <p>First name: {{ firstName }}</p>
    <p>Last name: {{ lastName }}</p>
    <p>Full name: {{ fullName }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe',
    };
  },
  computed: {
    fullName() {
      return this.firstName + ' ' + this.lastName;
    },
  },
};
</script>

在这个例子中,我们定义了一个计算属性 fullName,它依赖于 firstNamelastName。当 firstNamelastName 改变时,fullName 会自动重新计算。

缓存 vs 方法

你可能会问,为什么不直接使用方法来实现相同的功能?实际上,你确实可以使用方法来做这件事,但计算属性有缓存机制。这意味着,如果依赖的数据没有发生变化,计算属性不会重新计算,而是直接返回之前缓存的结果。这在处理复杂计算或大数据集时会更高效。

Getter 和 Setter

计算属性默认只有 getter ,但你也可以提供一个 setter:

<template>
  <div>
    <p>Full name: {{ fullName }}</p>
    <button @click="changeName">Change Name</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe',
    };
  },
  computed: {
    fullName: {
      get() {
        return this.firstName + ' ' + this.lastName;
      },
      set(newValue) {
        const names = newValue.split(' ');
        this.firstName = names[0];
        this.lastName = names[1];
      },
    },
  },
  methods: {
    changeName() {
      this.fullName = 'Jane Doe';
    },
  },
};
</script>

在这个例子中,我们添加了一个 setter ,当计算属性 fullName 被设置一个新值时,它会自动地更新 firstNamelastName

使用计算属性解决问题

计算属性非常适用于下列情况:

15.1 computed.html #

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue</title>
</head>

<body>
    <div id="app">
        <input v-model="firstName">
        <input v-model="lastName">
        <p>你的全名是: {{ fullName }}</p>
    </div>
    <script src="my-vue.js"></script>
    <script>
        new Vue({
            el: '#app',
            data: {
                firstName: 'A',
                lastName: 'B'
            },
            computed: {
                fullName: function () {
                    return this.firstName + ' ' + this.lastName
                }
            }
        })
    </script>
</body>
</html>

15.2 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const KeyMap = {
  enter: 'Enter',
  delete: ['Backspace', 'Delete'],
  up: 'ArrowUp',
  down: 'ArrowDown',
  left: 'ArrowLeft',
  right: 'ArrowRight',
  space: 'Space'
};
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
class Watcher {
  constructor(vm, exprOrFn, cb) {
    this.vm = vm;
    this.cb = cb;
    this.getter = typeof exprOrFn === 'function' ? exprOrFn : getEvaluate(exprOrFn, vm);
    Dep.target = this;
    this.oldValue = this.getter();
    Dep.target = null;
  }
  update() {
    const newValue = this.getter();
    if (this.oldValue !== newValue) {
      this.cb(this.oldValue, newValue);
      this.oldValue = newValue;
    }
  }
}
class Vue {
  constructor(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
    this.init();
  }
  init() {
    this.proxyData();
    this.proxyMethods();
+   this.initComputed(); 
    this.observe(this.$data);
    this.compile(this.$el);
  }
+ initComputed() {
+     const computed = this.$options.computed;
+     if (computed) {
+         Object.keys(computed).forEach(key => {
+             new Watcher(
+                 this,
+                 () => computed[key].call(this),
+                 (newValue) => {
+                     this[key] = newValue;
+                 }
+             );
+             Object.defineProperty(this, key, {
+                 get: () => {
+                     return computed[key].call(this);
+                 },
+                 set: () => {
+                     console.warn('Computed property "' + key + '" cannot be assigned to');
+                 },
+             });
+         });
+     }
+ }
  observe(obj) {
    for (let key in obj) {
      let value = obj[key];
      let dep = new Dep();
      Object.defineProperty(obj, key, {
        get() {
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return value;
        },
        set(newVal) {
          if (newVal !== value) {
            value = newVal;
            dep.notify();
          }
        }
      });
    }
  }
  proxyMethods() {
    for (let key in this.$methods) {
      this[key] = this.$methods[key].bind(this);
    }
  }
  proxyData() {
    for (let key in this.$data) {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newVal) {
          this.$data[key] = newVal;
        }
      });
    }
  }
  compile(el, isOnce = false, scope) {
    const vm = scope || this;
    const {
      childNodes
    } = el;
    const conditionalElements = [];
    [...childNodes].forEach(node => {
      console.log(node);
      if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
        return;
      }
      if (node.nodeType === ELEMENT_NODE) {
        if (node.hasAttribute('v-if') || node.hasAttribute('v-else-if') || node.hasAttribute('v-else')) {
          conditionalElements.push(node);
        }
        isOnce = isOnce || node.hasAttribute('v-once');
        compileAttributes([...node.attributes], node, vm, isOnce);
        if (el.contains(node)) {
          vm.compile(node, isOnce);
        }
      } else if (node.nodeType === TEXT_NODE) {
        let {
          originalTextContent
        } = node;
        if (!originalTextContent) {
          originalTextContent = node.textContent;
          node.originalTextContent = originalTextContent;
        }
        let reg = /\{\{(.+?)\}\}/g;
        const updateTextContent = () => {
          node.textContent = originalTextContent.replace(reg, (_, key) => {
            return evaluate(key, vm);
          });
        };
        updateTextContent();
        if (!isOnce) {
          let match;
          while ((match = reg.exec(originalTextContent)) !== null) {
            const key = match[1].trim();
            new Watcher(vm, key, updateTextContent);
          }
        }
      }
    });
    if (conditionalElements.length > 0) {
      handleConditionalElements(conditionalElements, vm);
    }
    el.removeAttribute('v-cloak');
  }
}
function handleConditionalElements(conditionalElements, vm) {
  let lastVisibleElement = null;
  conditionalElements.forEach(element => {
    let expression = element.getAttribute('v-if') || element.getAttribute('v-else-if') || 'true';
    let evaluate = getEvaluate(expression, vm);
    element.evaluate = evaluate;
    const shouldDisplay = evaluate();
    if (shouldDisplay && !lastVisibleElement) {
      lastVisibleElement = element;
    } else {
      element.parentNode.removeChild(element);
    }
  });
  const updateVisibility = () => {
    const nextVisibleElement = conditionalElements.find(({
      evaluate
    }) => {
      return evaluate();
    });
    if (nextVisibleElement && nextVisibleElement !== lastVisibleElement) {
      lastVisibleElement.parentNode.replaceChild(nextVisibleElement, lastVisibleElement);
      lastVisibleElement = nextVisibleElement;
    }
  };
  new Watcher(vm, () => conditionalElements.map(({
    evaluate
  }) => evaluate()), updateVisibility);
}
function evaluate(expression, vm) {
  return new Function(`with(this){return ` + expression + `}`).call(vm);
}
function getEvaluate(expression, vm) {
  return new Function(`with(this){return ` + expression + `}`).bind(vm);
}
function compileAttributes(attributes, node, vm, isOnce) {
  attributes.forEach(attr => {
    if (attr.name === 'v-text') {
      handleVText(node, attr, vm, isOnce);
    } else if (attr.name === 'v-html') {
      handleVHtml(node, attr, vm, isOnce);
    } else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
      handleVBind(node, attr, vm, isOnce);
    } else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
      handleEvent(node, attr, vm);
    } else if (attr.name === 'v-model') {
      handleVModel(node, attr, vm, isOnce);
    } else if (attr.name === 'v-show') {
      handleVShow(node, attr, vm, isOnce);
    } else if (attr.name === 'v-for') {
      handleVFor(node, attr, vm, isOnce);
    }
  });
}
function handleVFor(node, attr, vm, isOnce) {
  const originNode = node.cloneNode(true);
  const {
    parentNode
  } = node;
  const placeholder = document.createComment('v-for-placeholder');
  parentNode.replaceChild(placeholder, node);
  const expression = attr.value;
  const [itemVar,, listKey] = expression.split(' ');
  const update = () => {
    while (placeholder.nextSibling) {
      parentNode.removeChild(placeholder.nextSibling);
    }
    evaluate(listKey, vm).forEach(itemData => {
      const clonedNode = originNode.cloneNode(true);
      parentNode.appendChild(clonedNode);
      const newScope = Object.create(vm);
      newScope[itemVar] = itemData;
      vm.compile(clonedNode, isOnce, newScope);
    });
  };
  update();
  new Watcher(vm, listKey, update);
}
function handleVShow(node, attr, vm, isOnce) {
  const expr = attr.value;
  const originalDisplay = node.style.display;
  const update = () => {
    node.style.display = evaluate(expr, vm) ? originalDisplay : 'none';
  };
  update();
  if (!isOnce) {
    new Watcher(vm, expr, update);
  }
}
function handleVModel(node, attr, vm, isOnce) {
  const key = attr.value;
  node.value = vm[key];
  node.addEventListener('input', () => {
    vm[key] = node.value;
  });
  if (!isOnce) {
    new Watcher(vm, key, () => {
      node.value = vm[key];
    });
  }
}
function handleVText(node, attr, vm, isOnce) {
  const expr = attr.value;
  node.textContent = evaluate(expr, vm);
  if (!isOnce) {
    new Watcher(vm, expr, (oldValue, newValue) => {
      node.textContent = newValue;
    });
  }
}
function handleVHtml(node, attr, vm, isOnce) {
  const expr = attr.value;
  node.innerHTML = evaluate(expr, vm);
  if (!isOnce) {
    new Watcher(vm, expr, () => {
      node.innerHTML = evaluate(expr, vm);
    });
  }
}
function handleVBind(node, attr, vm, isOnce) {
  let attrName = attr.name;
  if (attrName.startsWith(':')) {
    attrName = `v-bind${attrName}`;
  }
  attrName = attrName.slice(7);
  const key = attr.value;
  node.setAttribute(attrName, vm[key]);
  if (!isOnce) {
    new Watcher(vm, key, () => {
      node.setAttribute(attrName, vm[key]);
    });
  }
}
function handleEvent(node, attr, vm) {
  let attrName = attr.name;
  if (attrName.startsWith('@')) {
    attrName = `v-on:${attrName.slice(1)}`;
  }
  const [eventName, ...modifiers] = attrName.slice(5).split('.');
  const methodExpression = attr.value.trim();
  let methodName = methodExpression;
  let args = [];
  let parenIndex = methodExpression.indexOf('(');
  if (parenIndex !== -1) {
    methodName = methodExpression.substring(0, parenIndex);
    const argsString = methodExpression.substring(parenIndex + 1, methodExpression.length - 1).trim();
    args = argsString.split(',').map(arg => arg.trim());
  } else {
    args = ['$event'];
  }
  let options = {
    capture: modifiers.includes('capture'),
    once: modifiers.includes('once'),
    passive: modifiers.includes('passive')
  };
  node.addEventListener(eventName, event => {
    if (modifiers.includes('stop')) event.stopPropagation();
    if (modifiers.includes('prevent')) event.preventDefault();
    if (modifiers.includes('self')) {
      if (event.target !== event.currentTarget) {
        return;
      }
    }
    let modifierKeys = modifiers.map(modifier => KeyMap[modifier]).filter(Boolean).flat();
    if (modifierKeys.length > 0) {
      if (!modifierKeys.includes(event.key)) {
        return;
      }
    }
    let actualArgs = args.map(arg => {
      if (!isNaN(arg)) return arg;
      if (arg === '$event') return event;
      if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
      return vm[arg];
    });
    vm[methodName](...actualArgs);
  }, options);
}

16.watch #

在 Vue.js 中,watch 选项提供了一种机制来观察和响应 Vue 实例上的数据变动。当某个数据属性发生变化时,watch 会触发一个函数,该函数用于处理这种变化。这样,你可以以声明式的方式进行数据变化的侧效应处理。

下面是一些使用 watch 的基础示例:

基础用法

new Vue({
  data: {
    message: 'Hello, Vue!'
  },
  watch: {
    message: function (newVal, oldVal) {
      console.log('新值:', newVal, '旧值:', oldVal);
    }
  }
});

在这个例子中,当 message 的值发生变化时,watch 会触发一个函数,该函数接收新值和旧值作为参数。

深度观察

对于对象或数组,你可能需要深度观察。这样做可以通过设置 deeptrue

new Vue({
  data: {
    obj: {
      key: 'value'
    }
  },
  watch: {
    obj: {
      handler: function (newVal, oldVal) {
        console.log('对象改变');
      },
      deep: true
    }
  }
});

这样,即使 obj.key 发生变化,watch 也会触发。

立即执行

有时候你可能需要在页面加载时立即执行一次观察函数,可以设置 immediate: true

new Vue({
  data: {
    message: 'Hello, Vue!'
  },
  watch: {
    message: {
      handler: function (newVal, oldVal) {
        console.log('值改变');
      },
      immediate: true
    }
  }
});

清理与取消观察

你还可以通过 watch 返回的对象,调用其 unwatch 方法来停止观察:

const vm = new Vue({
  data: {
    message: 'Hello, Vue!'
  },
  watch: {
    message: function (newVal, oldVal) {
      console.log('值改变');
    }
  }
});

const unwatch = vm.$watch('message', function (newVal, oldVal) {
  console.log('手动观察');
});

// 取消观察
unwatch();

这样可以使你有更多的灵活性来控制何时开始和停止观察。

watch 主要用于执行异步操作或较大的开销操作,而不是在数据变化时直接更新 DOM。对于后者,计算属性(computed)通常是更好的选择。

16.1 watch.html #

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue</title>
</head>
<body>
    <div id="app">
        {{message}}
    </div>
    <script src="my-vue.js"></script>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                message: 'hello'
            },
            watch: {
                message(newVal, oldVal) {
                    console.log('值改变',newVal, oldVal);
                }
            }
        })
        vm.message='world';
    </script>
</body>

</html>

16.2 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const KeyMap = {
  enter: 'Enter',
  delete: ['Backspace', 'Delete'],
  up: 'ArrowUp',
  down: 'ArrowDown',
  left: 'ArrowLeft',
  right: 'ArrowRight',
  space: 'Space'
};
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
class Watcher {
  constructor(vm, exprOrFn, cb) {
    this.vm = vm;
    this.cb = cb;
    this.getter = typeof exprOrFn === 'function' ? exprOrFn : getEvaluate(exprOrFn, vm);
    Dep.target = this;
    this.oldValue = this.getter();
    Dep.target = null;
  }
  update() {
    const newValue = this.getter();
    if (this.oldValue !== newValue) {
+     this.cb(newValue, this.oldValue);
      this.oldValue = newValue;
    }
  }
}
class Vue {
  constructor(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
    this.init();
  }
  init() {
    this.proxyData();
    this.proxyMethods();
    this.observe(this.$data);
    this.initComputed(); 
+   this.initWatch();
    this.compile(this.$el);
  }
+ initWatch() {
+   const watch = this.$options.watch;
+   if (watch) {
+       Object.keys(watch).forEach(key => {
+           new Watcher(
+               this,
+               key,
+               (newValue, oldValue) => {
+                   watch[key].call(this, newValue, oldValue);
+               }
+           );
+       });
+   }
+ }
  initComputed() {
      const computed = this.$options.computed;
      if (computed) {
          Object.keys(computed).forEach(key => {
              new Watcher(
                  this,
                  () => computed[key].call(this),
                  (newValue) => {
                      this[key] = newValue;
                  }
              );
              Object.defineProperty(this, key, {
                  get: () => {
                      return computed[key].call(this);
                  },
                  set: () => {
                      console.warn('Computed property "' + key + '" cannot be assigned to');
                  },
              });
          });
      }
  }
  observe(obj) {
    for (let key in obj) {
      let value = obj[key];
      let dep = new Dep();
      Object.defineProperty(obj, key, {
        get() {
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return value;
        },
        set(newVal) {
          if (newVal !== value) {
            value = newVal;
            dep.notify();
          }
        }
      });
    }
  }
  proxyMethods() {
    for (let key in this.$methods) {
      this[key] = this.$methods[key].bind(this);
    }
  }
  proxyData() {
    for (let key in this.$data) {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newVal) {
          this.$data[key] = newVal;
        }
      });
    }
  }
  compile(el, isOnce = false, scope) {
    const vm = scope || this;
    const {
      childNodes
    } = el;
    const conditionalElements = [];
    [...childNodes].forEach(node => {
      console.log(node);
      if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
        return;
      }
      if (node.nodeType === ELEMENT_NODE) {
        if (node.hasAttribute('v-if') || node.hasAttribute('v-else-if') || node.hasAttribute('v-else')) {
          conditionalElements.push(node);
        }
        isOnce = isOnce || node.hasAttribute('v-once');
        compileAttributes([...node.attributes], node, vm, isOnce);
        if (el.contains(node)) {
          vm.compile(node, isOnce);
        }
      } else if (node.nodeType === TEXT_NODE) {
        let {
          originalTextContent
        } = node;
        if (!originalTextContent) {
          originalTextContent = node.textContent;
          node.originalTextContent = originalTextContent;
        }
        let reg = /\{\{(.+?)\}\}/g;
        const updateTextContent = () => {
          node.textContent = originalTextContent.replace(reg, (_, key) => {
            return evaluate(key, vm);
          });
        };
        updateTextContent();
        if (!isOnce) {
          let match;
          while ((match = reg.exec(originalTextContent)) !== null) {
            const key = match[1].trim();
            new Watcher(vm, key, updateTextContent);
          }
        }
      }
    });
    if (conditionalElements.length > 0) {
      handleConditionalElements(conditionalElements, vm);
    }
    el.removeAttribute('v-cloak');
  }
}
function handleConditionalElements(conditionalElements, vm) {
  let lastVisibleElement = null;
  conditionalElements.forEach(element => {
    let expression = element.getAttribute('v-if') || element.getAttribute('v-else-if') || 'true';
    let evaluate = getEvaluate(expression, vm);
    element.evaluate = evaluate;
    const shouldDisplay = evaluate();
    if (shouldDisplay && !lastVisibleElement) {
      lastVisibleElement = element;
    } else {
      element.parentNode.removeChild(element);
    }
  });
  const updateVisibility = () => {
    const nextVisibleElement = conditionalElements.find(({
      evaluate
    }) => {
      return evaluate();
    });
    if (nextVisibleElement && nextVisibleElement !== lastVisibleElement) {
      lastVisibleElement.parentNode.replaceChild(nextVisibleElement, lastVisibleElement);
      lastVisibleElement = nextVisibleElement;
    }
  };
  new Watcher(vm, () => conditionalElements.map(({
    evaluate
  }) => evaluate()), updateVisibility);
}
function evaluate(expression, vm) {
  return new Function(`with(this){return ` + expression + `}`).call(vm);
}
function getEvaluate(expression, vm) {
  return new Function(`with(this){return ` + expression + `}`).bind(vm);
}
function compileAttributes(attributes, node, vm, isOnce) {
  attributes.forEach(attr => {
    if (attr.name === 'v-text') {
      handleVText(node, attr, vm, isOnce);
    } else if (attr.name === 'v-html') {
      handleVHtml(node, attr, vm, isOnce);
    } else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
      handleVBind(node, attr, vm, isOnce);
    } else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
      handleEvent(node, attr, vm);
    } else if (attr.name === 'v-model') {
      handleVModel(node, attr, vm, isOnce);
    } else if (attr.name === 'v-show') {
      handleVShow(node, attr, vm, isOnce);
    } else if (attr.name === 'v-for') {
      handleVFor(node, attr, vm, isOnce);
    }
  });
}
function handleVFor(node, attr, vm, isOnce) {
  const originNode = node.cloneNode(true);
  const {
    parentNode
  } = node;
  const placeholder = document.createComment('v-for-placeholder');
  parentNode.replaceChild(placeholder, node);
  const expression = attr.value;
  const [itemVar,, listKey] = expression.split(' ');
  const update = () => {
    while (placeholder.nextSibling) {
      parentNode.removeChild(placeholder.nextSibling);
    }
    evaluate(listKey, vm).forEach(itemData => {
      const clonedNode = originNode.cloneNode(true);
      parentNode.appendChild(clonedNode);
      const newScope = Object.create(vm);
      newScope[itemVar] = itemData;
      vm.compile(clonedNode, isOnce, newScope);
    });
  };
  update();
  new Watcher(vm, listKey, update);
}
function handleVShow(node, attr, vm, isOnce) {
  const expr = attr.value;
  const originalDisplay = node.style.display;
  const update = () => {
    node.style.display = evaluate(expr, vm) ? originalDisplay : 'none';
  };
  update();
  if (!isOnce) {
    new Watcher(vm, expr, update);
  }
}
function handleVModel(node, attr, vm, isOnce) {
  const key = attr.value;
  node.value = vm[key];
  node.addEventListener('input', () => {
    vm[key] = node.value;
  });
  if (!isOnce) {
    new Watcher(vm, key, () => {
      node.value = vm[key];
    });
  }
}
function handleVText(node, attr, vm, isOnce) {
  const expr = attr.value;
  node.textContent = evaluate(expr, vm);
  if (!isOnce) {
    new Watcher(vm, expr, (oldValue, newValue) => {
      node.textContent = newValue;
    });
  }
}
function handleVHtml(node, attr, vm, isOnce) {
  const expr = attr.value;
  node.innerHTML = evaluate(expr, vm);
  if (!isOnce) {
    new Watcher(vm, expr, () => {
      node.innerHTML = evaluate(expr, vm);
    });
  }
}
function handleVBind(node, attr, vm, isOnce) {
  let attrName = attr.name;
  if (attrName.startsWith(':')) {
    attrName = `v-bind${attrName}`;
  }
  attrName = attrName.slice(7);
  const key = attr.value;
  node.setAttribute(attrName, vm[key]);
  if (!isOnce) {
    new Watcher(vm, key, () => {
      node.setAttribute(attrName, vm[key]);
    });
  }
}
function handleEvent(node, attr, vm) {
  let attrName = attr.name;
  if (attrName.startsWith('@')) {
    attrName = `v-on:${attrName.slice(1)}`;
  }
  const [eventName, ...modifiers] = attrName.slice(5).split('.');
  const methodExpression = attr.value.trim();
  let methodName = methodExpression;
  let args = [];
  let parenIndex = methodExpression.indexOf('(');
  if (parenIndex !== -1) {
    methodName = methodExpression.substring(0, parenIndex);
    const argsString = methodExpression.substring(parenIndex + 1, methodExpression.length - 1).trim();
    args = argsString.split(',').map(arg => arg.trim());
  } else {
    args = ['$event'];
  }
  let options = {
    capture: modifiers.includes('capture'),
    once: modifiers.includes('once'),
    passive: modifiers.includes('passive')
  };
  node.addEventListener(eventName, event => {
    if (modifiers.includes('stop')) event.stopPropagation();
    if (modifiers.includes('prevent')) event.preventDefault();
    if (modifiers.includes('self')) {
      if (event.target !== event.currentTarget) {
        return;
      }
    }
    let modifierKeys = modifiers.map(modifier => KeyMap[modifier]).filter(Boolean).flat();
    if (modifierKeys.length > 0) {
      if (!modifierKeys.includes(event.key)) {
        return;
      }
    }
    let actualArgs = args.map(arg => {
      if (!isNaN(arg)) return arg;
      if (arg === '$event') return event;
      if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
      return vm[arg];
    });
    vm[methodName](...actualArgs);
  }, options);
}

17.filter #

在 Vue.js 中,过滤器(filter)用于应用一些文本转换和格式化。过滤器可以在两个地方使用:双花括号插值和 v-bind 表达式。

过滤器并不改变原数据,它们仅仅改变数据的输出格式。

基础用法

定义一个全局过滤器

Vue.filter('capitalize', function(value) {
  if (!value) return '';
  value = value.toString();
  return value.charAt(0).toUpperCase() + value.slice(1);
});

在组件中定义局部过滤器

export default {
  filters: {
    capitalize: function(value) {
      if (!value) return '';
      value = value.toString();
      return value.charAt(0).toUpperCase() + value.slice(1);
    }
  }
}

在模板中使用过滤器

<!-- 在双花括号中 -->
{{ message | capitalize }}

<!-- 在 `v-bind` 中 -->
<a :href="url | formatUrl"></a>

过滤器可以链式调用

{{ message | filterA | filterB }}

在这种情况下,filterA 会首先应用,然后其结果将传递给 filterB

过滤器可以接受参数

Vue.filter('prefix', function(value, prefix) {
  if (!value) return '';
  value = value.toString();
  return prefix + value;
});
{{ message | prefix('Hello: ') }}

在这个例子中,过滤器 prefix 接受一个参数 'Hello: ',然后将它添加到 message 前面。

总结

过滤器是 Vue.js 中用于文本格式化的一种有效工具,但应当注意的是,它们不应用于复杂的逻辑或副作用,这些应当由计算属性或方法处理。过滤器主要用于格式化输出,使之更易读或更符合要求。

17.1 filter.html #

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue</title>
</head>
<body>
    <div id="app">
        {{message | upper | addSuffix }}
    </div>
    <script src="my-vue.js"></script>
    <script>
        Vue.filter('upper', function (value) {
            return value.toUpperCase();
        });
        Vue.filter('addSuffix', function (value) {
            return value+'$';
        });
        var vm = new Vue({
            el: '#app',
            data: {
                message: 'hello'
            }
        })
    </script>
</body>
</html>

17.2 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const KeyMap = {
  enter: 'Enter',
  delete: ['Backspace', 'Delete'],
  up: 'ArrowUp',
  down: 'ArrowDown',
  left: 'ArrowLeft',
  right: 'ArrowRight',
  space: 'Space'
};
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
class Watcher {
  constructor(vm, exprOrFn, cb) {
    this.vm = vm;
    this.cb = cb;
    this.getter = typeof exprOrFn === 'function' ? exprOrFn : getEvaluate(exprOrFn, vm);
    Dep.target = this;
    this.oldValue = this.getter();
    Dep.target = null;
  }
  update() {
    const newValue = this.getter();
    if (this.oldValue !== newValue) {
      this.cb(newValue, this.oldValue);
      this.oldValue = newValue;
    }
  }
}

class Vue {
+ static filters = {}
+ static filter = function (name, func) {
+   Vue.filters[name] = func;
+ };
  constructor(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
    this.init();
  }
  init() {
    this.proxyData();
    this.proxyMethods();
    this.observe(this.$data);
    this.initComputed(); 
    this.initWatch();
    this.compile(this.$el);
  }
  initWatch() {
    const watch = this.$options.watch;
    if (watch) {
        Object.keys(watch).forEach(key => {
            new Watcher(
                this,
                key,
                (newValue, oldValue) => {
                    watch[key].call(this, newValue, oldValue);
                }
            );
        });
    }
  }
  initComputed() {
      const computed = this.$options.computed;
      if (computed) {
          Object.keys(computed).forEach(key => {
              new Watcher(
                  this,
                  () => computed[key].call(this),
                  (newValue) => {
                      this[key] = newValue;
                  }
              );
              Object.defineProperty(this, key, {
                  get: () => {
                      return computed[key].call(this);
                  },
                  set: () => {
                      console.warn('Computed property "' + key + '" cannot be assigned to');
                  },
              });
          });
      }
  }
  observe(obj) {
    for (let key in obj) {
      let value = obj[key];
      let dep = new Dep();
      Object.defineProperty(obj, key, {
        get() {
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return value;
        },
        set(newVal) {
          if (newVal !== value) {
            value = newVal;
            dep.notify();
          }
        }
      });
    }
  }
  proxyMethods() {
    for (let key in this.$methods) {
      this[key] = this.$methods[key].bind(this);
    }
  }
  proxyData() {
    for (let key in this.$data) {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newVal) {
          this.$data[key] = newVal;
        }
      });
    }
  }
  compile(el, isOnce = false, scope) {
    const vm = scope || this;
    const {
      childNodes
    } = el;
    const conditionalElements = [];
    [...childNodes].forEach(node => {
      console.log(node);
      if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
        return;
      }
      if (node.nodeType === ELEMENT_NODE) {
        if (node.hasAttribute('v-if') || node.hasAttribute('v-else-if') || node.hasAttribute('v-else')) {
          conditionalElements.push(node);
        }
        isOnce = isOnce || node.hasAttribute('v-once');
        compileAttributes([...node.attributes], node, vm, isOnce);
        if (el.contains(node)) {
          vm.compile(node, isOnce);
        }
      } else if (node.nodeType === TEXT_NODE) {
        let {
          originalTextContent
        } = node;
        if (!originalTextContent) {
          originalTextContent = node.textContent;
          node.originalTextContent = originalTextContent;
        }
        let reg = /\{\{(.+?)\}\}/g;
        const updateTextContent = () => {
          node.textContent = originalTextContent.replace(reg, (_, key) => {
            return evaluate(key, vm);
          });
        };
        updateTextContent();
        if (!isOnce) {
          let match;
          while ((match = reg.exec(originalTextContent)) !== null) {
            const key = match[1].trim();
            new Watcher(vm, key, updateTextContent);
          }
        }
      }
    });
    if (conditionalElements.length > 0) {
      handleConditionalElements(conditionalElements, vm);
    }
    el.removeAttribute('v-cloak');
  }
}
function handleConditionalElements(conditionalElements, vm) {
  let lastVisibleElement = null;
  conditionalElements.forEach(element => {
    let expression = element.getAttribute('v-if') || element.getAttribute('v-else-if') || 'true';
    let evaluate = getEvaluate(expression, vm);
    element.evaluate = evaluate;
    const shouldDisplay = evaluate();
    if (shouldDisplay && !lastVisibleElement) {
      lastVisibleElement = element;
    } else {
      element.parentNode.removeChild(element);
    }
  });
  const updateVisibility = () => {
    const nextVisibleElement = conditionalElements.find(({
      evaluate
    }) => {
      return evaluate();
    });
    if (nextVisibleElement && nextVisibleElement !== lastVisibleElement) {
      lastVisibleElement.parentNode.replaceChild(nextVisibleElement, lastVisibleElement);
      lastVisibleElement = nextVisibleElement;
    }
  };
  new Watcher(vm, () => conditionalElements.map(({
    evaluate
  }) => evaluate()), updateVisibility);
}
function evaluate(expression, vm) {
  return getEvaluate(expression, vm).call(vm);
}
+function getEvaluate(expression, vm) {
+  return function() {
+    const [expr, ...filters] = expression.split('|').map(e => e.trim());
+    let value = new Function(`with(this){return ` + expr + `}`).call(vm);
+    for (const filter of filters) {
+      const filterFunc = Vue.filters[filter];
+      value = filterFunc(value);
+    }
+    return value;
+  }
+}
function compileAttributes(attributes, node, vm, isOnce) {
  attributes.forEach(attr => {
    if (attr.name === 'v-text') {
      handleVText(node, attr, vm, isOnce);
    } else if (attr.name === 'v-html') {
      handleVHtml(node, attr, vm, isOnce);
    } else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
      handleVBind(node, attr, vm, isOnce);
    } else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
      handleEvent(node, attr, vm);
    } else if (attr.name === 'v-model') {
      handleVModel(node, attr, vm, isOnce);
    } else if (attr.name === 'v-show') {
      handleVShow(node, attr, vm, isOnce);
    } else if (attr.name === 'v-for') {
      handleVFor(node, attr, vm, isOnce);
    }
  });
}
function handleVFor(node, attr, vm, isOnce) {
  const originNode = node.cloneNode(true);
  const {
    parentNode
  } = node;
  const placeholder = document.createComment('v-for-placeholder');
  parentNode.replaceChild(placeholder, node);
  const expression = attr.value;
  const [itemVar,, listKey] = expression.split(' ');
  const update = () => {
    while (placeholder.nextSibling) {
      parentNode.removeChild(placeholder.nextSibling);
    }
    evaluate(listKey, vm).forEach(itemData => {
      const clonedNode = originNode.cloneNode(true);
      parentNode.appendChild(clonedNode);
      const newScope = Object.create(vm);
      newScope[itemVar] = itemData;
      vm.compile(clonedNode, isOnce, newScope);
    });
  };
  update();
  new Watcher(vm, listKey, update);
}
function handleVShow(node, attr, vm, isOnce) {
  const expr = attr.value;
  const originalDisplay = node.style.display;
  const update = () => {
    node.style.display = evaluate(expr, vm) ? originalDisplay : 'none';
  };
  update();
  if (!isOnce) {
    new Watcher(vm, expr, update);
  }
}
function handleVModel(node, attr, vm, isOnce) {
  const key = attr.value;
  node.value = vm[key];
  node.addEventListener('input', () => {
    vm[key] = node.value;
  });
  if (!isOnce) {
    new Watcher(vm, key, () => {
      node.value = vm[key];
    });
  }
}
function handleVText(node, attr, vm, isOnce) {
  const expr = attr.value;
  node.textContent = evaluate(expr, vm);
  if (!isOnce) {
    new Watcher(vm, expr, (oldValue, newValue) => {
      node.textContent = newValue;
    });
  }
}
function handleVHtml(node, attr, vm, isOnce) {
  const expr = attr.value;
  node.innerHTML = evaluate(expr, vm);
  if (!isOnce) {
    new Watcher(vm, expr, () => {
      node.innerHTML = evaluate(expr, vm);
    });
  }
}
function handleVBind(node, attr, vm, isOnce) {
  let attrName = attr.name;
  if (attrName.startsWith(':')) {
    attrName = `v-bind${attrName}`;
  }
  attrName = attrName.slice(7);
  const key = attr.value;
  node.setAttribute(attrName, vm[key]);
  if (!isOnce) {
    new Watcher(vm, key, () => {
      node.setAttribute(attrName, vm[key]);
    });
  }
}
function handleEvent(node, attr, vm) {
  let attrName = attr.name;
  if (attrName.startsWith('@')) {
    attrName = `v-on:${attrName.slice(1)}`;
  }
  const [eventName, ...modifiers] = attrName.slice(5).split('.');
  const methodExpression = attr.value.trim();
  let methodName = methodExpression;
  let args = [];
  let parenIndex = methodExpression.indexOf('(');
  if (parenIndex !== -1) {
    methodName = methodExpression.substring(0, parenIndex);
    const argsString = methodExpression.substring(parenIndex + 1, methodExpression.length - 1).trim();
    args = argsString.split(',').map(arg => arg.trim());
  } else {
    args = ['$event'];
  }
  let options = {
    capture: modifiers.includes('capture'),
    once: modifiers.includes('once'),
    passive: modifiers.includes('passive')
  };
  node.addEventListener(eventName, event => {
    if (modifiers.includes('stop')) event.stopPropagation();
    if (modifiers.includes('prevent')) event.preventDefault();
    if (modifiers.includes('self')) {
      if (event.target !== event.currentTarget) {
        return;
      }
    }
    let modifierKeys = modifiers.map(modifier => KeyMap[modifier]).filter(Boolean).flat();
    if (modifierKeys.length > 0) {
      if (!modifierKeys.includes(event.key)) {
        return;
      }
    }
    let actualArgs = args.map(arg => {
      if (!isNaN(arg)) return arg;
      if (arg === '$event') return event;
      if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
      return vm[arg];
    });
    vm[methodName](...actualArgs);
  }, options);
}

18.:class #

在 Vue.js 中,:class 是一个特殊的绑定语法,允许你动态地将类添加到 DOM 元素上。这使得元素类名可以根据组件的数据或状态进行更改,从而实现响应式样式。

基础用法

最简单的用法是绑定一个字符串:

<!-- activeClass 是 data 或 computed 属性中定义的 -->
<div :class="activeClass">Hello, world!</div>
data() {
  return {
    activeClass: 'active'
  };
}

对象语法

你也可以传递一个对象来动态地切换类:

<!-- active 会评估为一个布尔值 -->
<div :class="{ active: isActive, 'text-danger': hasError }">Hello, world!</div>
data() {
  return {
    isActive: true,
    hasError: false
  };
}

在这个例子里,active 类会在 isActivetrue 的时候添加到 <div> 元素上,而 text-danger 类会在 hasErrortrue 的时候添加。

数组语法

:class 也可以接受一个数组,以应用多个类:

<div :class="[activeClass, errorClass]">Hello, world!</div>
data() {
  return {
    activeClass: 'active',
    errorClass: 'text-danger'
  };
}

数组内部还可以使用对象语法:

<div :class="[isActive ? activeClass : '', { 'text-danger': hasError }]">Hello, world!</div>

使用计算属性

如果类逻辑比较复杂,你可以使用计算属性:

<div :class="classObject">Hello, world!</div>
computed: {
  classObject() {
    return {
      active: this.isActive,
      'text-danger': this.hasError
    };
  }
}

在这个例子中,classObject 是一个计算属性,返回一个根据组件状态动态改变的对象。

19.:style #

在 Vue.js 中,:style 是一个用于动态地绑定样式的特殊 attribute。使用这个 attribute 可以让你以数据绑定的方式动态地设置 HTML 元素的样式。

:style 可以接受一个对象或者数组作为其值,该对象或数组则用于定义样式规则。

对象语法

当使用对象语法时,你需要传递一个对象,其中的键名对应于 CSS 属性名,键值对应于 CSS 属性值。例如:

<template>
  <div :style="styleObject"></div>
</template>

<script>
export default {
  data() {
    return {
      styleObject: {
        color: 'red',
        fontSize: '16px',
      },
    };
  },
};
</script>

在这个例子中,div 元素的文字颜色会被设置为红色,并且字体大小会被设置为 16 像素。

数组语法

当使用数组语法时,你可以传递一个包含多个样式对象的数组,这在你想要根据不同的条件应用多个样式对象时非常有用。例如:

<template>
  <div :style="[baseStyles, overridingStyles]"></div>
</template>

<script>
export default {
  data() {
    return {
      baseStyles: {
        color: 'red',
        fontSize: '16px',
      },
      overridingStyles: {
        color: 'blue',
      },
    };
  },
};
</script>

在这个例子中,div 元素的文字颜色最终会被设置为蓝色(因为 overridingStyles 覆盖了 baseStyles),并且字体大小会被设置为 16 像素。

使用计算属性

你也可以使用计算属性来动态生成样式对象:

<template>
  <div :style="computedStyles"></div>
</template>

<script>
export default {
  data() {
    return {
      isActive: true,
    };
  },
  computed: {
    computedStyles() {
      return {
        color: this.isActive ? 'green' : 'red',
        fontSize: this.isActive ? '20px' : '16px',
      };
    },
  },
};
</script>

这里,当 isActivetrue 时,文字颜色会是绿色并且字体大小为 20 像素;否则,它们会被设置为红色和 16 像素。

18.1 style.html #

<body>
    <div id="app">
        <div :style="styleObject">style</div>
    </div>
    <script src="my-vue.js"></script>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                styleObject: {
                    color: 'red',
                    fontSize: '16px',
                },
            }
        })
    </script>
</body>

18.2 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const KeyMap = {
  enter: 'Enter',
  delete: ['Backspace', 'Delete'],
  up: 'ArrowUp',
  down: 'ArrowDown',
  left: 'ArrowLeft',
  right: 'ArrowRight',
  space: 'Space'
};
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
class Watcher {
  constructor(vm, exprOrFn, cb) {
    this.vm = vm;
    this.cb = cb;
    this.getter = typeof exprOrFn === 'function' ? exprOrFn : getEvaluate(exprOrFn, vm);
    Dep.target = this;
    this.oldValue = this.getter();
    Dep.target = null;
  }
  update() {
    const newValue = this.getter();
    if (this.oldValue !== newValue) {
      this.cb(newValue, this.oldValue);
      this.oldValue = newValue;
    }
  }
}

class Vue {
  static filters = {}
  static filter = function (name, func) {
    Vue.filters[name] = func;
  };
  constructor(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
    this.init();
  }
  init() {
    this.proxyData();
    this.proxyMethods();
    this.observe(this.$data);
    this.initComputed(); 
    this.initWatch();
    this.compile(this.$el);
  }
  initWatch() {
    const watch = this.$options.watch;
    if (watch) {
        Object.keys(watch).forEach(key => {
            new Watcher(
                this,
                key,
                (newValue, oldValue) => {
                    watch[key].call(this, newValue, oldValue);
                }
            );
        });
    }
  }
  initComputed() {
      const computed = this.$options.computed;
      if (computed) {
          Object.keys(computed).forEach(key => {
              new Watcher(
                  this,
                  () => computed[key].call(this),
                  (newValue) => {
                      this[key] = newValue;
                  }
              );
              Object.defineProperty(this, key, {
                  get: () => {
                      return computed[key].call(this);
                  },
                  set: () => {
                      console.warn('Computed property "' + key + '" cannot be assigned to');
                  },
              });
          });
      }
  }
  observe(obj) {
    for (let key in obj) {
      let value = obj[key];
      let dep = new Dep();
      Object.defineProperty(obj, key, {
        get() {
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return value;
        },
        set(newVal) {
          if (newVal !== value) {
            value = newVal;
            dep.notify();
          }
        }
      });
    }
  }
  proxyMethods() {
    for (let key in this.$methods) {
      this[key] = this.$methods[key].bind(this);
    }
  }
  proxyData() {
    for (let key in this.$data) {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newVal) {
          this.$data[key] = newVal;
        }
      });
    }
  }
  compile(el, isOnce = false, scope) {
    const vm = scope || this;
    const {
      childNodes
    } = el;
    const conditionalElements = [];
    [...childNodes].forEach(node => {
      console.log(node);
      if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
        return;
      }
      if (node.nodeType === ELEMENT_NODE) {
        if (node.hasAttribute('v-if') || node.hasAttribute('v-else-if') || node.hasAttribute('v-else')) {
          conditionalElements.push(node);
        }
        isOnce = isOnce || node.hasAttribute('v-once');
        compileAttributes([...node.attributes], node, vm, isOnce);
        if (el.contains(node)) {
          vm.compile(node, isOnce);
        }
      } else if (node.nodeType === TEXT_NODE) {
        let {
          originalTextContent
        } = node;
        if (!originalTextContent) {
          originalTextContent = node.textContent;
          node.originalTextContent = originalTextContent;
        }
        let reg = /\{\{(.+?)\}\}/g;
        const updateTextContent = () => {
          node.textContent = originalTextContent.replace(reg, (_, key) => {
            return evaluate(key, vm);
          });
        };
        updateTextContent();
        if (!isOnce) {
          let match;
          while ((match = reg.exec(originalTextContent)) !== null) {
            const key = match[1].trim();
            new Watcher(vm, key, updateTextContent);
          }
        }
      }
    });
    if (conditionalElements.length > 0) {
      handleConditionalElements(conditionalElements, vm);
    }
    el.removeAttribute('v-cloak');
  }
}
function handleConditionalElements(conditionalElements, vm) {
  let lastVisibleElement = null;
  conditionalElements.forEach(element => {
    let expression = element.getAttribute('v-if') || element.getAttribute('v-else-if') || 'true';
    let evaluate = getEvaluate(expression, vm);
    element.evaluate = evaluate;
    const shouldDisplay = evaluate();
    if (shouldDisplay && !lastVisibleElement) {
      lastVisibleElement = element;
    } else {
      element.parentNode.removeChild(element);
    }
  });
  const updateVisibility = () => {
    const nextVisibleElement = conditionalElements.find(({
      evaluate
    }) => {
      return evaluate();
    });
    if (nextVisibleElement && nextVisibleElement !== lastVisibleElement) {
      lastVisibleElement.parentNode.replaceChild(nextVisibleElement, lastVisibleElement);
      lastVisibleElement = nextVisibleElement;
    }
  };
  new Watcher(vm, () => conditionalElements.map(({
    evaluate
  }) => evaluate()), updateVisibility);
}
function evaluate(expression, vm) {
  return getEvaluate(expression, vm).call(vm);
}
function getEvaluate(expression, vm) {
  return function() {
    const [expr, ...filters] = expression.split('|').map(e => e.trim());
    let value = new Function(`with(this){return ` + expr + `}`).call(vm);
    for (const filter of filters) {
      const filterFunc = Vue.filters[filter];
      value = filterFunc(value);
    }
    return value;
  }
}
function compileAttributes(attributes, node, vm, isOnce) {
  attributes.forEach(attr => {
    if (attr.name === 'v-text') {
      handleVText(node, attr, vm, isOnce);
    } else if (attr.name === 'v-html') {
      handleVHtml(node, attr, vm, isOnce);
+   } else if (attr.name === 'v-bind:style' || attr.name === ':style') {
+     handleStyleBinding(node, attr, vm);
    } else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
      handleVBind(node, attr, vm, isOnce);
    } else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
      handleEvent(node, attr, vm);
    } else if (attr.name === 'v-model') {
      handleVModel(node, attr, vm, isOnce);
    } else if (attr.name === 'v-show') {
      handleVShow(node, attr, vm, isOnce);
    } else if (attr.name === 'v-for') {
      handleVFor(node, attr, vm, isOnce);
    }
  });
}
+function handleStyleBinding(node, attr, vm) {
+  const updateStyle = () => {
+    const styleObject = evaluate(attr.value, vm);
+    for (const [key, value] of Object.entries(styleObject)) {
+      node.style[key] = value;
+    }
+  };
+  updateStyle();
+  new Watcher(vm, attr.value, updateStyle);
+}
function handleVFor(node, attr, vm, isOnce) {
  const originNode = node.cloneNode(true);
  const {
    parentNode
  } = node;
  const placeholder = document.createComment('v-for-placeholder');
  parentNode.replaceChild(placeholder, node);
  const expression = attr.value;
  const [itemVar,, listKey] = expression.split(' ');
  const update = () => {
    while (placeholder.nextSibling) {
      parentNode.removeChild(placeholder.nextSibling);
    }
    evaluate(listKey, vm).forEach(itemData => {
      const clonedNode = originNode.cloneNode(true);
      parentNode.appendChild(clonedNode);
      const newScope = Object.create(vm);
      newScope[itemVar] = itemData;
      vm.compile(clonedNode, isOnce, newScope);
    });
  };
  update();
  new Watcher(vm, listKey, update);
}
function handleVShow(node, attr, vm, isOnce) {
  const expr = attr.value;
  const originalDisplay = node.style.display;
  const update = () => {
    node.style.display = evaluate(expr, vm) ? originalDisplay : 'none';
  };
  update();
  if (!isOnce) {
    new Watcher(vm, expr, update);
  }
}
function handleVModel(node, attr, vm, isOnce) {
  const key = attr.value;
  node.value = vm[key];
  node.addEventListener('input', () => {
    vm[key] = node.value;
  });
  if (!isOnce) {
    new Watcher(vm, key, () => {
      node.value = vm[key];
    });
  }
}
function handleVText(node, attr, vm, isOnce) {
  const expr = attr.value;
  node.textContent = evaluate(expr, vm);
  if (!isOnce) {
    new Watcher(vm, expr, (oldValue, newValue) => {
      node.textContent = newValue;
    });
  }
}
function handleVHtml(node, attr, vm, isOnce) {
  const expr = attr.value;
  node.innerHTML = evaluate(expr, vm);
  if (!isOnce) {
    new Watcher(vm, expr, () => {
      node.innerHTML = evaluate(expr, vm);
    });
  }
}
function handleVBind(node, attr, vm, isOnce) {
  let attrName = attr.name;
  if (attrName.startsWith(':')) {
    attrName = `v-bind${attrName}`;
  }
  attrName = attrName.slice(7);
  const key = attr.value;
  node.setAttribute(attrName, vm[key]);
  if (!isOnce) {
    new Watcher(vm, key, () => {
      node.setAttribute(attrName, vm[key]);
    });
  }
}
function handleEvent(node, attr, vm) {
  let attrName = attr.name;
  if (attrName.startsWith('@')) {
    attrName = `v-on:${attrName.slice(1)}`;
  }
  const [eventName, ...modifiers] = attrName.slice(5).split('.');
  const methodExpression = attr.value.trim();
  let methodName = methodExpression;
  let args = [];
  let parenIndex = methodExpression.indexOf('(');
  if (parenIndex !== -1) {
    methodName = methodExpression.substring(0, parenIndex);
    const argsString = methodExpression.substring(parenIndex + 1, methodExpression.length - 1).trim();
    args = argsString.split(',').map(arg => arg.trim());
  } else {
    args = ['$event'];
  }
  let options = {
    capture: modifiers.includes('capture'),
    once: modifiers.includes('once'),
    passive: modifiers.includes('passive')
  };
  node.addEventListener(eventName, event => {
    if (modifiers.includes('stop')) event.stopPropagation();
    if (modifiers.includes('prevent')) event.preventDefault();
    if (modifiers.includes('self')) {
      if (event.target !== event.currentTarget) {
        return;
      }
    }
    let modifierKeys = modifiers.map(modifier => KeyMap[modifier]).filter(Boolean).flat();
    if (modifierKeys.length > 0) {
      if (!modifierKeys.includes(event.key)) {
        return;
      }
    }
    let actualArgs = args.map(arg => {
      if (!isNaN(arg)) return arg;
      if (arg === '$event') return event;
      if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
      return vm[arg];
    });
    vm[methodName](...actualArgs);
  }, options);
}

20.多层属性 #

20.1 object.html #

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <div id="app" v-cloak>
             {{user.name}}
        </div>
        <script src="my-vue.js"></script>
        <script>
        var vm = new Vue({
            el:'#app',
            data:{
              user:{
                name:'beijing'
              }
            }
        });
        vm.user.name='guangzhou'
    </script>
    </body>
</html>

20.2 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const KeyMap = {
  enter: 'Enter',
  delete: ['Backspace', 'Delete'],
  up: 'ArrowUp',
  down: 'ArrowDown',
  left: 'ArrowLeft',
  right: 'ArrowRight',
  space: 'Space'
};
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
class Watcher {
  constructor(vm, exprOrFn, cb) {
    this.vm = vm;
    this.cb = cb;
    this.getter = typeof exprOrFn === 'function' ? exprOrFn : getEvaluate(exprOrFn, vm);
    Dep.target = this;
    this.oldValue = this.getter();
    Dep.target = null;
  }
  update() {
    const newValue = this.getter();
    if (this.oldValue !== newValue) {
      this.cb(newValue, this.oldValue);
      this.oldValue = newValue;
    }
  }
}

class Vue {
  static filters = {}
  static filter = function (name, func) {
    Vue.filters[name] = func;
  };
  constructor(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
    this.init();
  }
  init() {
    this.proxyData();
    this.proxyMethods();
    this.observe(this.$data);
    this.initComputed(); 
    this.initWatch();
    this.compile(this.$el);
  }
  initWatch() {
    const watch = this.$options.watch;
    if (watch) {
        Object.keys(watch).forEach(key => {
            new Watcher(
                this,
                key,
                (newValue, oldValue) => {
                    watch[key].call(this, newValue, oldValue);
                }
            );
        });
    }
  }
  initComputed() {
      const computed = this.$options.computed;
      if (computed) {
          Object.keys(computed).forEach(key => {
              new Watcher(
                  this,
                  () => computed[key].call(this),
                  (newValue) => {
                      this[key] = newValue;
                  }
              );
              Object.defineProperty(this, key, {
                  get: () => {
                      return computed[key].call(this);
                  },
                  set: () => {
                      console.warn('Computed property "' + key + '" cannot be assigned to');
                  },
              });
          });
      }
  }
+ observe(obj) {
+   if (!obj || typeof obj !== 'object') {
+     return;
+   }
+   for (let key in obj) {
+     this.defineReactive(obj, key, obj[key]);
+   }
+ }
+ defineReactive(obj, key, value) {
+   this.observe(value);
+   let dep = new Dep();
+   Object.defineProperty(obj, key, {
+     get() {
+       if (Dep.target) {
+         dep.addSub(Dep.target);
+       }
+       return value;
+     },
+     set: (newVal) => {
+       if (newVal !== value) {
+         this.observe(newVal);
+         value = newVal;
+         dep.notify();
+       }
+     }
+   });
+ }
  proxyMethods() {
    for (let key in this.$methods) {
      this[key] = this.$methods[key].bind(this);
    }
  }
  proxyData() {
    for (let key in this.$data) {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newVal) {
          this.$data[key] = newVal;
        }
      });
    }
  }
  compile(el, isOnce = false, scope) {
    const vm = scope || this;
    const {
      childNodes
    } = el;
    const conditionalElements = [];
    [...childNodes].forEach(node => {
      console.log(node);
      if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
        return;
      }
      if (node.nodeType === ELEMENT_NODE) {
        if (node.hasAttribute('v-if') || node.hasAttribute('v-else-if') || node.hasAttribute('v-else')) {
          conditionalElements.push(node);
        }
        isOnce = isOnce || node.hasAttribute('v-once');
        compileAttributes([...node.attributes], node, vm, isOnce);
        if (el.contains(node)) {
          vm.compile(node, isOnce);
        }
      } else if (node.nodeType === TEXT_NODE) {
        let {
          originalTextContent
        } = node;
        if (!originalTextContent) {
          originalTextContent = node.textContent;
          node.originalTextContent = originalTextContent;
        }
        let reg = /\{\{(.+?)\}\}/g;
        const updateTextContent = () => {
          node.textContent = originalTextContent.replace(reg, (_, key) => {
            return evaluate(key, vm);
          });
        };
        updateTextContent();
        if (!isOnce) {
          let match;
          while ((match = reg.exec(originalTextContent)) !== null) {
            const key = match[1].trim();
            new Watcher(vm, key, updateTextContent);
          }
        }
      }
    });
    if (conditionalElements.length > 0) {
      handleConditionalElements(conditionalElements, vm);
    }
    el.removeAttribute('v-cloak');
  }
}
function handleConditionalElements(conditionalElements, vm) {
  let lastVisibleElement = null;
  conditionalElements.forEach(element => {
    let expression = element.getAttribute('v-if') || element.getAttribute('v-else-if') || 'true';
    let evaluate = getEvaluate(expression, vm);
    element.evaluate = evaluate;
    const shouldDisplay = evaluate();
    if (shouldDisplay && !lastVisibleElement) {
      lastVisibleElement = element;
    } else {
      element.parentNode.removeChild(element);
    }
  });
  const updateVisibility = () => {
    const nextVisibleElement = conditionalElements.find(({
      evaluate
    }) => {
      return evaluate();
    });
    if (nextVisibleElement && nextVisibleElement !== lastVisibleElement) {
      lastVisibleElement.parentNode.replaceChild(nextVisibleElement, lastVisibleElement);
      lastVisibleElement = nextVisibleElement;
    }
  };
  new Watcher(vm, () => conditionalElements.map(({
    evaluate
  }) => evaluate()), updateVisibility);
}
function evaluate(expression, vm) {
  return getEvaluate(expression, vm).call(vm);
}
function getEvaluate(expression, vm) {
  return function() {
    const [expr, ...filters] = expression.split('|').map(e => e.trim());
    let value = new Function(`with(this){return ` + expr + `}`).call(vm);
    for (const filter of filters) {
      const filterFunc = Vue.filters[filter];
      value = filterFunc(value);
    }
    return value;
  }
}
function compileAttributes(attributes, node, vm, isOnce) {
  attributes.forEach(attr => {
    if (attr.name === 'v-text') {
      handleVText(node, attr, vm, isOnce);
    } else if (attr.name === 'v-html') {
      handleVHtml(node, attr, vm, isOnce);
    } else if (attr.name === 'v-bind:style' || attr.name === ':style') {
      handleStyleBinding(node, attr, vm);
    } else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
      handleVBind(node, attr, vm, isOnce);
    } else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
      handleEvent(node, attr, vm);
    } else if (attr.name === 'v-model') {
      handleVModel(node, attr, vm, isOnce);
    } else if (attr.name === 'v-show') {
      handleVShow(node, attr, vm, isOnce);
    } else if (attr.name === 'v-for') {
      handleVFor(node, attr, vm, isOnce);
    }
  });
}
function handleStyleBinding(node, attr, vm) {
  const updateStyle = () => {
    const styleObject = evaluate(attr.value, vm);
    for (const [key, value] of Object.entries(styleObject)) {
      node.style[key] = value;
    }
  };
  updateStyle();
  new Watcher(vm, attr.value, updateStyle);
}
function handleVFor(node, attr, vm, isOnce) {
  const originNode = node.cloneNode(true);
  const {
    parentNode
  } = node;
  const placeholder = document.createComment('v-for-placeholder');
  parentNode.replaceChild(placeholder, node);
  const expression = attr.value;
  const [itemVar,, listKey] = expression.split(' ');
  const update = () => {
    while (placeholder.nextSibling) {
      parentNode.removeChild(placeholder.nextSibling);
    }
    evaluate(listKey, vm).forEach(itemData => {
      const clonedNode = originNode.cloneNode(true);
      parentNode.appendChild(clonedNode);
      const newScope = Object.create(vm);
      newScope[itemVar] = itemData;
      vm.compile(clonedNode, isOnce, newScope);
    });
  };
  update();
  new Watcher(vm, listKey, update);
}
function handleVShow(node, attr, vm, isOnce) {
  const expr = attr.value;
  const originalDisplay = node.style.display;
  const update = () => {
    node.style.display = evaluate(expr, vm) ? originalDisplay : 'none';
  };
  update();
  if (!isOnce) {
    new Watcher(vm, expr, update);
  }
}
function handleVModel(node, attr, vm, isOnce) {
  const key = attr.value;
  node.value = vm[key];
  node.addEventListener('input', () => {
    vm[key] = node.value;
  });
  if (!isOnce) {
    new Watcher(vm, key, () => {
      node.value = vm[key];
    });
  }
}
function handleVText(node, attr, vm, isOnce) {
  const expr = attr.value;
  node.textContent = evaluate(expr, vm);
  if (!isOnce) {
    new Watcher(vm, expr, (oldValue, newValue) => {
      node.textContent = newValue;
    });
  }
}
function handleVHtml(node, attr, vm, isOnce) {
  const expr = attr.value;
  node.innerHTML = evaluate(expr, vm);
  if (!isOnce) {
    new Watcher(vm, expr, () => {
      node.innerHTML = evaluate(expr, vm);
    });
  }
}
function handleVBind(node, attr, vm, isOnce) {
  let attrName = attr.name;
  if (attrName.startsWith(':')) {
    attrName = `v-bind${attrName}`;
  }
  attrName = attrName.slice(7);
  const key = attr.value;
  node.setAttribute(attrName, vm[key]);
  if (!isOnce) {
    new Watcher(vm, key, () => {
      node.setAttribute(attrName, vm[key]);
    });
  }
}
function handleEvent(node, attr, vm) {
  let attrName = attr.name;
  if (attrName.startsWith('@')) {
    attrName = `v-on:${attrName.slice(1)}`;
  }
  const [eventName, ...modifiers] = attrName.slice(5).split('.');
  const methodExpression = attr.value.trim();
  let methodName = methodExpression;
  let args = [];
  let parenIndex = methodExpression.indexOf('(');
  if (parenIndex !== -1) {
    methodName = methodExpression.substring(0, parenIndex);
    const argsString = methodExpression.substring(parenIndex + 1, methodExpression.length - 1).trim();
    args = argsString.split(',').map(arg => arg.trim());
  } else {
    args = ['$event'];
  }
  let options = {
    capture: modifiers.includes('capture'),
    once: modifiers.includes('once'),
    passive: modifiers.includes('passive')
  };
  node.addEventListener(eventName, event => {
    if (modifiers.includes('stop')) event.stopPropagation();
    if (modifiers.includes('prevent')) event.preventDefault();
    if (modifiers.includes('self')) {
      if (event.target !== event.currentTarget) {
        return;
      }
    }
    let modifierKeys = modifiers.map(modifier => KeyMap[modifier]).filter(Boolean).flat();
    if (modifierKeys.length > 0) {
      if (!modifierKeys.includes(event.key)) {
        return;
      }
    }
    let actualArgs = args.map(arg => {
      if (!isNaN(arg)) return arg;
      if (arg === '$event') return event;
      if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
      return vm[arg];
    });
    vm[methodName](...actualArgs);
  }, options);
}

21.数组劫持 #

21.1 array.html #

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <div id="app">
             <ul>
                <li v-for="user in users">{{user.name}}</li>
             </ul>
        </div>
        <script src="my-vue.js"></script>
        <script>
        var vm = new Vue({
            el:'#app',
            data:{
              users:[
                {id:1,name:'zhangsan'},
                {id:2,name:'lisi'}
              ]
            }
        });
        vm.users.push({id:3,name:'wangwu'});
        vm.users[0].name = 'zhaoliu'
        vm.users[1].name = 'chenqi'
    </script>
    </body>
</html>

21.2 my-vue.js #

const ELEMENT_NODE = 1;
const TEXT_NODE = 3;
const KeyMap = {
  enter: 'Enter',
  delete: ['Backspace', 'Delete'],
  up: 'ArrowUp',
  down: 'ArrowDown',
  left: 'ArrowLeft',
  right: 'ArrowRight',
  space: 'Space'
};
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
class Watcher {
  constructor(vm, exprOrFn, cb) {
    this.vm = vm;
    this.cb = cb;
    this.getter = typeof exprOrFn === 'function' ? exprOrFn : getEvaluate(exprOrFn, vm);
    Dep.target = this;
    this.oldValue = this.getter();
    Dep.target = null;
  }
  update() {
    const newValue = this.getter();
    this.cb(newValue, this.oldValue);
    this.oldValue = newValue;
  }
}
const arrayMethods = Object.create(Array.prototype);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
  const original = arrayMethods[method];
  Object.defineProperty(arrayMethods, method, {
    value: function mutator(...args) {
      const result = original.apply(this, args);
      this.__dep__.notify();
      return result;
    },
    writable: true,
    configurable: true
  });
});
class Vue {
  static filters = {};
  static filter = (filterName, filterFunc) => {
    Vue.filters[filterName] = filterFunc;
  };
  constructor(options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
    this.init();
  }
  init() {
    this.proxyData();
    this.observe(this.$data);
    this.proxyMethods();
    this.initComputed();
    this.initWatch();
    this.compile(this.$el);
  }
  initWatch() {
    const {
      watch
    } = this.$options;
    if (!watch) return;
    const keys = Object.keys(watch);
    keys.forEach(key => {
      let value = watch[key];
      new Watcher(this, key, value.bind(this));
    });
  }
  initComputed() {
    const {
      computed
    } = this.$options;
    if (!computed) return;
    const keys = Object.keys(computed);
    keys.forEach(key => {
      const value = computed[key];
      let getter = value;
      let setter = () => console.warn(`计算属性无法重新赋值!`);
      if (typeof value === 'object') {
        getter = value.get;
        setter = value.set;
      }
      Object.defineProperty(this, key, {
        get: () => {
          return getter.call(this);
        },
        set: newValue => {
          setter.call(this, newValue);
        }
      });
    });
  }
+ observe(value) {
+   if (!value || typeof value !== 'object') {
+     return;
+   }
+   if (Array.isArray(value)) {
+     value.__proto__ = arrayMethods;
+     this.observeArray(value);
+   } else {
+     this.walk(value);
+   }
+ }
+  observeArray(items) {
+    for (let i = 0, l = items.length; i < l; i++) {
+      this.observe(items[i]);
+    }
+  }
+  walk(obj) {
+    const keys = Object.keys(obj);
+    keys.forEach(key => this.defineReactive(obj, key));
+  }
+  defineReactive(obj, key, value = obj[key]) {
+    let dep = new Dep();
+    this.observe(value);
+    if (Array.isArray(value)) {
+      value.__dep__ = dep;
+    }
+    Object.defineProperty(obj, key, {
+      get() {
+        if (Dep.target) {
+          dep.addSub(Dep.target);
+        }
+        return value;
+      },
+      set(newVal) {
+        if (newVal !== value) {
+          value = newVal;
+          dep.notify();
+        }
+      }
+    });
+  }
  proxyMethods() {
    for (let key in this.$methods) {
      this[key] = this.$methods[key].bind(this);
    }
  }
  proxyData() {
    for (let key in this.$data) {
      Object.defineProperty(this, key, {
        get() {
          return this.$data[key];
        },
        set(newVal) {
          this.$data[key] = newVal;
        }
      });
    }
  }
  compile(el, isOnce = false, scope) {
    const vm = scope || this;
    const {
      childNodes
    } = el;
    const conditionalElements = [];
    [...childNodes].forEach(node => {
      if (node.nodeType === ELEMENT_NODE && node.hasAttribute('v-pre')) {
        return;
      }
      if (node.nodeType === ELEMENT_NODE) {
        if (node.hasAttribute('v-if') || node.hasAttribute('v-else-if') || node.hasAttribute('v-else')) {
          conditionalElements.push(node);
        }
        isOnce = isOnce || node.hasAttribute('v-once');
        compileAttributes([...node.attributes], node, vm, isOnce);
        if (el.contains(node)) {
          vm.compile(node, isOnce);
        }
      } else if (node.nodeType === TEXT_NODE) {
        let {
          originalTextContent
        } = node;
        if (!originalTextContent) {
          originalTextContent = node.textContent;
          node.originalTextContent = originalTextContent;
        }
        let reg = /\{\{(.+?)\}\}/g;
        const updateTextContent = () => {
          node.textContent = originalTextContent.replace(reg, (_, key) => {
            return evaluate(key, vm);
          });
        };
        updateTextContent();
        if (!isOnce) {
          let match;
          while ((match = reg.exec(originalTextContent)) !== null) {
            const key = match[1].trim();
            new Watcher(vm, key, updateTextContent);
          }
        }
      }
    });
    if (conditionalElements.length > 0) {
      handleConditionalElements(conditionalElements, vm);
    }
    el.removeAttribute('v-cloak');
  }
}
function handleConditionalElements(conditionalElements, vm) {
  let lastVisibleElement = null;
  conditionalElements.forEach(element => {
    let expression = element.getAttribute('v-if') || element.getAttribute('v-else-if') || 'true';
    let evaluate = getEvaluate(expression, vm);
    element.evaluate = evaluate;
    const shouldDisplay = evaluate();
    if (shouldDisplay && !lastVisibleElement) {
      lastVisibleElement = element;
    } else {
      element.parentNode.removeChild(element);
    }
  });
  const updateVisibility = () => {
    const nextVisibleElement = conditionalElements.find(({
      evaluate
    }) => {
      return evaluate();
    });
    if (nextVisibleElement && nextVisibleElement !== lastVisibleElement) {
      lastVisibleElement.parentNode.replaceChild(nextVisibleElement, lastVisibleElement);
      lastVisibleElement = nextVisibleElement;
    }
  };
  new Watcher(vm, () => conditionalElements.map(({
    evaluate
  }) => evaluate()), updateVisibility);
}
function evaluate(expression, vm) {
  return getEvaluate(expression, vm).call(vm);
}
function getEvaluate(expression, vm) {
  return function () {
    const [expr, ...filters] = expression.split('|').map(expr => expr.trim());
    let value = new Function(`with(this){return ` + expr + `}`).call(vm);
    for (const filterName of filters) {
      const filterFunc = Vue.filters[filterName];
      value = filterFunc(value);
    }
    return value;
  };
}
function compileAttributes(attributes, node, vm, isOnce) {
  attributes.forEach(attr => {
    if (attr.name === 'v-text') {
      handleVText(node, attr, vm, isOnce);
    } else if (attr.name === 'v-html') {
      handleVHtml(node, attr, vm, isOnce);
    } else if (attr.name === 'v-bind:class' || attr.name === ':class') {
      handleVBindClass(node, attr, vm, isOnce);
    } else if (attr.name === 'v-bind:style' || attr.name === ':style') {
      handleVBindStyle(node, attr, vm, isOnce);
    } else if (attr.name.startsWith('v-bind:') || attr.name.startsWith(':')) {
      handleVBind(node, attr, vm, isOnce);
    } else if (attr.name.startsWith('v-on:') || attr.name.startsWith('@')) {
      handleEvent(node, attr, vm);
    } else if (attr.name === 'v-model') {
      handleVModel(node, attr, vm, isOnce);
    } else if (attr.name === 'v-show') {
      handleVShow(node, attr, vm, isOnce);
    } else if (attr.name === 'v-for') {
      handleVFor(node, attr, vm, isOnce);
    }
  });
}
function handleVBindStyle(node, attr, vm, isOnce) {
  let attrName = attr.name;
  if (attrName.startsWith(':')) {
    attrName = `v-bind${attrName}`;
  }
  attrName = attrName.slice(7);
  function updateStyle() {
    const styleObj = evaluate(attr.value, vm);
    for (const [key, value] of Object.entries(styleObj)) {
      node.style[key] = value;
    }
  }
  updateStyle();
  new Watcher(vm, attr.value, updateStyle);
}
function handleVBindClass(node, attr, vm, isOnce) {
  let attrName = attr.name;
  if (attrName.startsWith(':')) {
    attrName = `v-bind${attrName}`;
  }
  attrName = attrName.slice(7);
  const key = attr.value;
  function updateClassName() {
    let className = '';
    let value = evaluate(key, vm);
    if (Array.isArray(value)) {
      className = value.join(' ');
    } else if (typeof value === 'object') {
      className = Object.keys(value).filter(className => value[className]).join(' ');
    } else if (typeof value === 'string') {
      className = value;
    }
    node.setAttribute('class', className);
  }
  updateClassName();
  if (!isOnce) {
    new Watcher(vm, key, updateClassName);
  }
}
function handleVFor(node, attr, vm, isOnce) {
  const originNode = node.cloneNode(true);
  const {
    parentNode
  } = node;
  const placeholder = document.createComment('v-for-placeholder');
  parentNode.replaceChild(placeholder, node);
  const expression = attr.value;
  const [itemVar, , listKey] = expression.split(' ');
  const update = () => {
    while (placeholder.nextSibling) {
      parentNode.removeChild(placeholder.nextSibling);
    }
    evaluate(listKey, vm).forEach(itemData => {
      const clonedNode = originNode.cloneNode(true);
      parentNode.appendChild(clonedNode);
      const newScope = Object.create(vm);
      newScope[itemVar] = itemData;
      vm.compile(clonedNode, isOnce, newScope);
    });
  };
  update();
  new Watcher(vm, listKey, update);
}
function handleVShow(node, attr, vm, isOnce) {
  const expr = attr.value;
  const originalDisplay = node.style.display;
  const update = () => {
    node.style.display = evaluate(expr, vm) ? originalDisplay : 'none';
  };
  update();
  if (!isOnce) {
    new Watcher(vm, expr, update);
  }
}
function handleVModel(node, attr, vm, isOnce) {
  const key = attr.value;
  node.value = evaluate(key, vm);
  node.addEventListener('input', () => {
    vm[key] = node.value;
  });
  if (!isOnce) {
    new Watcher(vm, key, () => {
      node.value = evaluate(key, vm);
    });
  }
}
function handleVText(node, attr, vm, isOnce) {
  const expr = attr.value;
  node.textContent = evaluate(expr, vm);
  if (!isOnce) {
    new Watcher(vm, expr, (oldValue, newValue) => {
      node.textContent = newValue;
    });
  }
}
function handleVHtml(node, attr, vm, isOnce) {
  const expr = attr.value;
  node.innerHTML = evaluate(expr, vm);
  if (!isOnce) {
    new Watcher(vm, expr, () => {
      node.innerHTML = evaluate(expr, vm);
    });
  }
}
function handleVBind(node, attr, vm, isOnce) {
  let attrName = attr.name;
  if (attrName.startsWith(':')) {
    attrName = `v-bind${attrName}`;
  }
  attrName = attrName.slice(7);
  const key = attr.value;
  node.setAttribute(attrName, evaluate(key, vm));
  if (!isOnce) {
    new Watcher(vm, key, () => {
      node.setAttribute(attrName, evaluate(key, vm));
    });
  }
}
function handleEvent(node, attr, vm) {
  let attrName = attr.name;
  if (attrName.startsWith('@')) {
    attrName = `v-on:${attrName.slice(1)}`;
  }
  const [eventName, ...modifiers] = attrName.slice(5).split('.');
  const methodExpression = attr.value.trim();
  let methodName = methodExpression;
  let args = [];
  let parenIndex = methodExpression.indexOf('(');
  if (parenIndex !== -1) {
    methodName = methodExpression.substring(0, parenIndex);
    const argsString = methodExpression.substring(parenIndex + 1, methodExpression.length - 1).trim();
    args = argsString.split(',').map(arg => arg.trim());
  } else {
    args = ['$event'];
  }
  let options = {
    capture: modifiers.includes('capture'),
    once: modifiers.includes('once'),
    passive: modifiers.includes('passive')
  };
  node.addEventListener(eventName, event => {
    if (modifiers.includes('stop')) event.stopPropagation();
    if (modifiers.includes('prevent')) event.preventDefault();
    if (modifiers.includes('self')) {
      if (event.target !== event.currentTarget) {
        return;
      }
    }
    let modifierKeys = modifiers.map(modifier => KeyMap[modifier]).filter(Boolean).flat();
    if (modifierKeys.length > 0) {
      if (!modifierKeys.includes(event.key)) {
        return;
      }
    }
    let actualArgs = args.map(arg => {
      if (!isNaN(arg)) return arg;
      if (arg === '$event') return event;
      if (arg.startsWith("'") && arg.endsWith("'")) return arg.slice(1, -1);
      return evaluate(arg, vm);
    });
    vm[methodName](...actualArgs);
  }, options);
}