浅析Vue响应系统原理与搭建vue2.x迷你版

简介: 浅析Vue响应系统原理与搭建vue2.x迷你版

微信截图_20220506155254.png


Vue2.x响应式原理怎么实现的?


Vue 最独特的特性之一,是其非侵入性的响应式系统。那么什么是响应式原理?


数据模型仅仅是普通的JavaScript对象,而当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率。简言之,在改变数据的时候,视图会跟着更新。


了解概念之后,那么它是怎么实现的呢?


其实是利用Object.defineProperty()中的gettersetter方法和设计模式中的观察者模式


那么,我们先来看下Object.defineProperty()。MDN中它是这样解释它的:Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。


let data = {
  msg:'hello'
};
let vm = {};
Object.defineProperty(vm, 'msg', {
        enumerable: true, // 可枚举(可遍历)
        configurable: true, // 可配置(可以使用delete 删除,可以通过defineProperty重新定义)
        // 当获取值的时候执行
        get() {
            console.log('get', data.msg);
            return data.msg
        },
        // 当设置值的时候执行
        set(newVal) {
            if (newVal === data.msg) {
                return
            }
            data.msg = newVal;
            console.log('set', data.msg);
        }
})
// 测试
console.log(vm.msg);
/* 
> "get" "hello"
> "hello"
*/
vm.msg = 'world'; // > "set" "world"


简单介绍Object.defineProperty()之后,接着就是了解观察者模式,看到它,你可能会想起发布-订阅模式。其实它们的本质是相同的,但是也存在一定的区别。


我们不妨先来看下发布-订阅模式


发布-订阅者模式里面包含了三个模块,发布者,订阅者和统一调度中心。这里统一调度中心相当于报刊办事大厅。发布者相当与某个杂志负责人,他来中心这注册一个的杂志,而订阅者相当于用户,我在中心订阅了这分杂志。每当发布者发布了一期杂志,办事大厅就会通知订阅者来拿新杂志。发布-订阅者模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。


下面,我们将通过一个实现Vue自定义事件的例子来更进一步了解发布-订阅模式


function EventEmitter(){
    // 初始化统一调度中心
    this.subs = Object.create(null); // {'click':[fn1,fn2]}
}
// 注册事件
EventEmitter.prototype.$on = function (eventType,handler){
        console.log(this);
        this.subs[eventType]= this.subs[eventType]||[];
        this.subs[eventType].push(handler);
}
// 触发事件
EventEmitter.prototype.$emit = function (eventType,data){
        if(this.subs[eventType]){
                this.subs[eventType].forEach(handler => {
                    handler(data);
                });
        }
}
// 测试
const em = new EventEmitter();
//订阅者
em.$on('click1',(data)=>{
    console.log(data);
})
// 发布者
em.$emit('click1','maomin') //maomin


这种自定义事件广泛应用于Vue同级组件传值。


接下来,我们来介绍观察者模式


观察者模式是由目标调度,比如当事件触发时,目标就会调用观察者的方法,所以观察者模式的订阅者(观察者)与发布者(目标)之间存在依赖。


// 发布者(目标)
function Dep(){
    this.subs = [];
}
Dep.prototype.addSub = function (sub){
    if(sub&&sub.update){
            this.subs.push(sub);
    }
}
Dep.prototype.notify = function (data){
        this.subs.forEach(sub=>{
            sub.update(data);
        })
}
// 订阅者(观察者)
function Watcher(){}
    Watcher.prototype.update=function(data){
    console.log(data);
}
// 测试
let dep = new Dep();
let watcher = new Watcher();
// 收集依赖
dep.addSub(watcher);
// 发送通知
dep.notify('1');
dep.notify('2');


下图是区分两种模式。


微信截图_20220506155305.png


实现Vue2.x迷你版本


为什么要实现一个Vue迷你版本,目的就是加深对Vue响应式原理以及其中一些API的理解。首先我们先来分析Vue2.x 响应式原理的整体结构。


如下图所示:


微信截图_20220506155315.png


我们接下来,将根据这幅图片描述的流程来实现一款迷你版Vue。Vue2.x采用了Virtual DOM,但是因为这里只需要实现一个迷你版,所以我们这里做了简化,我们这里就是直接操作DOM。


下面,我们来看下我是如何搭建一款Vue mini的。


