Git Product home page Git Product logo

blog's People

Contributors

fe-linmu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

blog's Issues

02全家桶(Vue-router&&Vuex)源码实现

Vue-router

Vue Router 是 Vue.js 官方的路由管理器,它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。

安装:

vue add router

核心步骤:

  1. 使用 vue-router 插件,router.js

    import Route from 'vue-router'
    Vue.use(Router)
  2. 创建 Router 实例,router.js

    export default new Router({...})
  3. 在根组件上添加该实例,main.js

    import router from './router'
    new Vue({
      router
    }).$mount('#app')
  4. 添加路由视图,App.vue

    <router-view></router-view>
  5. 导航

    <router-link to='/'>Home</router-link>
    <router-link to='/about'>About</router-link>

vue-router 源码实现

单页面应用程序中,URL 发生变化,不刷新并显示对应的视图内容

需求分析

  1. spa 点击链接不能出刷新页面

    • hash #xxx

    • history api

  2. 事件 hashchange,通知 router-view 更新

    • 利用 Vue 数据响应式

    • 制作一个响应式数据表示当前 URL,在 router-view 的 render 函数使用它

任务

  1. 实现一个插件

    • 实现 VueRouter 类

    • 实现 install 方法

  2. 实现两个全局组件

    • router-link

    • router-view

创建 kvue-router.js

// 插件

let KVue

// 1. 实现一个install方法
class VueRouter {
  constructor(options) {
    this.$options = options

    // 响应式数据
    const initial = window.location.hash.slice(1) || '/'
    KVue.util.defineReactive(this, 'current', initial)

    // this.current = '/'

    // 监听事件
    window.addEventListener('hashchange', this.onHashChange.bind(this))
    window.addEventListener('load', this.onHashChange.bind(this))

    // 缓存path和route映射关系
    this.routeMap = {}
    this.$options.routes.forEach(route => {
      this.routeMap[route.path] = route
    })
  }

  onHashChange () {
    this.current = window.location.hash.slice(1)
    console.log(this.current)
  }
}

// 形参是Vue构造函数
KVueRouter.install = function (Vue) {
  // 保存构造函数(独立的包,不希望将vue也打包进去)
  KVue = Vue

  // 1. 挂载$router
  Vue.mixin({
    beforeCreate () {
      // 全局混入,将来在组件实例化的时候才执行
      // 此时Vue实例已经存在了
      // this指的是组件实例
      if (this.$options.router) {
        Vue.prototype.$router = this.$options.router
      }
    }
  })

  // 2. 实现两个全局组件
  Vue.component('router-link', {
    props: {
      to: {
        type: String,
        required: true
      }
    },
    // h是createElement函数
    render (h) {
      // <a href='#/xxx'></a>
      // h(tag,props,children)
      // jsx语法也可以用
      // return <a href={'#' + this.to}>{this.$slots.default}</a>
      return h(
        'a',
        {
          attrs: {
            href: '#' + this.to
          }
        },
        this.$slots.default
      )
    }
  })
  // rouetr-view 是一个容器
  Vue.component('router-view', {
    render (h) {
      // 1. 获取路由器实例
      // const routes = this.$router.$options.routes
      // const current = this.$router.current
      // const route = routes.find(route => route.path === current)
      // const comp = route ? route.component : null

      const { routeMap, current } = this.$router
      const comp = routeMap[current] ? routeMap[current].component : null

      // 获取路由表 eg:'/'===home组件
      // return h('div','view')
      return h(comp)
    }
  })
}

export default KVueRouter

Vuex 原理

Vuex 集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以可预测的方式发生变化。

Vuex

整合 vuex

vue add vuex

核心概念

  • state 状态、数据
  • mutations 更改状态的函数
  • actions 异步操作
  • store 包含以上概念的容器

状态 - state

state保存应用状态

export default new Vuex.Store({
  state: { counter:0 },
})

状态变更 - mutations

mutations用于修改状态,store.js

export default new Vuex.Store({
  mutations: {
    add (state) {
      state.counter++
    }
  }
})

派生状态 - getters

从state派生出新状态,类似计算属性

export default new Vuex.Store({
  getters: {
    doubleCounter (state) { // 计算剩余数量
      return state.counter * 2;
    }
  }
})

动作 - actions

添加业务逻辑,类似于 controller

export default new Vuex.Store({
  actions: {
    add ({ commit }) {
      setTimeout(() => {
        commit('add')
      }, 1000);
    }
  }
})

测试代码:

<p @click="$store.commit('add')">counter: {{$store.state.counter}}</p>
<p @click="$store.dispatch('add')">async counter: {{$store.state.counter}}</p>
<p>double:{{$store.getters.doubleCounter}}</p>

原理解析

任务

  1. 实现⼀个插件:声明Store类,挂载$store

  2. Store具体实现:

    • 创建响应式的 state,保存 mutations、actions 和 getters

    • 实现 commit 根据用户传入 type 执行对应 mutation

    • 实现 dispatch 根据用户传入 type 执行对应 action,同时传递上下文

    • 实现 getters,按照 getters 定义对 state 做派生

