Skip to main content
  1. Posts/

关于Vue的深刻以及不深刻理解

·2 分钟

什么是MVVM #

MVVM是一种视图模型双向绑定的设计模式,是Model-View-ViewModel的缩写,是从MVC模式演变过来的。

Model层代表数据模型,View代表UI组件,ViewModel是前两者之间的桥梁,数据会绑定到ViewModel层,并将数据渲染到页面上,视图变化时会通知ViewModel更新数据。

以前是操作DOM结构更新视图,现在是数据驱动视图

MVVM的优点 #

  • 低耦合:在MVC模式中View强依赖Model,导致View无法进行组件化设计;现在View可以独立于Model变化和修改,一个Model可以绑定到不同View
  • 可重用性:我们可以吧一些视图逻辑放在一个Model里,让很多View重用这段视图逻辑
  • 独立开发:开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员就专注页面设计
  • 可测试:在MVC中,View需要UI环境,因此依赖ViewController测试会变得很困难

Vue的底层逻辑 #

Vue是个典型的MVVM框架,它实现了数据与视图的双向绑定。

它是采用数据劫持结合发布订阅者模式来实现这一逻辑的。

Observer #

数据监听器,主要功能是对数据进行劫持,核心功能是通过Object.defineProperty()方法实现的,每当获取数据时,会触发方法内部的getter函数,将订阅者(Watcher)添加到订阅器(Dep)中;当数据发生变化的时候就触发setter函数,通知订阅者进行视图更新。

Watcher #

订阅者,作为CompileObserver之间通信的桥梁,主要的职责是:

  • Compile运行时实例化自身,生成需要传递给update()方法的回调函数
  • Observer监听到获取数据时,往Dep中添加自己的实例
  • 自身实例都会有一个update()方法,数据发生变化的时候,Dep.notify()给订阅者发布消息,触发update()方法

Compile #

指令解析器,就是用来解析模板指令的,主要的工作是:

  • 把模板中的变量替换成响应的数据,然后初始化渲染页面视图。
  • 生成订阅者更新视图时需要调用的回调函数
  • 给每个指令对应的节点绑定更新函数,实现数据的双向绑定。

Vue的生命周期 #

对于生命周期,官方文档是这么解释的:

每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

依然是官方给的生命周期图示:

生命周期
红字部分就是生命周期钩子,可以很清晰地看出它们执行的先后顺序。 文字描述这个过程大概就是:

  • 创建阶段
    • beforeCreat:创建前,data和methods中的数据都还没初始化
    • created:创建完,数据初始化完成,能够获取到data中的值
  • 编译阶段
    • beforeMount:进行模板编译
    • mounted:将数据挂载到视图上,可以获取和操作DOM
  • 更新阶段
    • beforeUpdate:监测到数据的变化,触发虚拟DOM的重新渲染
    • updated:视图更新完成
  • 销毁阶段
    • beforeDestroy:实例销毁前,可以手动销毁一些方法
    • destroyed:销毁后

组件生命周期 #

父组件beforeCreate –> 父组件created –> 父组件beforeMount –> 子组件beforeCreate –> 子组件created –> 子组件beforeMount –> 子组件 mounted –> 父组件mounted –>父组件beforeUpdate –>子组件beforeDestroy–> 子组件destroyed –> 父组件updated

规律就是,子组件在父组件数据挂载前才开始初始化,直到子组件挂载完毕父组件才挂载完毕;子组件的更新或者销毁都会触发父组件更新,直到子组件更新或销毁完毕,父组件才更新完毕;在销毁阶段同理,得在子组件销毁完毕后父组件才会触发destroyed钩子函数。

computed和watch的区别 #

watch:是一个对象,键是需要监听已在data中定义的属性,值是对应的回调函数。当监听的属性发生变化时,触发watch中的方法,通常需要执行异步或者开销较大的操作时使用

computed:计算属性是用来声明式的描述一个值依赖了其它的值,computed中的函数必须用return返回结果。只有当它依赖的属性发生变化时,才会调用函数重新计算,否则会直接从缓存中读取结果。

所以相比watch来说,computed更加高效,如果二者都能实现的功能,推荐优先使用computed。

如何实现一个computed #

computed的用法是这样的

data: () {
    return {
        a: 1,
        b: 2,
        name: 'helene'
    }
},
computed: {
    sum() {
        return this.a + this.b
    },
    myName: this.name
}

computed里的值可能是个函数,也可能是个对象,computed函数就能这么写

// 将vue实例传进来
function initComputed(vm) {
    // 获取到computed对象
    const computed = vm.$options.computed
    // 听过Object.keys()方法将computed对象的key转化成数组
    Object.keys(computed).forEach(key => {
        // 数据代理,获取方法时不用写成vm.$options[key],直接vm[key]就能获取了
        Object.defineProperty(vm, key, {
            /**
             * 判断computed里的值是对象还是函数,
             * 是函数就直接运行,读取依赖的值时会自动调用getter方法;
             * 是对象就手动调用get方法
            */
            get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
            set() {}
        })
    })
}