第一步


页面结构如下,我们可以先引入Vue2.x完整版本,看下实现效果。


<!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>Vue2.x Reactive</title>
</head>
<body>
    <div id="app">
        <h2>文本节点</h2>
        <div>{{msg}}</div>
        <div>{{count}}</div>
        <div>{{obj.name}}</div>
        <div>{{arr[0]}}</div>
        <div>{{obj.inner.age}}</div>
        <div>{{obj.inner.name}}</div>
        <h2>v-text</h2>
        <div v-text="msg"></div>
        <h2>v-model</h2>
        <input type="text" v-model="msg">
        <input type="text" v-model="count">
        <h2>v-html</h2>
        <div v-html="html"></div>
        <h2>v-show</h2>
        <div v-show="isShow">{{isShow}}</div>
        <h2>v-on</h2>
        <button v-on:click="handler">handler</button>
        <button @click="onClick">onClick</button>
        <h2>v-if</h2>
        <div>
            <p v-if="isIf">{{isIf}}</p>
        </div>
    </div>
    <script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
    <script>
        const vm = new Vue({
                el: '#app',
                data() {
                    return {
                        msg: 'maomin',
                        count: 1,
                        obj: {
                            name: 'hello',
                            inner: {
                                age: 17
                            }
                        },
                        arr: ['string1'],
                        html: '<div>{{msg}}</div>',
                        isShow: false,
                        isIf:true
                    }
                },
                methods: {
                    handler() {
                        // this.count = 2;
                        this.isIf = !this.isIf;
                    },
                    onClick() {
                        this.obj.inner.age = 18;
                        // console.log(this.obj.inner.age);
                    }
                }
            });
    </script>
</body>
</html>


经过测试,Vue2.x完整版搭载的页面显示如下。我们将使用Vue迷你版本同样实现以下页面效果。


微信截图_20220506155327.png


第二步


我们将根据整体结构图和页面结构来搭建这个Vue迷你版本,我们姑且将这个版本叫做vuemini.js


通过整体结构图我们发现,一共有VueObserverCompilerDepWatcher这几个构造函数。我们首先创建这几个构造函数,这里不使用class类来定义是因为Vue源码大部分也使用构造函数,另外,相对也好拓展。


Vue


// 实例。
function Vue(options) {
    this.$options = options || {};
    this._data = typeof options.data === 'function' ? options.data() : options.data || {};
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
    // 负责把data中的属性注入到Vue实例,转换成getter/setter
    this._proxyData(this._data);
    this.initMethods(this, options.methods || {})
    // 负责调用observer监听data中所有属性的变化
    new Observer(this._data);
    // 负责调用compiler解析指令/插值表达式
    new Compiler(this);
}
// 将data中的属性挂载到this上
Vue.prototype._proxyData = function (data) {
    Object.keys(data).forEach(key => {
        Object.defineProperty(this, key, {
            configurable: true,
            enumerable: true,
            get() {
                return data[key]
            },
            set(newVal) {
                if (newVal === data[key]) {
                    return
                }
                data[key] = newVal;
            }
        })
    })
}
function noop(a, b, c) { }
function polyfillBind(fn, ctx) {
    function boundFn(a) {
        var l = arguments.length;
        return l
            ? l > 1
                ? fn.apply(ctx, arguments)
                : fn.call(ctx, a)
            : fn.call(ctx)
    }
    boundFn._length = fn.length;
    return boundFn
}
function nativeBind(fn, ctx) {
    return fn.bind(ctx)
}
const bind = Function.prototype.bind
    ? nativeBind
    : polyfillBind;
// 初始化methods属性
Vue.prototype.initMethods = function (vm, methods) {
    for (var key in methods) {
        {
            if (typeof methods[key] !== 'function') {
                warn(
                    "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
                    "Did you reference the function correctly?",
                    vm
                );
            }
        }
        vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
    }
}


Observer