代码实现:

let KVue

// 实现 Store 类
class Store {
  constructor(options) {
    // 保存 mutations
    this._mutations = options.mutations

    // 保存 actions
    this._actions = options.actions

    // 绑定 this 到 store 实例
    // 绑定 commit 上下⽂否则 action 中调⽤ commit 时可能出问题!!
    // 同时也把 action 绑了,因为 action 可以互调
    const store = this
    const { commit, action } = store
    this.commit = function boundCommit (type, payload) {
      commit.call(store, type, payload)
    }
    this.action = function boundAction (type, payload) {
      return action.call(store, type, payload)
    }

    // getters
    // 1. 遍历用户传入 getters 所有 key,动态赋值,其值应该是函数执行结果
    // 2. 确保它是响应式的,Object.defineProperty(this.getters,key,{get(){}})
    // 3. 缓存结果,可以利用 computed    
    let computed = {}
    options.getters && this.handleGetters(options.getters, computed)

    // 响应式的 state
    this._vm = new KVue({
      data: {
        $$state: options.state
      },
      computed
    })
  }

  handleGetters (getters, computed) {
    this.getters = {}
    Object.keys(getters).forEach(key => {
      computed[key] = () => getters[key](this.state)
      Object.defineProperty(this.getters, key, {
        get: () => getters[key](this.state),
        enumerable: true,
      })
    })
  }

  get state () {
    return this._vm._data.$$state
  }
  set state (v) {
    console.error('请重新设置' + v + '的名称')
  }

  // commit(type,payload):执行 mutation,修改状态
  commit (type, payload) {
    // 根据 type 获取对应的 mutations
    const entry = this._mutations[type]
    if (!entry) {
      console.error('这是未知的 mutation 类型')
      return
    }
    entry(this.state, payload)
  }

  // dispatch(type,payload)
  dispatch (type, payload) {
    const entry = this._actions[type]

    if (!entry) {
      console.error('这是未知的 action 类型')
      return
    }

    return entry(this, payload)
  }
}

// 实现插件
function install (Vue) {
  KVue = Vue

  // 混入
  Vue.mixin({
    beforeCreate () {
      if (this.$options.store) {
        Vue.prototype.$store = this.$options.store
      }
    }
  })
}

// 此处导出的对象理解为 Vuex
export default { Store, install }

01Vue组件化实战

01 组件化实战

组件化

vue组件系统提供了⼀种抽象,让我们可以使用独立可复用的组件来构建大型应用,任意类型的应用界面都可以抽象为⼀个组件树。组件化能提高开发效率,方便重复使用,简化调试步骤,提升项目可维护性,便于多人协同开发。

image.png

组件通信常用方式

  • props

  • event

  • vuex

自定义事件

  1. 边界情况

    • $parent

    • $children

    • $root

    • $refs

    • provide/inject

  2. 非prop特性

    • $attrs

    • $listeners

组件通信

props

父子传值

// child
props: { msg: String }

// parent
<HelloWord msg="测试父子传值" />

自定义事件

子父传值

// child
this.$emit('sonToFather', 'son-->Father')

// parent
<Cart @sonToFather="testSonToFather($event)"></Cart>

事件总线

任意两个组件之间传值常用事件总线或 vuex 的方式。

// Bus: 事件派发、监听和回调管理
class Bus {
  constructor() {
    this.event = {}
  }
  // 订阅事件
  $on (eventName, callback) {
    if (!this.event[eventName]) {
      this.event[eventName] = []
    }
    this.event[eventName].push(callback)
  }
  // 触发事件(发布事件)
  $emit (eventName, params) {
    let eventArr = this.event[eventName]
    if (eventArr) {
      eventArr.map(item => {
        item(params)
      })
    }
  }
  // 删除订阅事件
  $off (eventName, callback) {
    let arr = this.event[eventName]
    if (arr) {
      if (callback) {
        let index = arr.indexOf(callback)
        arr.splice(index, 1)
      } else {
        arr.length = 0
      }
    }
  }
}

// main.js
Vue.prototype.$bus = new Bus()

// child1
this.$bus.$on('testBus',handle)

// child2
this.$bus.$emit('testBus')

实践中通常用 Vue 代替 Bus,因为 Vue 已经实现了相应接口

vuex

组件通信最佳实践

创建唯⼀的全局数据管理者 store,通过它管理数据并通知组件状态变更。

$parent/$root

兄弟组件之间通信可通过共同祖辈搭桥,$parent 或 $root。

// brother1
this.$parent.$on('testParent',handle)

// brother2
this.$parent.$emit('testParent')

$children

父组件可以通过 $children 访问子组件实现父子通信。

// parent
this.$children[0].xx = 'xxx'

注意:$children 不能保证子元素顺序

和 $refs 有什么区别?

$attrs/$listeners

包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 ( class 和 style 除外)。当⼀个组件没有
声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

// child:并未在props中声明foo
<p>{{$attrs.foo}}</p>

// parent
<HelloWorld foo="foo"/>

文档

refs

获取子节点引用

// parent
<HelloWorld ref="testRef">

