zoukankan      html  css  js  c++  java
  • Vue源码分析之实现一个简易版的Vue

    目标

    使用 Typescript 编写简易版的 vue 实现数据的响应式和基本的视图渲染,以及双向绑定功能。

    参考 https://cn.vuejs.org/v2/guide/reactivity.html

    测试代码中,编写vue.js是本篇的重点,基本使用方法与常规的Vue一样:

    <div id='app'>
        <div>{{ person.name }}</div>
        <div>{{ count }}</div>
        <div v-text='person.name'></div>
        <input type='text' v-model='msg' />
        <input type='text' v-model='person.name'/>
    </div>
    
    <script src='vue.js'></script>
    <script>
    let vm = new Vue({
        el: '#app',
        data: {
            msg: 'Hello vue',
            count: 100,
            person: { name: 'Tim' },
        }
    });
    vm.msg = 'Hello world';
    console.log(vm);
    
    //模拟数据更新
    setTimeout(() => { vm.person.name = 'Goooooood'; }, 1000);
    <script>
    

    页面渲染结果如下

    实现的简易Vue需要完成以下功能

    • 可以解析插值表达式,如 {{person.name}}
    • 可以解析内置指令,如 v-text
    • 可以双向绑定数据,如 v-model
    • 数据更新视图随之更新

    Vue当中有以下重要的组件

    1. 初始化时通过 Object.defineProperty 代理Vue.data的数据方便操作, 访问Vue.prop等于访问Vue.data.prop
    2. 通过 ObserverVue.data 里所有的数据及其子节点(递归)都进行捕捉,通过getter setter实现数据双向绑定
    3. 初始 Observergetter中收集依赖(watcher观察者)在setter中发送通知notify
    4. Watcher 中注册依赖Dep

    基层Vue

    Vue 数据结构,这里只关注下面三个属性

    字段 说明
    $options 存放构造时传入的配置参数
    $data 存放数据
    $el 存放需要渲染的元素

    实现Vue时,需要完成以下功能:

    • 负责接收初始化参数 options
    • 负责把data属性注入到vue,并转换成 getter/setter
    • 负责调用observer监听所有属性变化
    • 负责调用compiler解析指令和差值表达式

    类型接口定以

    为保持灵活性,这里直接用any类型

    interface VueData {
        [key: string]: any,
    }
    
    interface VueOptions {
        data: VueData;
        el: string | Element;
    }
    
    interface Vue {
        [key: string]: any,
    }
    

    Vue实现代码

    class Vue {
        public $options: VueOptions;
        public $data: VueData;
        public $el: Element | null;
    
        public constructor(options: VueOptions) {
            this.$options = options;
            this.$data = options.data || {};
            if (typeof options.el == 'string') {
                this.$el = document.querySelector(options.el);
            } else {
                this.$el = options.el;
            }
    
            if (!this.$el) {
                throw Error(`cannot find element by selector ${options.el}`);
                return;
            }
            this._proxyData(this.$data);
        }
    
        //生成代理,通过直接读写vue属性来代理vue.$data的数据,提高便利性
        //vue[key] => vue.data[key]
        private _proxyData(data: VueData) {
            Object.keys(data).forEach(key => {
                Object.defineProperty(this, key, {
                    enumerable: true,
                    configurable: true,
                    get() {
                        return data[key];
                    },
                    set(newVal) {
                        if (newVal == data[key]) {
                            return;
                        }
                        data[key] = newVal;
                    }
                })
            })
        }
    }
    
    
    • 对于Vue的元数据均以$开头表示,因为访问 Vue.data 会被代理成 Vue.$data.data,即注入属性与元属性进行区分
    • $el 可以为选择器或Dom,但最终需要转成Dom,若不存在Dom抛出错误
    • _porxyData,下划线开头为私有属性或方法,此方法可以将 $data 属性注入到vue中
    • enumerable 为可枚举, configurable 为可配置,如重定以和删除属性
    • setter 中,如果数据没有发生变化则return,发生变化更新 $data

    简单测试一下

    let vm = new Vue({
        el: '#app',
        data: {
            msg: 'Hello vue',
            count: 100,
            person: { name: 'Tim' },
        }
    });
    

    上图中颜色比较幽暗的,表示注入到Vue的属性已成功设置了getter和setter

    Observer

    • 负责把data选项中的属性转换成响应式数据
    • data中某个属性的值也是对象,需要递归转换成响应式
    • 数据发生变化时发送通知

    Observer 实现代码

    class Observer {
        constructor(data) {
            this.walk(data);
        }
        walk(data) {
            Object.keys(data).forEach(key => {
                this.defineReactive(data, key, data[key]);
            });
        }
        defineReactive(obj, key, val) {
            //递归处理成响应式
            if (typeof val === 'object') {
                this.walk(val);
            }
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                get() {
                    //注意:这里val不可改成obj[key],会无限递归直至堆栈溢出
                    return val;
                },
                set: (newVal) => {
                    if (newVal == val) {
                        return;
                    }
                    //注意:这里newVal不可改成obj[key],会触发 getter
                    val = newVal;
                    if (typeof newVal == 'object') {
                        this.walk(newVal);
                    }
                }
            });
        }
    }
    
    • walk方法 用于遍历$data属性,传递给defineReactive做响应式处理
    • defineReactive 如果值为对象则递归调用walk,如果值为原生数据则设置getter和setter

    关于defineReactive(data, key, val)中的形参val

    形参中的 val 等同于 data[key],这里千万不能在getter或setter内部中使用 data[key]获取值,会造成无需递归导致堆栈溢出

    因而需要在defineReactive调用前访问 data[key] 并将其值以 val形参传递内部使用中。 这个val会一直存放在闭包空间当中

    Observer 引用

    在上面编写的 Vue.constructor 中添加Observer的引用,并传入$data

        //Vue.constructor
        public constructor(options: VueOptions) {
            this.$options = options;
            this.$data = options.data || {};
            if (typeof options.el == 'string') {
                this.$el = document.querySelector(options.el);
            } else {
                this.$el = options.el;
            }
    
            if (!this.$el) {
                throw Error(`cannot find element by selector ${options.el}`);
                return;
            }
            this._proxyData(this.$data);
            new Observer(this.$data);          //新增此行
        }
    

    测试

    重新打印vm可以看到 $data 里的成员也有getter和setter方法了

    Compiler

    • 负责编译模板,解析指令v-xxx和插值表达式{{var}}
    • 负责页面首次渲染
    • 当数据发生变化时,重新渲染视图

    注意,为简化代码,这里的插值表达式,不处理复杂情况,只处理单一的变量读取

    {{count + 2}} => 不进行处理
    {{person.name}} => 可以处理

    Util 辅助工具

    为方便操作,我们需要提前编写几个简单的函数功能,并封装到 Util 类中静态方法里

    class Util {
        static isPrimitive(s: any): s is (string | number) {
            return typeof s === 'string' || typeof s === 'number';
        }
    
        static isHTMLInputElement(element: Element): element is HTMLInputElement {
            return (<HTMLInputElement>element).tagName === 'INPUT';
        }
    
        //处理无法引用 vm.$data['person.name'] 情况
        static getLeafData(obj: Object, key: string): any {
            let textData: Array<any> | Object | String | Number = obj;
    
            if (key.indexOf('.') >= 0) {
                let keys = key.split('.');
                for (let k of keys) {
                    textData = textData[k];
                }
            } else {
                textData = obj[key];
            }
    
            return textData;
        }
    
        static setLeafData(obj: Object, key: string, value: any): void {
            if (key.indexOf('.') >= 0) {
                let keys = key.split('.');
    
                for (let i = 0; i < keys.length; i++) {
                    let k = keys[i];
                    if (i == keys.length - 1) {
                        obj[k] = value;
                    } else {
                        obj = obj[k];
                    }
    
                }
            } else {
                if (obj[key]){
                    obj[key] = value;
                }
            }
        }
    }
    
    • isPrimitive

    该函数用于判断变量是否为原生类型(string or number)

    • isHTMLInputElement

    该函数用于判断元素是否为Input元素,用于后面处理 v-model 指令的双向绑定数据,默认:value @input

    • getLeafData

    因为key可能为 person.name, 如果直接中括号访问对象属性如 obj['person.name'] 无法等同于 obj.person.name

    该函数如果传递的键key中,若不包含点.,则直接返回 obj[key]。 若包含,则解析处理返回 obj.key1.key2.key3

    • setLeafData

    同上, key为person.name时,设置 obj.person.name = value,否则设置 obj.key = value

    Complier 实现代码

    class Compiler {
        public el: Element | null;
        public vm: Vue;
    
        constructor(vm: Vue) {
            this.el = vm.$el,
                this.vm = vm;
            if (this.el) {
                this.compile(this.el);
            }
        }
    
        compile(el: Element) {
            let childNodes = el.childNodes;
            Array.from(childNodes).forEach((node: Element) => {
                if (this.isTextNode(node)) {
                    this.compileText(node);
                } else if (this.isElementNode(node)) {
                    this.compileElement(node);
                }
    
                //递归处理孩子nodes
                if (node.childNodes && node.childNodes.length !== 0) {
                    this.compile(node);
                }
            })
        }
    
        //解析插值表达式 {{text}}
        compileText(node: Node) {
            let pattern: RegExpExecArray | null;
            if (node.textContent && (pattern = /{{(.*?)}}/.exec(node.textContent))) {
                let key = pattern[1].trim();
                if (key in this.vm.$data && Util.isPrimitive(this.vm.$data[key])) {
                    node.textContent = this.vm.$data[key];
                }
            }
        }
    
        //解析 v-attr 指令
        compileElement(node: Element) {
            Array.from(node.attributes).forEach((attr) => {
                if (this.isDirective(attr.name)) {
                    let directive: string = attr.name.substr(2);
                    let value = attr.value;
                    let processer: Function = this[directive + 'Updater'];
                    if (processer) {
                        processer.call(this, node, value);
                    }
    
                }
            })
        }
    
        //处理 v-model 指令
        modelUpdater(node: Element, key: string) {
            if (Util.isHTMLInputElement(node)) {
                let value = Util.getLeafData(this.vm.$data, key);
                if (Util.isPrimitive(value)) {
                    node.value = value.toString();
                }
    
                node.addEventListener('input', () => {
                    Util.setLeafData(this.vm.$data, key, node.value);
                    console.log(this.vm.$data);
                })
            }
        }
    
        //处理 v-text 指令
        textUpdater(node: Element, key: string) {
            let value = Util.getLeafData(this.vm.$data, key);
            if (Util.isPrimitive(value)) {
                node.textContent = value.toString();
            }
        }
    
        //属性名包含 v-前缀代表指令
        isDirective(attrName: string) {
            return attrName.startsWith('v-');
        }
    
        //nodeType为3属于文本节点
        isTextNode(node: Node) {
            return node.nodeType == 3;
        }
    
        //nodeType为1属于元素节点
        isElementNode(node: Node) {
            return node.nodeType == 1;
        }
    }
    
    • compile

    用于首次渲染传入的 div#app 元素, 遍历所有第一层子节点,判断子节点nodeType属于文本还是元素

    若属于 文本 则调用 compileText 进行处理, 若属于 元素 则调用 compileElement 进行处理。

    另外如果子节点的孩子节点 childNodes.length != 0 则递归调用 compile(node)

    • compileText

    用于渲染插值表达式,使用正则 {{(.*?)}} 检查是否包含插值表达式,提取括号内变量名

    通过工具函数 Utils.getLeafData(vm.$data, key) 尝试读取 vm.$data[key]vm.$data.key1.key2 的值

    如果能读取成功,则渲染到视图当中 node.textContent = this.vm.$data[key];

    • compileElement

    用于处理内置v-指令,通过 node.attributes 获取所有元素指令,Array.from() 可以使NamedNodeMap转成可遍历的数组

    获取属性名,判断是否有 v- 前缀,若存在则进行解析成函数,解析规则如下

    • v-text 解析的函数名为 textUpdater()
    • v-model 解析函数名为 modelUpdater()

    可以通过尝试方法获取,如 this[directive + "Updater"] 若不为 undefined 说明指令处理函数是存在的

    最后通过 call 调用,使得 this 指向 Compiler类实例

    • textUpdater

    与 compileText 类似,尝试读取变量并渲染到Dom中

    • modelUpdate

    除了尝试读取变量并渲染到Dom中,还需要设置 @input 函数监听视图的变化来更新数据

    node.addEventListener('input', () => {
        Util.setLeafData(this.vm.$data, key, node.value);
    })
    

    Complier 实例化引用

    在 Vue.constructor 中引用 Compiler 进行首次页面渲染

        //Vue.constructor
        public constructor(options: VueOptions) {
            this.$options = options;
            this.$data = options.data || {};
            if (typeof options.el == 'string') {
                this.$el = document.querySelector(options.el);
            } else {
                this.$el = options.el;
            }
    
            if (!this.$el) {
                throw Error(`cannot find element by selector ${options.el}`);
                return;
            }
            this._proxyData(this.$data);
            new Observer(this.$data);
            new Compiler(this);                  //新增此行
        }
    

    测试代码

    <div id='app'>
        <div>{{ person.name }}</div>
        <div>{{ count }}</div>
        <div v-text='person.name'></div>
        <input type='text' v-model='msg' />
        <input type='text' v-model='person.name'/>
    </div>
    <script src='vue.js'></script>
    <script>
    
    let vm = new Vue({
        el: '#app',
        data: {
            msg: 'Hello vue',
            count: 100,
            person: { name: 'tim' },
        }
    })
    </scirpt>
    
    

    渲染结果

    至此完成了初始化数据驱动和渲染功能,我们修改 input 表单里的元素内容是会通过 @input动态更新$data对应绑定v-model的数据

    但是此时我们在控制台中修改 vm.msg = 'Gooooood' ,视图是不会有响应式变化的,因此下面将通过WatcherDep 观察者模式来实现响应式处理

    Watcher 与 Dep

    Dep(Dependency)

    实现功能:

    • 收集依赖,添加观察者(Watcher)
    • 通知所有的观察者 (notify)

    Dep 实现代码

    class Dep {
        static target: Watcher | null;
        watcherList: Watcher[] = [];
    
        addWatcher(watcher: Watcher) {
            this.watcherList.push(watcher);
        }
    
        notify() {
            this.watcherList.forEach((watcher) => {
                watcher.update();
            })
        }
    }
    
    

    Watcher

    实现功能:

    • 当变化触发依赖时,Dep通知Watcher进行更新视图
    • 当自身实例化时,向Dep中添加自己

    Watcher 实现代码

    每个观察者Watcher都必须包含 update方法,用于描述数据变动时如何响应式渲染到页面中

    class Watcher {
        public vm: Vue;
        public cb: Function;
        public key: string;
        public oldValue: any;
    
        constructor(vm: Vue, key: string, cb: Function) {
            this.vm = vm;
            this.key = key;
            this.cb = cb;
    
            //注册依赖
            Dep.target = this;
    
            //访问属性触发getter,收集target
            this.oldValue = Util.getLeafData(vm.$data, key);
    
            //防止重复添加
            Dep.target = null;
        }
    
        update() {
            let newVal = Util.getLeafData(this.vm.$data, this.key);
    
            if (this.oldValue == newVal) {
                return;
            }
    
            this.cb(newVal);
        }
    }
    

    修改 Observer.defineReactive

    对于$data中每一个属性,都对应着一个 Dep,因此我们需要在$data初始化响应式时创建Dep实例,在getter 中收集观察者Dep.addWatcher(), 在 setter 中通知观察者 Dep.notify()

      
        defineReactive(obj: VueData, key: string, val: any) {
            let dep = new Dep();                        //新增此行,每个$data中的属性都对应一个Dep实例化
    
            //如果data值的为对象,递归walk
            if (typeof val === 'object') {
                this.walk(val);
            }
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                get() {
                    Dep.target && dep.addWatcher(Dep.target);       //检查是否有Watcher,收集依赖的观察者
                    //此处不能返回 obj[key] 会无限递归触发get
                    console.log('getter')
                    return val;
                },
                set: (newVal) => {
                    if (newVal == val) {
                        return;
                    }
                    val = newVal;
                    if (typeof newVal == 'object') {
                        this.walk(newVal)
                    }
    
                    //发送通知
                    dep.notify();                        //新增此行,$data中属性发送变动时发送通知
                }
            });
        }
    

    修改 Compiler类,下面几个方法均添加实例化Watcher

    每个视图对应一个Watcher,以key为关键字触发响应的Dep,并通过getter将Watcher添加至Dep中

    
    class Compiler {
        //插值表达式
        compileText(node: Node) {
            let pattern: RegExpExecArray | null;
            if (node.textContent && (pattern = /{{(.*?)}}/.exec(node.textContent))) {
                let key = pattern[1].trim();
                let value = Util.getLeafData(this.vm.$data, key);
                if (Util.isPrimitive(value)) {
                    node.textContent = value.toString();
                }
                new Watcher(this.vm, key, (newVal: string) => { node.textContent = newVal; });   //新增此行
            }
        }
    
        //v-model
        modelUpdater(node: Element, key: string) {
            if (Util.isHTMLInputElement(node)) {
                let value = Util.getLeafData(this.vm.$data, key);
                if (Util.isPrimitive(value)) {
                    node.value = value.toString();
                }
    
                node.addEventListener('input', () => {
                    Util.setLeafData(this.vm.$data, key, node.value);
                    console.log(this.vm.$data);
                })
    
                new Watcher(this.vm, key, (newVal: string) => { node.value = newVal; });  //新增此行
            }
        }
    
        //v-text
        textUpdater(node: Element, key: string) {
            let value = Util.getLeafData(this.vm.$data, key);
            if (Util.isPrimitive(value)) {
                node.textContent = value.toString();
            }
    
            new Watcher(this.vm, key, (newVal: string) => { node.textContent = newVal; });   //新增此行
        }
    }
    

    至此本篇目的已经完成,实现简易版Vue的响应式数据渲染视图和双向绑定,下面是完整 ts代码和测试代码

    实现简易版Vue完整代码

    //main.ts 这里要自己编译成 main.js
    interface VueData {
        [key: string]: any,
    }
    
    interface VueOptions {
        data: VueData;
        el: string | Element;
    }
    
    interface Vue {
        [key: string]: any,
    }
    
    class Util {
        static isPrimitive(s: any): s is (string | number) {
            return typeof s === 'string' || typeof s === 'number';
        }
    
        static isHTMLInputElement(element: Element): element is HTMLInputElement {
            return (<HTMLInputElement>element).tagName === 'INPUT';
        }
    
        //处理无法引用 vm.$data['person.name'] 情况
        static getLeafData(obj: Object, key: string): any {
            let textData: Array<any> | Object | String | Number = obj;
    
            if (key.indexOf('.') >= 0) {
                let keys = key.split('.');
                for (let k of keys) {
                    textData = textData[k];
                }
            } else {
                textData = obj[key];
            }
    
            return textData;
        }
    
        static setLeafData(obj: Object, key: string, value: any): void {
            if (key.indexOf('.') >= 0) {
                let keys = key.split('.');
    
                for (let i = 0; i < keys.length; i++) {
                    let k = keys[i];
                    if (i == keys.length - 1) {
                        obj[k] = value;
                    } else {
                        obj = obj[k];
                    }
    
                }
            } else {
                if (obj[key]){
                    obj[key] = value;
                }
            }
        }
    }
    
    class Vue {
        public $options: VueOptions;
        public $data: VueData;
        public $el: Element | null;
    
        public constructor(options: VueOptions) {
            this.$options = options;
            this.$data = options.data || {};
            if (typeof options.el == 'string') {
                this.$el = document.querySelector(options.el);
            } else {
                this.$el = options.el;
            }
    
            if (!this.$el) {
                throw Error(`cannot find element by selector ${options.el}`);
                return;
            }
            this._proxyData(this.$data);
            new Observer(this.$data);
            new Compiler(this);
        }
    
        //生成代理,通过直接读写vue属性来代理vue.$data的数据,提高便利性
        //vue[key] => vue.data[key]
        private _proxyData(data: VueData) {
            Object.keys(data).forEach(key => {
                Object.defineProperty(this, key, {
                    enumerable: true,
                    configurable: true,
                    get() {
                        return data[key];
                    },
                    set(newVal) {
                        if (newVal == data[key]) {
                            return;
                        }
                        data[key] = newVal;
                    }
                })
            })
        }
    }
    
    class Observer {
        constructor(data: VueData) {
            this.walk(data);
        }
    
        walk(data: VueData) {
            Object.keys(data).forEach(key => {
                this.defineReactive(data, key, data[key]);
            });
        }
    
        //观察vue.data的变化,并同步渲染至视图中
        defineReactive(obj: VueData, key: string, val: any) {
            let dep = new Dep();
    
    
            //如果data值的为对象,递归walk
            if (typeof val === 'object') {
                this.walk(val);
            }
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                get() {
                    //收集依赖
                    Dep.target && dep.addWatcher(Dep.target);
                    //此处不能返回 obj[key] 会无限递归触发get
                    console.log('getter')
                    return val;
                },
                set: (newVal) => {
                    if (newVal == val) {
                        return;
                    }
                    val = newVal;
                    if (typeof newVal == 'object') {
                        this.walk(newVal)
                    }
    
                    //发送通知
                    dep.notify();
                }
            });
        }
    }
    
    class Compiler {
        public el: Element | null;
        public vm: Vue;
    
        constructor(vm: Vue) {
            this.el = vm.$el,
                this.vm = vm;
            if (this.el) {
                this.compile(this.el);
            }
        }
    
        compile(el: Element) {
            let childNodes = el.childNodes;
            Array.from(childNodes).forEach((node: Element) => {
                if (this.isTextNode(node)) {
                    this.compileText(node);
                } else if (this.isElementNode(node)) {
                    this.compileElement(node);
                }
    
                //递归处理孩子nodes
                if (node.childNodes && node.childNodes.length !== 0) {
                    this.compile(node);
                }
            })
        }
    
        // {{text}}
        compileText(node: Node) {
            let pattern: RegExpExecArray | null;
            if (node.textContent && (pattern = /{{(.*?)}}/.exec(node.textContent))) {
                let key = pattern[1].trim();
                let value = Util.getLeafData(this.vm.$data, key);
                if (Util.isPrimitive(value)) {
                    node.textContent = value.toString();
                }
                new Watcher(this.vm, key, (newVal: string) => { node.textContent = newVal; })
            }
        }
    
        //v-attr
        compileElement(node: Element) {
            Array.from(node.attributes).forEach((attr) => {
                if (this.isDirective(attr.name)) {
                    let directive: string = attr.name.substr(2);
                    let value = attr.value;
                    let processer: Function = this[directive + 'Updater'];
                    if (processer) {
                        processer.call(this, node, value);
                    }
    
                }
            })
        }
    
        //v-model
        modelUpdater(node: Element, key: string) {
            if (Util.isHTMLInputElement(node)) {
                let value = Util.getLeafData(this.vm.$data, key);
                if (Util.isPrimitive(value)) {
                    node.value = value.toString();
                }
    
                node.addEventListener('input', () => {
                    Util.setLeafData(this.vm.$data, key, node.value);
                    console.log(this.vm.$data);
                })
    
                new Watcher(this.vm, key, (newVal: string) => { node.value = newVal; })
            }
        }
    
        //v-text
        textUpdater(node: Element, key: string) {
            let value = Util.getLeafData(this.vm.$data, key);
            if (Util.isPrimitive(value)) {
                node.textContent = value.toString();
            }
    
            new Watcher(this.vm, key, (newVal: string) => {
                node.textContent = newVal;
            });
        }
    
        isDirective(attrName: string) {
            return attrName.startsWith('v-');
        }
    
        isTextNode(node: Node) {
            return node.nodeType == 3;
        }
    
        isElementNode(node: Node) {
            return node.nodeType == 1;
        }
    }
    
    class Dep {
        static target: Watcher | null;
        watcherList: Watcher[] = [];
    
        addWatcher(watcher: Watcher) {
            this.watcherList.push(watcher);
        }
    
        notify() {
            this.watcherList.forEach((watcher) => {
                watcher.update();
            })
        }
    }
    
    class Watcher {
        public vm: Vue;
        public cb: Function;
        public key: string;
        public oldValue: any;
    
        constructor(vm: Vue, key: string, cb: Function) {
            this.vm = vm;
            this.key = key;
            this.cb = cb;
    
            //注册依赖
            Dep.target = this;
    
            //访问属性触发getter,收集target
            this.oldValue = Util.getLeafData(vm.$data, key);
    
            //防止重复添加
            Dep.target = null;
        }
    
        update() {
            let newVal = Util.getLeafData(this.vm.$data, this.key);
    
            if (this.oldValue == newVal) {
                return;
            }
    
            this.cb(newVal);
        }
    }
    
    
    
    

    测试代码

    <div id='app'>
        <div>{{ person.name }}</div>
        <div>{{ count }}</div>
        <div v-text='person.name'></div>
        <input type='text' v-model='msg' />
        <input type='text' v-model='person.name'/>
    </div>
        <script src='dist/main.js'></script>
    <script>
    let vm = new Vue({
        el: '#app',
        data: {
            msg: 'Hello vue',
            count: 100,
            person: { name: 'tim' },
        }
    })
    
    // vm.msg = 'Hello world';
    console.log(vm);
    
    setTimeout(() => { vm.person.name = 'Goooooood' }, 1000);
    </scirpt>
    
  • 相关阅读:
    怎样从外网访问内网数据库?
    怎样从外网访问内网Linux系统?
    怎样从外网访问内网Nginx?
    使用Holer外网SSH访问内网(局域网)Linux系统
    使用Holer远程桌面登录家里电脑和公司内网电脑
    使用Holer将本地端口映射到公网
    使用内网映射工具Holer将本地的Web应用映射到公网上访问
    算法学习笔记:关联分析(转)
    Python查看模块信息
    R语言中的因子
  • 原文地址:https://www.cnblogs.com/demonxian3/p/13546525.html
Copyright © 2011-2022 走看看