// 数据劫持。
// 负责把data(_data)选项中的属性转换成响应式数据。
function Observer(data) {
    this.walk(data);
}
Observer.prototype.walk = function (data) {
    if (!data || typeof data !== 'object') {
        return
    }
    Object.keys(data).forEach(key => {
        this.defineReactive(data, key, data[key]);
    })
}
Observer.prototype.defineReactive = function (obj, key, val) {
    let that = this;
    // 负责收集依赖
    let dep = new Dep();
    // 如果val是对象,把val内部的属性转换成响应式数据
    this.walk(val);
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            // 收集依赖
            Dep.target && dep.addSub(Dep.target)
            return val
        },
        set(newVal) {
            if (newVal === val) {
                return
            }
            val = newVal;
            // data内属性重新赋值后,使其转化为响应式数据。
            that.walk(newVal);
            // 发送通知
            dep.notify();
        }
    })
}


Compiler


// 编译模板,解析指令/插值表达式
// 负责页面的首次渲染
// 当数据变化时重新渲染视图
function Compiler(vm) {
    this.el = vm.$el;
    this.vm = vm;
    // 立即编译模板
    this.compile(this.el);
}
// 编译模板,处理文本节点和元素节点
Compiler.prototype.compile = function (el) {
    let childNodes = el.childNodes;
    Array.from(childNodes).forEach(node => {
        // 处理文本节点
        if (this.isTextNode(node)) {
            this.compileText(node);
        }
        // 处理元素节点 
        else if (this.isElementNode(node)) {
            this.compileElement(node);
        }
        // 判断node节点,是否有子节点,如果有子节点,要递归调用compile方法
        if (node.childNodes && node.childNodes.length) {
            this.compile(node);
        }
    })
}
// 编译文本节点,处理插值表达式
Compiler.prototype.compileText = function (node) {
    // console.dir(node);
    let reg = /\{\{(.+?)\}\}/;
    let value = node.textContent;
    if (reg.test(value)) {
        let key = RegExp.$1.trim();
        if (this.vm.hasOwnProperty(key)) {
            node.textContent = value.replace(reg, typeof this.vm[key] === 'object' ? JSON.stringify(this.vm[key]) : this.vm[key]);
            // 创建watcher对象,当数据改变更新视图
            new Watcher(this.vm, key, (newVal) => {
                node.textContent = newVal;
            })
        } else {
            const str = `this.vm.${key}`;
            node.textContent = value.replace(reg, eval(str));
            // 创建watcher对象,当数据改变更新视图
            new Watcher(this.vm, key, () => {
                const strw = `this.vm.${key}`;
                node.textContent = value.replace(reg, eval(strw));
            })
        }
    }
}
// 判断节点是否是文本节点
Compiler.prototype.isTextNode = function (node) {
    return node.nodeType === 3;
}
// 判断节点是否是元素节点
Compiler.prototype.isElementNode = function (node) {
    return node.nodeType === 1;
}
// 编译元素节点,处理指令
Compiler.prototype.compileElement = function (node) {
    // console.log(node.attributes);
    // 遍历所有的属性节点
    Array.from(node.attributes).forEach(attr => {
        let attrName = attr.name;
        // console.log(attrName);
        // 判断是否是指令
        if (this.isDirective(attrName)) {
            // 判断:如v-on:click
            let eventName;
            if (attrName.indexOf(':') !== -1) {
                const strArr = attrName.substr(2).split(':');
                attrName = strArr[0];
                eventName = strArr[1];
            } else if (attrName.indexOf('@') !== -1) {
                eventName = attrName.substr(1);
                attrName = 'on';
            } else {
                attrName = attrName.substr(2);
            }
            let key = attr.value;
            this.update(node, key, attrName, eventName);
        }
    })
}
// 判断元素属性是否是指令
Compiler.prototype.isDirective = function (attrName) {
    return attrName.startsWith('v-') || attrName.startsWith('@');
}
// 指令辅助函数
Compiler.prototype.update = function (node, key, attrName, eventName) {
    let updateFn = this[attrName + 'Updater'];
    updateFn && updateFn.call(this, node, this.vm[key], key, eventName);
}
// 处理v-text指令
Compiler.prototype.textUpdater = function (node, value, key) {
    node.textContent = value;
    new Watcher(this.vm, key, (newVal) => {
        node.textContent = newVal;
    })
}
// 处理v-html指令
Compiler.prototype.htmlUpdater = function (node, value, key) {
    node.insertAdjacentHTML('beforeend', value);
    new Watcher(this.vm, key, (newVal) => {
        node.insertAdjacentHTML('beforeend', newVal);
    })
}
// 处理v-show指令
Compiler.prototype.showUpdater = function (node, value, key) {
    !value ? node.style.display = 'none' : node.style.display = 'block'
    new Watcher(this.vm, key, (newVal) => {
        !newVal ? node.style.display = 'none' : node.style.display = 'block';
    })
}
// 处理v-if指令
Compiler.prototype.ifUpdater = function (node, value, key) {
    const nodew = node;
    const nodep = node.parentNode;
    if (!value) {
        node.parentNode.removeChild(node)
    }
    new Watcher(this.vm, key, (newVal) => {
        console.log(newVal);
        !newVal ? nodep.removeChild(node) : nodep.appendChild(nodew);
    })
}
// 处理v-on指令
Compiler.prototype.onUpdater = function (node, value, key, eventName) {
    if (eventName) {
        const handler = this.vm.$options.methods[key].bind(this.vm);
        node.addEventListener(eventName, handler);
    }
}
// 处理v-model指令
Compiler.prototype.modelUpdater = function (node, value, key) {
    node.value = value;
    new Watcher(this.vm, key, (newVal) => {
        node.value = newVal;
    })
    // 双向绑定,视图变化更新数据
    node.addEventListener('input', () => {
        this.vm[key] = node.value;
    })
}