mounted() {
  this.$refs.testRef.xx='xxx'
}

provide/inject

能够实现祖先和后代之间传值

// ancestor
provide() {
  return {foo: 'foo'}
}

// descendant
inject: ['foo']

范例:组件通信

插槽

插槽语法是 Vue 实现的内容分发 API,用于复合组件开发。该技术在通用组件库开发中有大量应用。

匿名插槽

// comp1
<div>
  <slot></slot>
</div>

// parent
<Comp>testSlot</Comp>

具名插槽

将内容分发到子组件指定位置

// comp2
<div>
  <slot></slot>
  <slot name="content"></slot>
</div>

// parent
<Comp2>
  <!-- 默认插槽用default做参数 -->
  <template v-slot:default>具名插槽</template>
  <!-- 具名插槽用插槽名做参数 -->
  <template v-slot:content>内容...</template>
</Comp2>

作用域插槽

分发内容要用到子组件中的数据

// comp3
<div>
  <slot :foo="foo"></slot>
</div>

// parent
<Comp3>
  <!-- 把v-slot的值指定为作用域上下文对象 -->
  <template v-slot:default="slotProps">来自子组件数据:{{slotProps.foo}}</template>
</Comp3>

范例:插槽

组件化实战

通用表单组件

收集数据、校验数据并提交。

需求分析

  1. 实现 KForm

    • 指定数据、校验规则
  2. KformItem

    • label 标签添加

    • 执行校验

    • 显示错误信息

  3. KInput

    • 维护数据

最终效果:Element 表单

范例代码

KInput

创建 components/form/KInput.vue

<template>
  <div>
    <input :value="value" @input="onInput" v-bind="$attrs">
  </div>
</template>

<script>
  export default {
    inheritAttrs:false,
    props:{
      value:{
        type:String,
        default:''
      }
    },
    methods:{
      onInput(e){
        this.$emit('input',e.target.value)
      }
    }
  }
</script>

使用 KInput

创建 components/form/index.vue,添加如下代码:

<template>
  <div>
    <h3>Form表单</h3>
    <hr>
    <k-input v-model="model.username"></k-input>
    <k-input type="password" v-model="model.password"></k-input>>
  </div>
</template>

<script>
import KInput from './KInput'

export default {
  components:{
    KInput
  },
  data(){
    return {
      model:{
        username:'tom',
        password:''
      }
    }
  }
}
</script>

实现 KFormItem

创建components/form/KFormItem.vue

<template>
  <div>
    <label v-if="label">{{label}}</label>
    <slot></slot>
    <p v-if="error">{{error}}</p>
  </div>
</template>

<script>
export default {
  props: {
    label:{ // 输入项标签
      type: String,
      default:''
    },
    prop:{ // 字段名
      type: String,
      default: ''
    }
  },
  data() {
    return {
      error: '' // 校验错误
    }
  }
}
</script>

使用 KFormItem

components/form/index.vue,添加基础代码:

<template>
  <div>
    <h3>Form表单</h3>
    <hr>
    <k-form-item label="用户名" prop="username">
      <k-input v-model="model.username"></k-input>
    </k-form-item>
    <k-form-item label="确认密码" prop="password">
      <k-input type="password" v-model="model.password"></k-input>
    </k-form-item>
  </div>
</template>

实现 KForm

<template>
  <form>
    <slot></slot>
  </form>
</template>

<script>
export default {
  provide() {
    return {
      form: this // 将组件实例作为提供者,子代组件可方便获取
    }
  },
  props:{
    model:{
      type: Object,
      required: true
    },
    rules:{
      type: Object
    }
  }
}
</script>

使用 KForm

components/form/index.vue,添加基础代码:

<template>
  <div>
    <h3>Form表单</h3>
    <hr/>
    <k-form :model="model" :rules="rules" ref="loginForm">
      ...
    </k-form>
  </div>
</template>

<script>
import KForm from './KForm'

export default {
  components: {
    KForm
  },
  data() {
    return {
      rules: {
        username: [{
          required: true,
          message: '请输入用户名'
        }],
        password: [{
          required: true,
          message: '请输入密码'
        }]
      }
    }
  },
  methods: {
    submitForm() {
      this.$refs['loginForm'].validate(valid => {
        if (valid) {
          alert('请求登录')
        } else {
          alert('校验失败')
        }
      })
    }
  }
}
</script>

数据校验

Input 通知校验

onInput(e) {
  // ...
  // $parent指FormItem
  this.$parent.$emit('validate')
}

FormItem 监听校验通知,获取规则并执行校验

inject: ['form'], // 注入
mounted() { // 监听校验事件
  this.$on('validate',() => {this.validate()})
},
methods:{
  validate() {
    // 获取对应 FormItem 校验规则
    console.log(this.form.rules[this.prop])
    // 获取校验值
    console.log(this.form.model[this.prop])
  }
}

安装 async-validator:

npm i async-validator -S
import Schema from 'async-validator'