组件中的data为什么是一个函数 #

一个组件可以复用多次,这样会创建出多个实例,但本质都是由一个构造函数创建的。如果data是一个对象的话,那么data就是一个引用类型,当我们修改一个实例的data属性时,其它实例的属性会跟着一起变化。为了防止数据污染,data必须是一个函数,这样里面的属性就有自己的作用域了。

vue的路由原理 #

首先,为什么要使用路由? 众所周知,vue生成的页面是单页面(SPA)。服务器上只有一个由前端生成的index.html静态文件,用户在页面上点击操作不会触发http请求新的静态文件index.html,但依然可以做到局部刷新数据,让用户体验到反馈,这就是路由的作用。

路由有两种模式,hash模式和history模式。

hash模式 #

hash模式URL长这样

  • 首页:yourdomain.xxx.com/index.html/#/
  • 博文:yourdomain.xxx.com/index.html/#/posts #号后面的内容就是hash部分,不同显示内容对应的hash不同,路由就是通过监听hash的变化进行相应的内容渲染的。 实现hash模式的核心就是浏览器暴露给开发者的hashchange方法。
class VueRouter {
    constructor(routes = []) {
        // 获取路由映射
        this.routes = routes
        this.currentHash = ''
        this.refresh = this.refresh.bind(this)
        // 监听加载和路由变化事件,调用refresh方法
        window.addEventListener('load', this.refresh, false)
        window.addEventListener('hashchange', this.refresh, false)
    }
    // 获取hash值
    getUrlPath(url) {
        return url.indexOf('#') >= 0 ? url.slice(url.indexOf('#') + 1) : '/'
    }
    refresh(event) {
        let newHash = ''
        let oldHash = null
        if (event.newURL) {
            oldHash = this.getUrlPath(event.oldURL || '')
            newHash = this.getUrlPath(event.newURL || '')
        } else {
            newHash = this.getUrlPath(window.location.hash)
        }
        // 记录当前路由的hash值
        this.currentHash = newHash
        // 通过hash值获取相应组件内容
        this.matchComponent()
    }

    matchComponent() {
        // 找到路由映射中与当前hash匹配的路由数据
        let currentRoute = this.routes.find(route => route.path === this.currentHash)
        if(!currentRoute) {
            // 如果没有数据匹配就返回到首页(根据具体场景可能有所不同
            currentRoute = this.routes.find(route => route.paht === '/')
        }
        // 获取到组件数据
        const { component } = currentRoute
        // 将数据挂载到DOM上
        documnet.querySelector('#content').innerHTML = component
    }
}

const router = new VueRouter([
    {
        path: '/',
        name: 'home',
        component: '<div>首页</div>'
    }, {
        path: '/',
        name: 'posts',
        component: '<div>博文</div>'
    }
])

从代码可以看出,hash模式可以完全由前端实现。

history模式 #

history模式的核心是HTML5提供的一个history全局对象,它记录了用户在访问页面时留下的历史会话信息,同时这个对象中的一些方法可以对浏览器历史会话进行操作。

  • window.history.go:可以跳转到浏览器历史会话中指定的某一个记录页
  • window.history.forward:跳转到历史会话中的下一页
  • window.history.back:跳转到历史会话中的上一页
  • window.history.pushState:将数据压入历史回话栈中
  • window.hisotry.replaceState:将当前回话页信息替换成指定数据 其实不论hash模式还是history模式,我们都是通过监听url的变化来实现数据更新的。

通过上面对history方法的介绍可以看出来,如果我们能够监听到pushStatereplaceState事件的触发,就能够更新页面数据了。但这两个方法只能改变URL却并不能触发页面的刷新,这是为什么?

我们还要了解window在处理历史会话发生变化时触发的事件popstate,它只会在浏览器行为下触发,比如点击前进、后退键,history.replaceState()history.pushState无法触发popstate

那我们只能手动将这两个方法的事件监听器注册到window上了。

let _wr = function(type) {
    let orig = history[type]
    return function() {
        // 改变原始方法的this指向
        let rv = orig.apply(this, arguments)
        // 初始化事件
        let e = new Event(type)
        // 将原方法调用的参数传给新创建的事件对象
        e.arguments = arguments
        // 将事件派发给window,当事件调用时,会触发EventListener
        window.dispatchEvent(e)
        return rv
    }
}
 history.pushState = _wr('pushState')
 history.replaceState = _wr('replaceState')

然后我们就可以在监听回调中处理页面渲染了,思路和hash的实现大同小异。

但history模式有个问题,一旦刷新,页面会报404。 因为没刷新,不论我们怎么通过pushState改变URL,都是访问默认路径yourdomain.xxx.com下的index.html文件,刷新后就变成访问yourdomain.xxx.com/posts下的index.html文件了,但服务器上并没有这个路径资源。这种时候就需要配置nginx代理服务器,告诉服务器如果路径资源不存在,默认指向静态资源index.html