Dep


// 发布者。
// 收集依赖,添加所有的观察者(watcher)。通知所有的观察者。
function Dep() {
    // 存储所有的观察者watcher
    this.subs = [];
}
// 添加观察者
Dep.prototype.addSub = function (sub) {
    if (sub && sub.update) {
        this.subs.push(sub);
    }
}
// 发送通知
Dep.prototype.notify = function () {
    this.subs.forEach(sub => {
        sub.update();
    })
}


Watcher


function Watcher(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    // 把当前watcher对象记录到Dep类的静态属性target
    Dep.target = this;
    if (vm.hasOwnProperty(key)) {
        this.oldVal = vm[key];
    } else {
        const str = `vm.${key}`;
        this.oldVal = eval(str);
    }
    Dep.target = null;
}
// 当数据发生变化的时候更新视图
Watcher.prototype.update = function () {
    let newVal;
    if (this.vm.hasOwnProperty(this.key)) {
        newVal = this.vm[this.key];
    } else {
        const str = `this.vm.${this.key}`;
        newVal = eval(str);
    }
    this.cb(newVal);
}


以上这几个构造函数就实现了我们所说的迷你版本,将它们整合成一个文件vuemini.js。在上面所提示的页面引入,查看效果。


微信截图_20220506155345.png


另外,我在data中绑定了一个html属性,值为一个'<div>{{msg}}</div>',与之前完整版相比,图中的v-html下方的maomin文本也被渲染出来。


尤大开发的Vue2.x迷你版本