validate() {
  // 获取对应 FormItem 校验规则
  const rules = this.form.rules[this.prop]
  // 获取校验值
  const value = this.form.model[this.prop]
  // 校验描述对象
  const descriptor = {[this.prop]:rules}
  // 创建校验器
  const schema = new Schema(descriptor)
  // 返回 Promise,没有触发 catch 就说明验证通过
  return schema.validate({[this.prop]:value},errors=>{
    if (errors) {
      // 将错误信息显示
      this.error = errors[0].message
    } else {
      // 校验通过
      this.error = ''
    }
  })
}

表单全局验证,为 Form 提供 validate 方法

validate(cb){
  // 调用所有含有 prop 属性的子组件的 validate 方法并得到 Promise 的值
  const tasks = this.$children
      .filter(item => item.prop)
      .map(item => item.validate())
  // 所有任务必须全部成功才算校验通过,任一失败则校验失败
  Promise.all(tasks)
      .then(() => cb(true))
      .catch(() => cb(false))
}

实现弹窗组件

弹窗这类组件的特点是它们在当前 vue 实例之外独立存在,通常挂载于 body;它们是通过 JS 动态创建
的,不需要在任何组件中声明。常见使用姿势:

this.$create(Notice, {
  title: '林慕-弹窗组件'
  message: '提示信息',
  duration: 1000
}).show()

create 函数

import Vue from 'vue'

// 创建函数接收要创建组件定义
function create(Component, props) {
  // 创建一个 Vue 实例
  const vm = new Vue({
    render(h) {
      // render 函数将传入组件配置对象转换为虚拟 dom
      console.log(h(Component,{props}))
      return h(Component, {props})
    }
  }).$mount() // 执行挂载函数,但未指定挂载目标,表示只执行初始化工作

  // 将生成 dom 元素追加至 body
  document.body.appendChild(vm.$el)
  // 给组件实例添加销毁方法
  const comp = vm.$children[0]
  comp.remove = () => {
    document.body.removeChild(vm.$el)
    vm.$destroy()
  }
  return comp
}

// 暴露调用接口
export default create

另一种创建组件实例的方式: Vue.extend(Component)

通知组件

新建通知组件,Notice.vue

<template>
  <div class="box" v-if="isShow">
    <h3>{{title}}</h3>
    <p class="box-content">{{message}}</p>
  </div>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      default: ''
    },
    message: {
      type: String,
      default: ''
    },
    duration: {
      type: Number,
      default: 1000
    }
  },
  data() {
    return {
      isShow: false
    }
  },
  methods: {
    show() {
      this.isShow = truw
      setTimeout(this.hide, this.duration)
    },
    hide() {
      this.isShow = false
      this.remove()
    }
  }
}
</script>
<style>
.box {
  position: fixed;
  width: 100%;
  top: 16px;
  left: 0;
  text-align: center;
  pointer-events: none;
  background-color: #fff;
  border: grey 3px solid;
  box-sizing: border-box;
}
.box-content {
  width: 200px;
  margin: 10px auto;
  font-size: 14px;
  padding: 8px 16px;
  background: #fff;
  border-radius: 3px;
  margin-bottom: 8px;
}
</style>

使用 create api

测试,components/form/index.vue

<script>
import create from "@/utils/create"
import Notice from "@/components/Notice"

export default {
  methods: {
    submitForm(form) {
      this.$refs[form].validate(valid => {
        const notice = create(Notice, {
          title: '林慕-create',
          message: valid ? '请求登录' : '校验失败',
          duration: 1000
        })
        notice.show()
      })
    }
  }
}
</script>

递归组件

// TODO

拓展

  1. 使用 Vue.extend 方式实现 create 方法
  • 方法一:和第一个 create 方法类似
export function create2 (Component, props) {
  let VueMessage = Vue.extend({
    render(h) {
      return h(Component, {props})
    }
  })
  let newMessage = new VueMessage()
  let vm = newMessage.$mount()
  let el = vm.$el
  document.body.appendChild(el) 
  const comp = vm.$children[0]
  comp.remove = () => {
    document.body.removeChild(vm.$el)
    vm.$destroy()
  }
  return comp
}
  • 方法二:利用 propsData 属性
export function create3 (Component, props) {
  // 组件构造函数如何获取?
  // 1. Vue.extend()
  const Ctor = Vue.extend(Component)
  // 创建组件实例
  const comp = new Ctor({ propsData: props })
  comp.$mount()
  document.body.appendChild(comp.$el)
  comp.remove = function () {
    document.body.removeChild(comp.$el)
    comp.$destroy()
  }
  return comp
}

方法三:使用插件进一步封装便于使用,create.js

import Notice from '@/components/Notice.vue'
// ...
export default {
  install(Vue) {
    Vue.prototype.$notice = function (options) {
      return create(Notice, options)
    }
  }
}
// 使用
this.$notice({title: 'xxx'})
  1. 修正 input 中 $parent 写法的问题
  • mixin emitter

  • 声明 componentName

  • dispatch()

  1. 学习 Element 源码

03Vue源码实现

Vue 源码实现

理解 Vue 的设计**

  • MVVM 模式

image.png

MVVM 框架的三要素:数据响应式模板引擎渲染

