Vue2 源码浅析

来源于尤雨溪在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/setterObject.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:

  1. (Essentially) A lightweight JavaScript data format to represent what the actual DOM should look like at a given point in time
  2. 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

场景: 需要传入url,而这里需要一个入用户名便能搜索url生成头像的组件

问题:为什么用高阶组件而不用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 }
})
updatedupdated2024-07-162024-07-16