下面,我们将看下尤大开发的迷你版本,这个版本引入了Virtual 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>vue2mini</title>
</head>
<body>
    <div id="app"></div>
    <script>
        // reactivity ---
        let activeEffect
        class Dep {
            subscribers = new Set()
            depend() {
                if (activeEffect) {
                    this.subscribers.add(activeEffect)
                }
            }
            notify() {
                this.subscribers.forEach(effect => effect())
            }
        }
        function watchEffect(effect) {
            activeEffect = effect
            effect()
            activeEffect = null
        }
        function reactive(raw) {
            // use Object.defineProperty
            // 1. iterate over the existing keys
            Object.keys(raw).forEach(key => {
                // 2. for each key: create a corresponding dep
                const dep = new Dep()
                // 3. rewrite the property into getter/setter
                let realValue = raw[key]
                Object.defineProperty(raw, key, {
                    get() {
                        // 4. call dep methods inside getter/setter
                        dep.depend()
                        return realValue
                    },
                    set(newValue) {
                        realValue = newValue
                        dep.notify()
                    }
                })
            })
            return raw
        }
        // vdom ---
        function h(tag, props, children) {
            return { tag, props, children };
        }
        function mount(vnode, container, anchor) {
            const el = document.createElement(vnode.tag);
            vnode.el = el;
            // props
            if (vnode.props) {
                for (const key in vnode.props) {
                    if (key.startsWith('on')) {
                        el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])
                    } else {
                        el.setAttribute(key, vnode.props[key]);
                    }
                }
            }
            if (vnode.children) {
                if (typeof vnode.children === "string") {
                    el.textContent = vnode.children;
                } else {
                    vnode.children.forEach(child => {
                        mount(child, el);
                    });
                }
            }
            if (anchor) {
                container.insertBefore(el, anchor)
            } else {
                container.appendChild(el);
            }
        }
        function patch(n1, n2) {
            // Implement this
            // 1. check if n1 and n2 are of the same type
            if (n1.tag !== n2.tag) {
                // 2. if not, replace
                const parent = n1.el.parentNode
                const anchor = n1.el.nextSibling
                parent.removeChild(n1.el)
                mount(n2, parent, anchor)
                return
            }
            const el = n2.el = n1.el
            // 3. if yes
            // 3.1 diff props
            const oldProps = n1.props || {}
            const newProps = n2.props || {}
            for (const key in newProps) {
                const newValue = newProps[key]
                const oldValue = oldProps[key]
                if (newValue !== oldValue) {
                    if (newValue != null) {
                        el.setAttribute(key, newValue)
                    } else {
                        el.removeAttribute(key)
                    }
                }
            }
            for (const key in oldProps) {
                if (!(key in newProps)) {
                    el.removeAttribute(key)
                }
            }
            // 3.2 diff children
            const oc = n1.children
            const nc = n2.children
            if (typeof nc === 'string') {
                if (nc !== oc) {
                    el.textContent = nc
                }
            } else if (Array.isArray(nc)) {
                if (Array.isArray(oc)) {
                    // array diff
                    const commonLength = Math.min(oc.length, nc.length)
                    for (let i = 0; i < commonLength; i++) {
                        patch(oc[i], nc[i])
                    }
                    if (nc.length > oc.length) {
                        nc.slice(oc.length).forEach(c => mount(c, el))
                    } else if (oc.length > nc.length) {
                        oc.slice(nc.length).forEach(c => {
                            el.removeChild(c.el)
                        })
                    }
                } else {
                    el.innerHTML = ''
                    nc.forEach(c => mount(c, el))
                }
            }
        }
        // paste all previous code from Codepen
        const app = {
            data: reactive({
                count: 0
            }),
            render() {
                return h('div', {
                    onClick: () => {
                        app.data.count++
                    }
                }, String(app.data.count))
            }
        }
        function mountApp(component, selector) {
            let isMounted = false
            let oldTree
            watchEffect(() => {
                if (!isMounted) {
                    mount(oldTree = component.render(), document.querySelector(selector))
                    isMounted = true
                } else {
                    const newTree = component.render()
                    patch(oldTree, newTree)
                    oldTree = newTree
                }
            })
        }
        mountApp(app, '#app')
    </script>
</body>
</html>


相关文章
|
2天前
|
存储 JavaScript 前端开发
搞懂Vue一篇文章就够了
搞懂Vue一篇文章就够了
9 0
|
2天前
|
JavaScript 前端开发 UED
Vue 异步组件
Vue 异步组件
10 0
|
2天前
|
JavaScript
Vue自定义指令的三个方法
Vue自定义指令的三个方法
7 0
|
2天前
|
存储 资源调度 JavaScript
vue引入vuex
vue引入vuex
25 7
|
2天前
|
JavaScript 前端开发
vue怎么自定义底部导航
vue怎么自定义底部导航
21 10
|
3天前
|
JavaScript 开发者 UED
Vue入门到关门之第三方框架elementui
ElementUI是一个基于Vue.js的组件库,包含丰富的UI组件如按钮、表格,强调易用性、响应式设计和可自定义主题。适用于快速构建现代化Web应用。官网:[Element.eleme.cn](https://element.eleme.cn/#/zh-CN)。安装使用时,需在项目中导入ElementUI和其样式文件。
|
3天前
|
Web App开发 缓存 JavaScript
优化Vue首页加载速度的实用方法
优化Vue首页加载速度的实用方法
13 1
|
3天前
|
JavaScript 索引
vue 在什么情况下在数据发生改变的时候不会触发视图更新
vue 在什么情况下在数据发生改变的时候不会触发视图更新
15 2
|
3天前
|
存储 缓存 JavaScript
vue中性能优化
vue中性能优化
12 1
|
3天前
|
JavaScript
vue常用指令
vue常用指令
12 1