数据响应式:监听数据变化并在视图中更新

  • Object.defineProperty()

  • Proxy

模板引擎:提供描述视图的模板语法

  • 插值:{{}}

  • 指令:v-bind/v-on/v-model/v-for/v-if

渲染:如何将模板转换为 html

  • 模板=>vdom=>dom

数据响应式原理

数据变更能够响应在视图中,就是数据响应式,Vue2 中利用 Object.defineProperty() 实现变更检测。

image.png

简单实现

const obj = {}

function defineReactive (obj, key, val) {
  Object.defineProperty(obj, key, {
    get () {
      console.log(`get ${key}:${val}`)
      return val
    },
    set (newVal) {
      if (newVal !== val) {
        console.log(`set ${key}:${newVal}`)
        val = newVal
      }
    }
  })
}

defineReactive(obj, 'foo', 'foo')

obj.foo = 'test foo'

结合视图

<!DOCTYPE html>
<html lang="en">

<head></head>

<body>
  <div id="app"></div>
  <script>
    const obj = {}
    function defineReactive(obj, key, val) {
      Object.defineProperty(obj, key, {
        get() {
          console.log(`get ${key}:${val}`);
          return val
        },
        set(newVal) {
          if (newVal !== val) {
            val = newVal
            update()
          }
        }
      })
    }
    defineReactive(obj, 'foo', '')
    obj.foo = new Date().toLocaleTimeString()
    function update() {
      app.innerText = obj.foo
    }
    setInterval(() => {
      obj.foo = new Date().toLocaleTimeString()
    }, 1000);
  </script>
</body>

</html>

遍历需要响应化的对象

// 对象响应化:遍历每个key,定义getter、setter
function observe (obj) {
  if (typeof obj !== 'object' || obj == null) {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

const obj = { foo: 'foo', bar: 'bar', baz: { a: 1 } }
observe(obj)
obj.foo = 'foooooooooooo'
obj.bar = 'barrrrrrrrrrr'
obj.baz.a = 10 // 嵌套对象no ok

当有嵌套的对象怎么办呢?

解决方法:

function defineReactive (obj, key, val) {
  observe(val)    // 递归 observer 方法处理嵌套对象
  Object.defineProperty(obj, key, {
    //...
  })
}

解决赋的值是对象的情况

obj.baz = {a:1}
obj.baz.a = 10 // no ok
set(newVal) {
    if (newVal !== val) {
        observe(newVal) // 新值是对象的情况
        notifyUpdate()

如果添加、删除了新属性无法检测

obj.dong='林慕'
obj.dong // 并没有 get 信息

测试

set(obj,'dong','林慕’)
obj.dong

写到现在,大家应该也发现了,Object.defineProperty() 不支持检测对象,如果修改对象的话需要 Vue.$set 方法,即上面的 set 方法,当然,源码中的 $set 会有一些边界判断条件,当确定是对象时,执行 defineReactive 方法,将对象进行响应式绑定。

思考:Vue 数组的响应化是如何处理的呢?

Vue 在处理数组时将可以改变数组的7个方法进行了重写,分别是 push、pop、shift、unshift、splice、sort 和 reverse。

重写数组的实现方式如下:

const arrayProto = Array.prototype
// 先克隆一份数组原型
export const arrayMethods = Object.create(arrayProto)
// 七个改变数组的方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

// 拦截变化方法并发出事件
methodsToPatch.forEach(function (method) {
  // 缓存原方法
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    // 执行原始方法
    const result = original.apply(this, args)
    // 额外通知变更,只有这7个方法有这个待遇
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 对新加入对象进行响应化处理
    if (inserted) ob.observerArray(inserted)
    // 通知改变
    ob.dep.notify()
    return result
  })
})

【注】:最后面的总结部分贴出的源码,未包含数组的响应式处理,如需添加,可查看数组的响应式处理有何特殊之处

Vue 中的数据响应化

目标代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body>
  <div id="app">
    <p>{{counter}}</p>
  </div>
  <script src="node_modules/vue/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        counter: 1
      },
    })
    setInterval(() => {
      app.counter++
    }, 1000);
  </script>
</body>

</html>

Vue 原理分析

初始化流程

