Last active
July 23, 2019 18:07
-
-
Save DaiwenZh5/b81de8014da8cb638a9efee4d95c7ed6 to your computer and use it in GitHub Desktop.
#md #test
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 目的 | |
在子组件渲染完成之后,父组件可以及时获取 dom 进行操作。 | |
# 描述 | |
这里是因为我在获取 markdown 渲染之后的文章目录时,无法及时更新视图, | |
因为其执行过程是在组件渲染完成之后,将 markdown 渲染的文本通过 | |
`innerHTML` 置入 dom 中,因此需要在组件渲染完成,且将文件放置到 dom 之后触发事件。 | |
# 尝试 | |
## 组件生命周期钩子 | |
1. 在父组件中订阅子组件生命周期: | |
`@hook:mounted="rendered"`,该方法等同与子组件的`mounted`方法中发布事件,提供外部访问子组件`mounted`生命周期,但向比较而言,`@hook`提供了无入侵式方法。 | |
2. 在父组件`mounted`方法中操作,因为子组件是先于父组件渲染完成的,为了确保无误,可以使用`this.$nextTick`方法来明确操作执行与 DOM 更新之后。 | |
```vue.js | |
async mounted() { | |
// DOM 更新前 | |
await this.$nextTick(); | |
// DOM 更新后 | |
} | |
``` | |
## 结果 | |
只能确保视图渲染完成,DOM 存在,但是不能确保此时已经完成`innerHTML`操作。 | |
# 解决 | |
`innerHTML`插入完成之后使用`$emit`方法发布事件,在订阅事件中进行 DOM 操作。确保两点: | |
1. `innerHTML`之后插入,确保了 DOM 操作的执行时机 | |
2. `$emit`方法虽然执行于父组件,但是完成的依旧是子组件事件,该方法封装一套逻辑执行于子组件发布的位置,从而保证了执行顺序的无误。 | |
# 方法:修改子组件 | |
需要完成两点: | |
1. 获取 DOM: | |
- `ref`属性 | |
- 自定义指令 | |
2. 发布自定义事件 | |
## `ref`属性 | |
在组件上定义该属性,即可使用`this.$refs`来获取 DOM | |
```vue.js | |
//- 在子组件上指定 ref 值 | |
ref="markdown-it-vue-container" | |
// 在 innerHTML 执行之前插入 | |
this.init(this.$refs["markdown-it-vue-container"]) | |
``` | |
## 自定义指令 | |
在组件上绑定自定义指令 | |
```vue,js | |
//- 子组件 render 指令绑定 init 函数 | |
v-render="init" | |
// render 指令此处用了方法简写,其意味着 bind、update 函数一致 | |
directives: { | |
render(el, binding) { | |
// value 的值为函数,用于暴露 dom,方便组件操作 | |
binding.value(el); | |
} | |
}, | |
``` | |
## 发布自定义事件 | |
```vue.js | |
methods: { | |
init(el) { | |
// 子组件自身需要执行的操作 | |
el.innerHTML = this.md.render(this.content); | |
// 暴露给父组件的方法 | |
this.$emit("rendered", el); | |
} | |
}, | |
``` | |
## 订阅自定义事件 | |
父组件需要进行的内容: | |
```vue.js | |
//- 父组件订阅事件,并指定方法名为 rendered | |
//- 需要注意的是,这里的发布的事件名与订阅事件名一致或可能出现冲突 | |
@rendered="rendered" | |
methods: { | |
// el 是在自定义指令中作为参数传递的 | |
// init 方法 -> rendered 事件 | |
rendered(ref) { | |
// do anything you want | |
// - 下面是我的应用 | |
// getCatalog 是用于获取 dom 内的文章目录 | |
// 因此需要子组件渲染之后执行 | |
let { levels, noLevels } = getCatalog(ref); | |
this.catalog = { | |
levels, | |
noLevels | |
}; | |
} | |
}, | |
``` | |
# 问题 | |
尽管完成效果,但是由于通过在子组件内部发布自定义事件来实现的,因此对于一些封装好的组件无能为力,无法做到无侵入式切入。 | |
但是通过 AOP 的思想,可以在组件被调用前,对其进行切面。 | |
# 方法:面向切面编程(AOP) | |
## 组件改装,加入切面 | |
在网上粘过来的 `after`方法 | |
```js | |
Function.prototype.after = function(action) { | |
//保留当前函数环境 | |
var func = this; | |
// return 被包装过的函数,这里就可以执行其他功能了。 | |
// 并且该方法挂在Function.prototype上, | |
// 被返回的函数依然具有after属性,可以链式调用 | |
return async function() { | |
// 原函数执行 | |
// 这是里用了异步,是因为切片所在的函数也是异步的 | |
var result = await func.apply(this, arguments); | |
// 执行之后的操作,after 方法是否异步不重要,其必然在原方法之后执行 | |
await action.apply(this, arguments); | |
// 将执行结果返回 | |
return result; | |
}; | |
}; | |
``` | |
对组件进行分析,跑一边调试、打印,定位到组件的 watch,该组件是用于渲染 markdown 文档的,其对 content 字符串进行监听,一旦改动则使用 markdown-it 对其渲染成 html 字符串,然后将其插入 dom 中。 | |
```vue.js | |
// 切面,这里只装载了 after 方法 | |
// 需要在引入组件之前进行 | |
MarkdownItVue.watch.content.handler = MarkdownItVue.watch.content.handler.after( | |
async function() { | |
console.log("1 - aop"); | |
// console.log(this.$el) | |
setTimeout(() => { | |
// 同样需要提供父组件调用,因此要发布事件 | |
this.$emit("aopRendered"); | |
}, 20); | |
} | |
); | |
``` | |
## 订阅事件 | |
本质上还是在子组件中发布了自定义事件,父组件中同样需要订阅。 | |
```vue.js | |
methods: { | |
rendered() { | |
console.log("2 - rendered"); | |
const el = this.$refs["markdown"].$el; | |
console.log(el); | |
let { levels, noLevels } = getCatalog(el); | |
this.catalog = { | |
levels, | |
noLevels | |
}; | |
} | |
}, | |
``` | |
## 结果 | |
执行结果符合预期:订阅事件生效,且在 after 方法之后执行 | |
![](https://daiwenzh5.github.io/post-images/1563736529159.png) | |
# 方法:钩子函数 | |
实际上也是 AOP 思想,同样需要在组件使用前进行改写。 | |
```vue.js | |
// 要使用该方法,执行将此处的方法名,即等式左边替换掉即可 | |
MarkdownItVue.watch.content.handler = (function(fn) { | |
// 返回包装的函数,运行时将包装函数this传入原函数 | |
return async function() { | |
console.log("1 - 钩子函数"); | |
console.log("执行之前"); | |
const result = await fn.apply(this, arguments); | |
console.log("执行之后"); | |
this.$emit("aopRendered"); | |
return result; | |
}; | |
// 等式从右边执行,因此可以直接使用原函数,无需缓存 | |
})(MarkdownItVue.watch.content.handler); | |
``` | |
其余代码和 AOP 中一致。 | |
## 结果 | |
执行结果也是符合预期的,当然执行之中的由于没有改动子组件自然是没法配合了,不过订阅的事件也同样生效了,即拓展成功。 | |
![](https://daiwenzh5.github.io/post-images/1563736700356.png) | |
# 总结 | |
1. 1. 子组件通过`$emit`发布事件能够与父组件进行通信 | |
2. AOP 编程可以无侵入式的对组件进行拓展,上面之所以区分,是因为 aop 方法提供了 before、after、around 三种切面,更规范一些。这里将 aop 拓展在了 `Function`方法上,而事实上,对于单个函数拓展,自然是使用钩子函数来的更轻松一些,只作用于目标函数,用完即走,只进入身体、不进入生活,潇洒地惹人喜爱。 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment