总览

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2022-02-19

记录尤雨溪公开课笔记 视频地址

MVVM

所有视图渲染系统的高度抽象形式都是

onStateChanged(() => {
  view = render(state);
})

// render 也相当于一个更新函数 我们将更新函数放在外面,要求用户不能随便调用 必须调用特定函数(setState)变更状态
// 则换为这种写法
let update;
const onStateChanged = (_update) => {
  update = _update; // 发生变化的时候,指定更新函数
}

// setState 只负责把旧状态替换为新状态,然后调用更新函数
const setState = (newState) => {
  state = newState; 
  update();
}

函数组件

普通组件支持实例化,但是函数组件不支持实例化。**函数组件可以理解为一个函数,它们接受参数然后返回一个虚拟DOM。**它不拥有任何状态,实际上,在Vue虚拟DOM实现中,函数组件更容易扩展,如果你在父组件中使用函数组件,这个函数组件的 render 函数会比父组件的 render 函数更早调用,所以这里会耗费一点资源,但是非常少,因为它没有创建数据实例。

如果你的组件只是简单接受数据进行展示,接受 props 然后根据 props 进行渲染,那么使用函数组件是适合的。如果这些组件在很多地方复用了,把它们转成函数组件,可以提升应用的性能。

Vue.component('example', {
  props: ['tags'],
  functional: true,
  render(h, context) {

    // 第二个参数如果是数组会被当做children,API 设计的是接受属性对象 eg. { class: 'my-div' }
    // 因为是函数式的,所以不能访问this实例,需要用 h 函数的第二个参数, context 代表函数渲染上下文
    return h('div', context.props.tags.map((tag, i) => h(tag, i))) 

    // 也可以通过 context.slots() 拿到函数式组件的插槽信息,与 this.$slots 效果一样

    // 通过 context.parent 拿到所在组件的根实例,函数组件被用在哪个组件(.vue文件)内,parent 就指向这个组件的根实例。
  }
})

高阶组件

接收一个组件作为参数,并返回一个新的被增强的组件。

<script src="../node_modules/vue/dist/vue.js"></script>

<div id="app">
  <!-- 使用,传参 username -->
  <smart-avatar username="vuejs"></smart-avatar>
</div>

<script>
function fetchURL (username, cb) {
  setTimeout(() => {
    cb('https://avatars3.githubusercontent.com/u/6128107?v=4&s=200')
  }, 500)
}

// 组件
const Avatar = {
  props: ['src'],
  template: `<img :src="src">`
}

// 增强组件
function withAvatarURL (InnerComponent) {

  // 你甚至可以在这里缓存用户名和URL,这样就能避免重复请求了 并直接设置data中

  return {
    props: {
      username: String
    },
    data () {
      return {
        url: 'http://via.placeholder.com/200x200' // 占位图
      }
    },
    created () {
      // 1、获取图片,没有就是显示占位符
      fetchURL(this.username, (url) => { this.url = url })
    },
    render (h) {
      // 2、渲染传入的组件,并给其传 props
      // 增强的功能就是:在图片获取到之前都显示占位图。
      return h(InnerComponent, { props: { src: this.url } })
    }
  }
}

const SmartAvatar = withAvatarURL(Avatar)

new Vue({
  el: '#app',
  components: { SmartAvatar }
})
</script>

使用高阶组件并不会影响你的父组件,子组件。是一种很好的封装机制,父子之间的通讯只通过 props,这样可以确保在此内部实现中进行增强,不会影响代码中的其他部分,如果你有一个庞大的项目,这就很重要了,你需要确保你的改动不会影响别人的代码,不会破坏其他东西,诀窍是,确保这段代码与代码库其他部分有最小的耦合。在这种情况下,唯一与外部连接的部分就是 props,它很小概率会破坏别人的代码。

也可以进一步的封装,将获取用户信息的API也通过参数来传入,上面的示例中,通过 username 来获取用户的头像信息,你可以传入别的API,获取用户的别的信息。

function withAvatarURL(InnerComponet, fetchApi) {
  // ...
}

在高阶组件中,你也可以将插槽继续传给 InnerComponent

render (h) {
  return h(InnerComponent, {
    props: { 
      src: this.url,
    }
  }, this.$slots.default) 
  // 通过this.$slots.default 拿到默认插槽,通过 this.$slots.foo 拿到具名插槽 foo
}

当你使用高级组件进行 attribute。 进行传递的时候

<smart-avatar username="vuejs" id="foo"></smart-avatar>

这个 id 属性并不会传递给 InnerComponent, 因为我们内部没有明确定义它, 但是可以通过 this.$attrs 拿到上层传递的 attribute。

render (h) {
  return h(InnerComponent, {
    props: { 
      src: this.url,
      attrs: this.$attrs, // 这样id就会在 InnerComponent 中渲染
    }
  }, this.$slots.default)
}

状态管理

Flux设计模式就像眼镜,你知道何时需要它,如果你看到的一切都很好,那么你可能不需要它,但是一旦你觉得有点问题,那你就需要它,这是一个自然的过程。

不同组件都需要同样的数据,但是又不是父子这样直接的关系,那么,如果数据是应用中多个组件共享的,这些数据需要提取出来集中管理。

在 options API 中,数据必须是函数的原因是因为大多数时候,我们希望每个组件实例都有自己独立的唯一的数据,而不是所有这些组件都共享相同的数据,恰恰相反,但是在状态管理下,我们要实现的就是使它们共享同一条数据。

const state = {
  count: 0
}

const Counter = { // 组件
  data () {
    return state
  },
  render(h) {
    return h('div', this.count) // 因为 state 就是返回的对象,就可以将count通过this访问
  }
}

new Vue({
  el: '#app',
  components: {
    Counter,
  },
  methods: {
    inc() {
      state.count++; // 注意,这里是 state.count++,因为vue将state转换成了响应式对象
    },
  }
})

这样,所有的 Counter 组件中的 count 就是共享了同一个数据,当 inc 被调用后,所有 Counter 组件中使用的 count 数据都将自增1。

上面的 state 我们可以直接使用 Vue 实例来代替:

const state = new Vue({
  data: {
    count: 0
  },
  methods: {
    inc() {
      this.count++;
    }
  }
})

const Counter = {
  render: h => h('div', state.count)
}

new Vue({
  el: '#app',
  components: {
    Counter,
  },
  methods: {
    inc() {
      state.inc() 
      // Vue会将data/methods/computed/inject/provide都挂载到实例上
      // 所以可以通过 state.inc 调用来改变 state.count 
      // 这样子用起来就非常接近Vuex了
    },
  }
})

这样与上一例中的代码效果一致,我们同样可以通过 state.count 来访问被 Vue 处理成响应式数据了的 count,这里的 data 将不再是一个函数返回一个对象,而就是一个对象,方便被需要的组件共享。

由此,我们可以来模拟一个简易版的 Vuex:

function createStore({ state, mutations}) { // 等同于 new Vuex.store()
  return new Vue({
    data: { state },
    methods: {
      commit(type, ...args) {
        mutations[type](this.state, ...args)
      }
    }
  })
}

const store = createStore({
  state: { count: 0 },
  mutations: {
    inc(state) {
      state.count++;
    }
  }
} )

new Vue({
  el: '#app',
  components: {
    Counter,
  },
  methods: {
    inc() {
      store.commit('inc')
    },
  }
})

路由

vue-router hash模式的基础就是基于浏览器API进行hash监听。当通过vue-router调用 this.$router.push 的时候,是通过 window.location.hash 来改变页面的hash的,然后根据 this.$router.push的参数来更新路由渲染组件(页面),由于向服务器发请求的时候会忽略#后面的值,所以hash改变也不会再次发请求

window.addEventListener('hashchange', () => {
  // ...
})

而 history 模式,就是通过 window.history API 来完成,这通常需要 nginx 的配置,因为我们的一些路由在服务器上是没有对应的资源的,当在 Vue应用中,请求的不是首页,那么浏览器会向服务器发送请求,而此时如果 nginx 没有配置,就会返回404。

所以需要在 nginx 中做一些配置,请求任何路径(比如 www.xxx.com/a/b)的时候,都会转到 index.html,然后加载代码,此时 vue-router 的代码已经存在,拿到当前页面的 url,然后 vue-router根据路由匹配,渲染出对应的组件(页面)。

当用户通过浏览器动作(点击浏览器回退或前进按钮的时候)也会改变URL,此时会触发一个 popstate 事件,vue-router同样会监听这个事件,做出反应。

当在采用了history 模式的Vue应用中调用 this.$router.push() 的时候,会根据路传入的路径参数匹配路由,渲染页面,而浏览器URL的改变是在vue-router中用 window.history.pushState 这个API来改变的,它会改变浏览器的当前URL,但是不会发送请求到服务器。同样的 this.$router.replace 也会根据路径参数匹配路由,改变url的实现就是对应的 window.history.replaceState

所以这两者都可以做到,更新视图但不重新请求页面。

关于hash模式的进一步了解

<!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>
  <script src="https://lib.baomitu.com/vue/2.6.12/vue.js"></script>