image.png

  • 创建 Vue 实例对象 init 过程会初始化生命周期,初始化事件中心,初始化渲染、执行 beforeCreate 周期函数、初始化 data、props、computed、watcher、执行 created 周期函数等。

  • 初始化后,调用 $mount 方法对 Vue 实例进行挂载(挂载的核心过程包括模板编译渲染以及更新三个过程)。

  • 如果没有在 Vue 实例上定义 render 方法而是定义了 template,那么需要经历编译阶段。需要先将 template 字符串编译成 render function,template 字符串编译步骤如下 :

    • parse 正则解析 template 字符串形成 AST(抽象语法树,是源代码的抽象语法结构的树状表现形式)

    • optimize 标记静态节点跳过 diff 算法(diff 算法是逐层进行比对,只有同层级的节点进行比对,因此时间的复杂度只有 O(n)。

    • generate 将 AST 转化成 render function 字符串

  • 编译成 render function 后,调用 $mount 的 mountComponent 方法,先执行 beforeMount 钩子函数,然后核心是实例化一个渲染 Watcher,在它的回调函数(初始化的时候执行,以及组件实例中监测到数据发生变化时执行)中调用 updateComponent 方法(此方法调用 render 方法生成虚拟 Node,最终调用 update 方法更新 DOM)。

  • 调用 render 方法将 render function 渲染成虚拟的 Node(真正的 DOM 元素是非常庞大的,因为浏览器的标准就把 DOM 设计的非常复杂。如果频繁的去做 DOM 更新,会产生一定的性能问题,而 Virtual DOM 就是用一个原生的 JavaScript 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多,而且修改属性也很轻松,还可以做到跨平台兼容),render 方法的第一个参数是 createElement (或者说是 h 函数),这个在官方文档也有说明。

  • 生成虚拟 DOM 树后,需要将虚拟 DOM 树转化成真实的 DOM 节点,此时需要调用 update 方法,update 方法又会调用 pacth 方法把虚拟 DOM 转换成真正的 DOM 节点。需要注意在图中忽略了新建真实 DOM 的情况(如果没有旧的虚拟 Node,那么可以直接通过 createElm 创建真实 DOM 节点),这里重点分析在已有虚拟Node的情况下,会通过 sameVnode 判断当前需要更新的 Node 节点是否和旧的 Node 节点相同(例如我们设置的 key 属性发生了变化,那么节点显然不同),如果节点不同那么将旧节点采用新节点替换即可,如果相同且存在子节点,需要调用 patchVNode 方法执行 diff 算法更新DOM,从而提升DOM操作的性能。

响应式流程

image.png

  1. new Vue() 首先先执行初始化,对 data 执行响应化处理,这个过程发生在 Observer 中

  2. 同时对模板执行编译,找到其中动态绑定的数据,从 data 中获取并初始化视图,这个过程发生在 Compile 中

  3. 同时定义⼀个更新函数和 Watcher,将来对应数据变化时 Watcher 会调用更新函数

  4. 由于 data 的某个 key 在⼀个视图中可能出现多次,所以每个 key 都需要⼀个管家 Dep 来管理多个 Watcher

  5. 将来 data 中数据⼀旦发生变化,会首先找到对应的 Dep,通知所有 Watcher 执行更新函数

涉及类型介绍

  • KVue:框架构造函数

  • Observer:执行数据响应化(分辨数据是对象还是数组)

  • Compile:编译模板,初始化视图,收集依赖(更新函数、watcher 创建)

  • Watcher:执行更新函数(更新dom)

  • Dep:管理多个 Watcher,批量更新

KVue

框架构造函数:执行初始化

  • 执行初始化,对 data 执行响应化处理,kvue.js
function observe (obj) {
  if (typeof obj !== 'object' || obj === null) {
    return
  }

  new Observer(obj)
}

function defineReactive (obj, key, val) { }

class KVue {
  constructor(options) {
    this.$options = options
    this.$data = options.data

    observe(this.$data)
  }
}

class Observer {
  constructor(value) {
    this.value = value
    this.walk(value)
  }
  walk (obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }
}
  • 为 $data 做代理
class KVue {
  constructor(options) {
    // ...
    proxy(this, '$data')
  }
}

function proxy (vm) {
  Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(vm, key, {
      get () {
        return vm.$data[key]
      },
      set (newVal) {
        vm.$data[key] = newVal
      }
    })
  })
}

编译 —— Compile

image.png

初始化视图

根据节点类型编译,compile.js

class Compile {
  constructor(el, vm) {
    this.$vm = vm
    this.$el = document.querySelector(el)

    if (this.$el) {
      // 编译模板
      this.compile(this.$el)
    }
  }
  compile (el) {
    // 递归遍历el
    const childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      // 判断其类型
      if (this.isElement(node)) {
        console.log('编译元素:', node.nodeName)
      } else if (this.isInterpolation(node)) {
        console.log('编译插值文本:', node.textContent)
      }
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }

  // 元素
  isElement (node) {
    return node.nodeType === 1
  }

  // 判断是否是插值表达式{{xxx}}
  isInterpolation (node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }
}  

编译插值,compile.js

compile(el) {
  // ...
    } else if (this.isInerpolation(node)) {
      // console.log("编译插值⽂本" + node.textContent);
      this.compileText(node);
    }
  });
}
compileText(node) {
  console.log(RegExp.$1);
  node.textContent = this.$vm[RegExp.$1];
}

编译元素

compile(el) {
  //...
  if (this.isElement(node)) {
    // console.log("编译元素" + node.nodeName);
    this.compileElement(node)
  }
}
compileElement(node) {
  let nodeAttrs = node.attributes;
  Array.from(nodeAttrs).forEach(attr => {
    let attrName = attr.name;
    let exp = attr.value;
    if (this.isDirective(attrName)) {
      let dir = attrName.substring(2);
      this[dir] && this[dir](node, exp);
    }
  });
}
isDirective(attr) {
  return attr.indexOf("k-") == 0;
}
text(node, exp) {
  node.textContent = this.$vm[exp];
}

