本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2022-02-19
记录尤雨溪公开课笔记 视频地址
所有视图渲染系统的高度抽象形式都是
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
。
所以这两者都可以做到,更新视图但不重新请求页面。
<!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>