</head>
<body>
  <div id="app">
    <component :is="url"></component>
    <a href="#foo">foo</a>
    <a href="#bar">bar</a>
  </div>
  <script>
    window.addEventListener('hashchange', () => {
      app.url = window.location.hash.slice(1) 
      // 当hash改变式,主动改变了根实例的data属性url,这也会被对应的Watcher监听到
      // 从而做出反应,更新应用
    })
  
    const app = new Vue({
      el: '#app',
      data: {
        url: window.location.hash.slice(1),
        // 需要在应用中将URL保存为响应式的数据,在根实例中通过url的data属性来记录当前url
      },
      components: {
        foo: { template: `<div>我是组件foo</div>` },
        bar: { template: `<div>我是组件bar</div>` },
      }
    })
  </script>
</body>
</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>
  <script src="https://lib.baomitu.com/vue/2.6.12/vue.js"></script>
</head>
<body>
  <div id="app"></div>
  <script>
    const Foo = { template: `<div>我是组件foo</div>` };
    const Bar = { template: `<div>我是组件bar</div>` };
    const NotFound = { template: `<div>Not Found !!</div>` };

    const routeTable = {
      'foo': Foo,
      'bar': Bar,
    }

    window.addEventListener('hashchange', () => {
      app.url = window.location.hash.slice(1) 
    })
  
    const app = new Vue({
      el: '#app',
      data: {
        url: window.location.hash.slice(1),
      },
      render(h) {
        return h('div', [
          h(routeTable[this.url] || NotFound),
          h('a', { attrs: { href: '#foo' }}, '我是组件foo'),
          ' | ',
          h('a', { attrs: { href: '#bar' }}, '我是组件bar'),
        ])
      }
    })
  </script>
</body>
</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>
  <script src="https://lib.baomitu.com/vue/2.6.12/vue.js"></script>
  <script src="./path-to-regexp.js"></script>
  <!-- 示例的github地址 https://github.com/zhengguorong/vue-advanced-workshop -->
  <!-- path-to-regexp: https://github.com/zhengguorong/vue-advanced-workshop/blob/master/5-routing/path-to-regexp.js -->
</head>
<body>
  <div id="app"></div>
  <script>
    const Foo = {
      props: ['id'],
      template: `<div>我是组件foo id: {{ id }}</div>` 
    };
    const Bar = { template: `<div>我是组件bar</div>` };
    const NotFound = { template: `<div>Not Found !!</div>` };

    const routeTable = {
      '/foo/:id': Foo,
      '/bar': Bar,
    }

    const compliedRoutes = [];
    Object.keys(routeTable).forEach(path => {
      const dynamicSegments = [];
      const regex = pathToRegexp(path, dynamicSegments)
      // 调用之后会改变 dynamicSegments 如下demo
      // var keys = [];
      // var re = pathToRegexp('/foo/:bar', keys);
      // re => /^\/foo\/([^\/]+?)\/?$/i
      // keys => [{ name: 'bar', delimiter: '/', repeat: false, optional: false }]

      const component = routeTable[path]
      
      compliedRoutes.push({
        component,
        regex,
        dynamicSegments
      })
    })

    window.addEventListener('hashchange', () => {
      app.url = window.location.hash.slice(1) 
    })
  
    const app = new Vue({ 
      el: '#app',
      data: {
        url: window.location.hash.slice(1),
      },
      render(h) {
        const path = '/' + this.url

        let componentToRender;
        let props = {};

        compliedRoutes.some(route => {
          const match = route.regex.exec(path)
          //  match: ["/foo/123", "123", index: 0, input: "/foo/123", groups: undefined]
          
          componentToRender = NotFound
          
          // 当前路由能与路由表中的某个路由匹配上
          if (match) {
            componentToRender = route.component; // 赋值对应的组件

            // dynamicSegments => [{name: "id", prefix: "/", delimiter: "/", optional: false, repeat: false, …}] 
            route.dynamicSegments.forEach((segment, index) => {
              props[segment.name] = match[index+1]; // 赋值对应的 props
            })

            return true
            // 使用some是因为只需要匹配到就可以返回 true,结束循环
          }
        })

        return h('div', [
          h(componentToRender , { props }),
          h('a', { attrs: { href: '#foo/123' }}, '我是组件foo id是123'),
          ' | ',
          h('a', { attrs: { href: '#foo/234' }}, '我是组件foo id是234'),
          ' | ',
          h('a', { attrs: { href: '#bar' }}, '我是组件bar'),
        ])
      }
    })
  </script>
</body>
</html>