k-html

html(node, exp) {
    node.innerHTML = this.$vm[exp]
}

依赖收集

视图中会用到 data 中某 key,这称为依赖。同⼀个 key 可能出现多次,每次都需要收集出来用⼀个 Watcher 来维护它们,此过程称为依赖收集。多个 Watcher 需要⼀个 Dep 来管理,需要更新时由 Dep 统⼀通知。

看下面案例,理出思路:

new Vue({
  template:
    `<div>
      <p>{{name1}}</p>
      <p>{{name2}}</p>
      <p>{{name1}}</p>
    <div>`,
  data: {
    name1: 'name1',
    name2: 'name2'
  }
});

image.png

实现思路

  1. defineReactive 时为每⼀个 key 创建⼀个 Dep 实例

  2. 初始化视图时读取某个 key,例如 name1,创建⼀个 watcher1

  3. 由于触发 name1 的 getter 方法,便将 watcher1添加到 name1 对应的 Dep 中

  4. 当 name1 更新,setter 触发时,便可通过对应 Dep 通知其管理所有 Watcher 更新

image.png

创建 Watcher,kvue.js

const watchers = []  // 临时用于保存 watcher 测试用
// 监听器:负责更新视图
class Watcher {
  constructor(vm, key, updateFn) {
    // kvue 实例
    this.vm = vm;
    // 依赖 key
    this.key = key;
    // 更新函数
    this.updateFn = updateFn;
    // 临时放入 watchers 数组
    watchers.push(this)
  }
  // 更新
  update () {
    this.updateFn.call(this.vm, this.vm[this.key]);
  }
}

编写更新函数、创建 watcher

// 调 update 函数执插值文本赋值
compileText(node) {
  // console.log(RegExp.$1);
  // node.textContent = this.$vm[RegExp.$1];
  this.update(node, RegExp.$1, 'text')
}
text(node, exp) {
  this.update(node, exp, 'text')
}
html(node, exp) {
  this.update(node, exp, 'html')
}
update(node, exp, dir) {
  const fn = this[dir + 'Updater']
  fn && fn(node, this.$vm[exp])
  new Watcher(this.$vm, exp, function (val) {
    fn && fn(node, val)
  })
}
textUpdater(node, val) {
  node.textContent = val;
}
htmlUpdater(node, val) {
  node.innerHTML = val
}

声明 Dep

class Dep {
  constructor() {
    this.deps = []
  }
  addDep (dep) {
    this.deps.push(dep)
  }
  notify () {
    this.deps.forEach(dep => dep.update());
  }
}

创建 watcher 时触发 getter

class Watcher {
  constructor(vm, key, updateFn) {
    Dep.target = this;
    this.vm[this.key];
    Dep.target = null;
  }
}

依赖收集,创建 Dep 实例

defineReactive(obj, key, val) {
  this.observe(val);
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get () {
      Dep.target && dep.addDep(Dep.target);
      return val
    },
    set (newVal) {
      if (newVal === val) return
      dep.notify()
    }
  })
}

总结

以上是一个简单的 Vue 实现,此时 Watcher 监听的粒度太过于精细,导致 Watcher 过多,不需要 vdom。

后面的文章会写类似于 Vue2.0 的监听粒度问题,Vue2.0 的监听粒度会折中,每个组件一个 Watcher,当组件内部的值发生变化时,响应式系统已经知道是哪个组件发生了变化,然后在组件内部进性 diff 算法的操作,最后更新为最新的节点信息。

整体代码

html 部分:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>example</title>
  <script src="./lvue.js" charset="utf-8"></script>
</head>

<body>
  <div id="app">
    <p>{{name}}</p>
    <p k-text="name"></p>
    <p>{{age}}</p>
    <p>
      {{doubleAge}}
    </p>
    <input type="text" k-model="name">
    <button @click="changeName">呵呵</button>
    <div k-html="html"></div>
  </div>
  <script src='./compile.js'></script>
  <script src='./lvue.js'></script>

  <script>
    let k = new KVue({
      el: '#app',
      data: {
        name: 'i am test',
        age: 12,
        html: '<button>这是一个按钮</button>'
      },
      created() {
        console.log('开始啦')
        setTimeout(() => {
          this.name = '我是蜗牛'
        }, 1600)
      },
      methods: {
        changeName() {
          this.name = 'changed name'
          this.age = 1
          this.id = 'xxx'
          console.log(1, this)
        }
      }
    })
  </script>
</body>

</html>

Watcher、Dep 部分

