来源于尤雨溪在Frontend Masters的课程(盗版女自学的一生:书是盗版的,软件是盗版的,课也听的盗版的……谢谢海盗党:D
1. Reactivity
与React中通过调用setState()更新状态不同,Vue中的state对象本身是响应式的:通过调用Object.defineProperty改写了所有属性的getter和setter对象。
onStateChanged(() => {
view = render(state)
})
setState({a: 5}) // React中的更新方式
state.a = 5 // Vue中的更新方式
Vue2文档对 深入响应式原理 说明如下:
当你把一个普通的 JavaScript 对象传入 Vue 实例作为
data
选项,Vue 将遍历此对象所有的 property,并使用Object.defineProperty
把这些 property 全部转为 getter/setter。Object.defineProperty
是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 getter/setter 的格式化并不同,所以建议安装 vue-devtools 来获取对检查数据更加友好的用户界面。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
下面简单实现一下响应式原理:
1.1 getter/setter
实现效果:
const obj = { foo: 123 }
convert(obj)
obj.foo // should log: 'getting key "foo": 123'
obj.foo = 234 // should log: 'setting key "foo" to: 234'
obj.foo // should log: 'getting key "foo": 234'
实现方式:利用Object.defineProperty()以及外部变量记录当前的值
function convert (obj) {
if(!Object.prototype.toString.call(obj) === '[object Object]') return TypeError
for(let key of obj){
let currentVal = obj[key]
Object.defineProperty(obj, key, {
get: function(){
console.log(`getting key "${key}": ${currentVal}`)
return currentVal
},
set: function(val){
console.log(`setting key "${key}" to: ${val}`)
currentVal = val
}
})
}
}
1.2 dependency tracking
实现效果:
const dep = new Dep();
autorun(() => {
dep.depend();
console.log("updated");
});
// should log: "updated"
dep.notify();
// should log: "updated"
实现方式:
无论在什么样的计算中,我们总是会有多个依赖项,那么首先把依赖项抽象成一个类,每个依赖项都是这个类的实例。 每个实例需要有两个方法,分别是 depend() 和 notify()。depend()的作用是将该依赖项所在的所有函数记录下来, notify()的作用是在该依赖项变化时, 将之前存储的函数全部运行一次。
也就是实现一个订阅发布模式。值得注意的是,这里用 activeUpdate全局变量和wrappedUpdate方法 去获取正在执行的更新函数。
// a class representing a dependency
// exposing it on window is necessary for testing
window.Dep = class Dep {
constructor () {
this.subscribers = new Set()
}
depend () {
if (activeUpdate) {
// register the current active update as a subscriber
this.subscribers.add(activeUpdate)
}
}
notify () {
// run all subscriber functions
this.subscribers.forEach(subscriber => subscriber())
}
}
let activeUpdate
function autorun (update) {
function wrappedUpdate () {
activeUpdate = wrappedUpdate
update()
activeUpdate = null
}
wrappedUpdate()
}
1.3 mini-observer
实现效果:
const state = {
count: 0
}
observe(state)
autorun(() => {
console.log(state.count)
})
// should immediately log "count is: 0"
state.count++
// should log "count is: 1"
实现方式:
当读取 state.a 或者 state.b 时,在 get() 方法中触发 depend(); 当给 state.a 或 state.b 赋值时,在 set()中触发 notify()。由此,我们将1.1的 convert()函数修改一下:
function observe (obj) {
if(typeof obj !== 'object') throw new TypeError()
for(let key in obj){
let currentVal = obj[key]
let dep = new Dep()
Object.defineProperty(obj, key, {
get: function(){
dep.depend()
return currentVal
},
set: function(newVal){
let isChanged = newVal !== currentVal
if(isChanged){
currentVal = newVal
dep.notify()
}
}
})
}
}
window.Dep = class Dep {
constructor(){
this.subscribers = new Set()
}
depend(){
if(activeUpdate){
this.subscribers.add(activeUpdate)
}
}
notify(){
this.subscribers.forEach(sub => sub())
}
}
let activeUpdate
function autorun (update) {
function wrappedUpdate(){
activeUpdate = wrappedUpdate
update()
activeUpdate = null
}
wrappedUpdate()
}
2. Plugin
const vm = new Vue({
data: { foo: 10 },
rules: {
foo: {
validate: value => value > 1,
message: 'foo must be greater than one'
}
}
})
vm.foo = 0 // should log: "foo must be greater than one"
const RulesPlugin = {
install(vue){
vue.mixin({
created(){
if(this.$options.rules){
const rules = this.$options.rules
Object.keys(rules).forEach(key => {
this.$watch(key, newValue => {
const result = rules[key].validate(newValue)
if(!result) console.log(rules[key].message)
})
})
}
}
})
}
}
Vue.use(RulesPlugin)
3. Render function
3.1 Virtual DOM
benefit:
- (Essentially) A lightweight JavaScript data format to represent what the actual DOM should look like at a given point in time
- Decouples rendering logic from the actual DOM - enables rendering capabilities in non-browser environments e.g. server-side and native mobile rendering
3.2 Render function
Render Function: A function that returns Virtual DOM
Template -> [ Compiler ] -> Render Function :可通过 Vue Template Explorer 来查看编译过程
3.3 Templates v.s. JSX
same: both declaring the relationship between the DOM and the states
diff: templates is just a more static and more constraining form of expression (which allowing better compile time optimizations)
JSX render functions more dynamic
3.4 代码实现
Vue 提供了一个 h()
函数用于创建 vnodes:h()
是 hyperscript 的简称——意思是“能生成 HTML (超文本标记语言) 的 JavaScript”。这个名字来源于许多虚拟 DOM 实现默认形成的约定。
3.4.1 render-tags
render-tags: 普通组件(即有状态组件)写法
<div id="app">
<example :tags="['h1', 'h2', 'h3']"></example>
</div>
which renders the expected output:
<div>
<h1>0</h1> // take index as content
<h2>1</h2>
<h3>2</h3>
</div>
<script>
Vue.component('example', {
props: ['tags'],
render: function(h){
return h('div', this.tags.map((tag, index) => h(tag, index)))
}
})
new Vue({ el: '#app' })
</script>
render-tags: 函数式组件写法
<script>
Vue.component('example', {
functional: true, // 表示函数式组件
props: {
tags: {
type: Array,
validator (arr) { return !!arr.length }
}
},
render: (h, context) => { // 在context对象中访问原this实例内容
const tags = context.props.tags
return h('div', context.data, tags.map((tag, index) => h(tag, index)))
}
})
new Vue({ el: '#app' })
</script>
3.4.2 render-component:
<div id="app">
<example :ok="ok"></example>
<button @click="ok = !ok">toggle</button>
</div>
<script>
const Foo = {
render(h){
return h('div', 'foo')
}
}
const Bar = {
render(h){
return h('div', 'bar')
}
}
Vue.component('example', {
props: ['ok'],
render(h){
return this.ok ? h(Foo) : h(Bar)
}
})
new Vue({
el: '#app',
data: {ok: true}
})
</script>
3.4.3 high-order components
场景:
问题:为什么用高阶组件而不用mixin?
高阶函数可以使avatar组件不受污染,因此可以用在其他地方;即高阶组件的复用性更强。同时,使得内部组件的可测试性更强,因为保证了这段代码与外部代码有最小的耦合:唯一与外部代码连接的部分是传入的props和传出的props。
instead of:
<avatar url="/path/to/image.png"></avatar>
You can now do:
<smart-avatar username="vuejs"></smart-avatar>
// mock API
function fetchURL (username, cb) {
setTimeout(() => {
// hard coded, bonus: exercise: make it fetch from gravatar!
cb('https://avatars3.githubusercontent.com/u/6128107?v=4&s=200')
}, 500)
}
const Avatar = {
props: ['src'],
template: `<img :src="src">`
}
function withAvatarURL (InnerComponent) {
return {
props: ['username'],
data(){
return {
url:'http://via.placeholder.com/200x200'
}
},
created(){
fetchURL(this.username, (url)=>{
this.url = url // 实际上是维护url这个中间状态
})
},
render(h){
return h(InnerComponent, {
props: {
src: this.url
}
})
}
}
}
const SmartAvatar = withAvatarURL(Avatar)
new Vue({
el: '#app',
components: { SmartAvatar }
})