class KVue {
  constructor(options) {
    this.$options = options // 挂载实例
    this.$data = options.data  // 数据响应化
    // 监听拦截数据
    this.observe(this.$data)

    // // 模拟一个 watcher 创建
    // new Watcher()
    // this.$data.a
    // new Watcher()
    // this.$data.c.d
    // // 模拟结束
    new Compile(options.el, this)
    // created 执行
    if (options.created) {
      options.created.call(this)
    }
  }
  observe (value) {
    if (!value || typeof value !== 'object') {
      return
    }
    // 遍历该对象
    Object.keys(value).forEach(key => {
      this.defineReactive(value, key, value[key])
      // 代理 data 中的属性到 vue 实例上
      this.proxyData(key)
    })
  }
  // 数据响应化
  defineReactive (obj, key, val) {

    this.observe(val) // 递归解决数据的嵌套

    const dep = new Dep() // 每执行一次 defineReactive,就创建一个 Dep 实例

    Object.defineProperty(obj, key, { // 数据劫持
      configurable: true,
      enumerable: true,
      get () {
        Dep.target && dep.addDep(Dep.target)
        return val
      },
      set (newVal) {
        if (newVal === val) {
          return
        }
        val = newVal
        console.log(`${key}属性更新了:${val}`)
        dep.notify()
      }
    })
  }
  proxyData (key) {
    Object.defineProperty(this, key, {
      configurable: true,
      enumerable: true,
      get () {
        return this.$data[key]
      },
      set (newVal) {
        this.$data[key] = newVal
      }
    })
  }
}
// Dep:用来管理 Watcher
class Dep {
  constructor() {
    this.deps = [] // 这里存放若干依赖(watcher),一个依赖对应一个属性,依赖就是视图上的引用
  }
  addDep (dep) {
    this.deps.push(dep)
  }
  notify () {
    this.deps.forEach(dep => dep.update())
  }
}
// Watcher:小秘书,界面中的一个依赖对应一个小秘书
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb
    // 将当前 Watcher 实例指定到 Dep 静态属性 target
    Dep.target = this
    this.vm[this.key]  // 触发 getter、添加依赖
    Dep.target = null
  }
  update () {
    console.log('Watcher监听的属性更新了')
    this.cb.call(this.vm, this.vm[this.key])
  }
}

Compile 部分

// 用法 new Compile(el,vm)
class Compile {
  constructor(el, vm) {
    // 要遍历的宿主节点
    this.$el = document.querySelector(el)
    this.$vm = vm
    // 开始编译
    if (this.$el) {
      // 转换内部内容为片段 Fragment
      this.$fragment = this.node2Fragment(this.$el)
      // 执行编译
      this.compile(this.$fragment)
      // 将编译完的 html 结果追加至 $el
      this.$el.appendChild(this.$fragment)
    }
  }
  // 将宿主元素中代码片段拿出来遍历,这样做比较高效
  node2Fragment (el) {
    const frag = document.createDocumentFragment()
    // 将 el 中所有子元素搬家至 frag 中
    let child
    while (child = el.firstChild) {
      frag.appendChild(child)
    }
    return frag
  }
  // 编译过程
  compile (el) {
    const childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      // 类型判断
      if (this.isElement(node)) {
        // 元素
        console.log('编译元素' + node.nodeName)
        // 查找k-/@/:
        const nodeAttrs = node.attributes
        Array.from(nodeAttrs).forEach((attr) => {
          const attrName = attr.name // 属性名
          const exp = attr.value  // 属性值
          if (this.isDirective(attrName)) {
            // k-text
            const dir = attrName.substring(2)
            // 执行指令
            this[dir] && this[dir](node, this.$vm, exp)
          }
          if (this.isEvent(attrName)) {
            const dir = attrName.substring(1) // @click
            this.eventHandler(node, this.$vm, exp, dir)
          }
        })
      } else if (this.isInterpolation(node)) {
        // 插值文本
        console.log('编译文本' + node.textContent)
        this.compileText(node)
      }
      // 递归子节点
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }
  compileText (node) {
    console.log(RegExp.$1)
    // node.textContent = this.$vm.$data[RegExp.$1]
    this.update(node, this.$vm, RegExp.$1, 'text')
  }
  // 更新函数
  update (node, vm, exp, dir) {
    const updaterFn = this[dir + 'Updater']
    // 初始化
    updaterFn && updaterFn(node, vm[exp])
    // 依赖收集
    new Watcher(vm, exp, function (value) {
      updaterFn && updaterFn(node, value)
    })
  }
  html (node, vm, exp) {
    this.update(node, vm, exp, 'html')
  }
  htmlUpdater (node, value) {
    node.innerHTML = value
  }
  text (node, vm, exp) {
    this.update(node, vm, exp, 'text')
  }
  // 双绑
  model (node, vm, exp) {
    // 指定 input 的 value 属性
    this.update(node, vm, exp, 'model')
    // 视图对模型响应
    node.addEventListener('input', e => {
      vm[exp] = e.target.value
    })
  }
  modelUpdater (node, value) {
    node.value = value
  }
  textUpdater (node, value) {
    node.textContent = value
  }
  // 事件处理器
  eventHandler (node, vm, exp, dir) {
    let fn = vm.$options.methods && vm.$options.methods[exp]
    if (dir && fn) {
      node.addEventListener(dir, fn.bind(vm))
    }
  }
  isDirective (attr) {
    return attr.indexOf('k-') === 0
  }
  isEvent (attr) {
    return attr.indexOf('@') === 0
  }
  isElement (node) {
    return node.nodeType === 1
  }
  // 插值文本
  isInterpolation (node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.