注册

手写迷你版Vue




手写迷你版Vue

参考代码:github.com/57code/vue-…

Vue响应式设计思路

Vue响应式主要包含:

  • 数据响应式

  • 监听数据变化,并在视图中更新

  • Vue2使用Object.defineProperty实现数据劫持

  • Vu3使用Proxy实现数据劫持

  • 模板引擎

  • 提供描述视图的模板语法

  • 插值表达式{{}}

  • 指令 v-bind, v-on, v-model, v-for,v-if

  • 渲染

  • 将模板转换为html

  • 解析模板,生成vdom,把vdom渲染为普通dom

数据响应式原理

image.png

数据变化时能自动更新视图,就是数据响应式 Vue2使用Object.defineProperty实现数据变化的检测

原理解析

  • new Vue()⾸先执⾏初始化,对data执⾏响应化处理,这个过程发⽣在Observer

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

Compile

  • 同时定义⼀个更新函数和Watcher实例,将来对应数据变化时,Watcher会调⽤更新函数

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

Watcher

  • 将来data中数据⼀旦发⽣变化,会⾸先找到对应的Dep,通知所有Watcher执⾏更新函数

image.png

一些关键类说明

CVue:自定义Vue类 Observer:执⾏数据响应化(分辨数据是对象还是数组) Compile:编译模板,初始化视图,收集依赖(更新函数、 watcher创建) Watcher:执⾏更新函数(更新dom) Dep:管理多个Watcher实例,批量更新

涉及关键方法说明

observe: 遍历vm.data的所有属性,对其所有属性做响应式,会做简易判断,创建Observer实例进行真正响应式处理

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>cvue</title>
<script src="./cvue.js"></script>
</head>
<body>
<div id="app">
  <p>{{ count }}</p>
</div>

<script>
  const app = new CVue({
    el: '#app',
    data: {
      count: 0
    }
  })
  setInterval(() => {
    app.count +=1
  }, 1000);
</script>
</body>
</html>

CVue

  • 创建基本CVue构造函数:

  • 执⾏初始化,对data执⾏响应化处理

// 自定义Vue类
class CVue {
constructor(options) {
  this.$options = options
  this.$data = options.data

  // 响应化处理
  observe(this.$data)
}
}

// 数据响应式, 修改对象的getter,setter
function defineReactive(obj, key, val) {
// 递归处理,处理val是嵌套对象情况
observe(val)
Object.defineProperty(obj, key, {
  get() {
    return val
  },
  set(newVal) {
    if(val !== newVal) {
      console.log(`set ${key}:${newVal}, old is ${val}`)

      val = newVal
      // 继续进行响应式处理,处理newVal是对象情况
      observe(val)
    }
  }
})
}

// 遍历obj,对其所有属性做响应式
function observe(obj) {
// 只处理对象类型的
if(typeof obj !== 'object' || obj == null) {
  return
}
// 实例化Observe实例
new Observe(obj)
}

// 根据传入value的类型做相应的响应式处理
class Observe {
constructor(obj) {
  if(Array.isArray(obj)) {
    // TODO
  } else {
    // 对象
    this.walk(obj)
  }
}
walk(obj) {
  // 遍历obj所有属性,调用defineReactive进行响应化
  Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}
}

为vm.$data做代理

方便实例上设置和获取数据

例如

原本应该是

vm.$data.count
vm.$data.count = 233

代理之后后,可以使用如下方式

vm.count
vm.count = 233

给vm.$data做代理

class CVue {
constructor(options) {
  // 省略
  // 响应化处理
  observe(this.$data)

  // 代理data上属性到实例上
  proxy(this)
}
}

// 把CVue实例上data对象的属性到代理到实例上
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
  Object.defineProperty(vm, key, {
    get() {
      // 实现 vm.count 取值
      return vm.$data[key]
    },
    set(newVal) {
      // 实现 vm.count = 123赋值
      vm.$data[key] = newVal
    }
  })
})
}

编译

image.png

初始化视图

根据节点类型进行编译
class CVue {
constructor(options) {
  // 省略。。
  // 2 代理data上属性到实例上
  proxy(this)

  // 3 编译
  new Compile(this, this.$options.el)
}
}

// 编译模板中vue语法,初始化视图,更新视图
class Compile {
constructor(vm, el) {
  this.$vm = vm
  this.$el = document.querySelector(el)

  if(this.$el) {
    this.complie(this.$el)
  }
}
// 编译
complie(el) {
  // 取出所有子节点
  const childNodes = el.childNodes
  // 遍历节点,进行初始化视图
  Array.from(childNodes).forEach(node => {
    if(this.isElement(node)) {
      // TODO
      console.log(`编译元素 ${node.nodeName}`)
    } else if(this.isInterpolation(node)) {
      console.log(`编译插值文本 ${node.nodeName}`)
    }
    // 递归编译,处理嵌套情况
    if(node.childNodes) {
      this.complie(node)
    }
  })
}
// 是元素节点
isElement(node) {
  return node.nodeType === 1
}
// 是插值表达式
isInterpolation(node) {
  return node.nodeType === 3
    && /\{\{(.*)\}\}/.test(node.textContent)
}
}
编译插值表达式
// 编译模板中vue语法,初始化视图,更新视图
class Compile {
complie(el) {
  Array.from(childNodes).forEach(node => {
    if(this.isElement(node)) {
      console.log(`编译元素 ${node.nodeName}`)
    } else if(this.isInterpolation(node)) {
      // console.log(`编译插值文本 ${node.textContent}`)
      this.complieText(node)
    }
    // 省略
  })
}
// 是插值表达式
isInterpolation(node) {
  return node.nodeType === 3
    && /\{\{(.*)\}\}/.test(node.textContent)
}
// 编译插值
complieText(node) {
  // RegExp.$1是isInterpolation()中/\{\{(.*)\}\}/匹配出来的组内容
  // 相等于{{ count }}中的count
  const exp = String(RegExp.$1).trim()
  node.textContent = this.$vm[exp]
}
}
编译元素节点和指令

需要取出指令和指令绑定值 使用数据更新视图

// 编译模板中vue语法,初始化视图,更新视图
class Compile {
complie(el) {
  Array.from(childNodes).forEach(node => {
    if(this.isElement(node)) {
      console.log(`编译元素 ${node.nodeName}`)
      this.complieElement(node)
    }
    // 省略
  })
}
// 是元素节点
isElement(node) {
  return node.nodeType === 1
}
// 编译元素
complieElement(node) {
  // 取出元素上属性
  const attrs = node.attributes
  Array.from(attrs).forEach(attr => {
    // c-text="count"中c-text是attr.name,count是attr.value
    const { name: attrName, value: exp } = attr
    if(this.isDirective(attrName)) {
      // 取出指令
      const dir = attrName.substring(2)
      this[dir] && this[dir](node, exp)
    }
  })
}
// 是指令
isDirective(attrName) {
  return attrName.startsWith('')
}
// 处理c-text文本指令
text(node, exp) {
  node.textContent = this.$vm[exp]
}
// 处理c-html指令
html(node, exp) {
  node.innerHTML = this.$vm[exp]
}
}

以上完成初次渲染,但是数据变化后,不会触发页面更新

依赖收集

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

image.png

  • data中的key和dep是一对一关系

  • 视图中key出现和Watcher关系,key出现一次就对应一个Watcher

  • dep和Watcher是一对多关系

实现思路

  • defineReactive中为每个key定义一个Dep实例

  • 编译阶段,初始化视图时读取key, 会创建Watcher实例

  • 由于读取过程中会触发key的getter方法,便可以把Watcher实例存储到key对应的Dep实例

  • 当key更新时,触发setter方法,取出对应的Dep实例Dep实例调用notiy方法通知所有Watcher更新

定义Watcher类

监听器,数据变化更新对应节点视图

// 创建Watcher监听器,负责更新视图
class Watcher {
// vm vue实例,依赖key,updateFn更新函数(编译阶段传递进来)
constructor(vm, key, updateFn) {
  this.$vm = vm
  this.$key = key
  this.$updateFn = updateFn
}
update() {
  // 调用更新函数,获取最新值传递进去
  this.$updateFn.call(this.$vm, this.$vm[this.$key])
}
}
修改Compile类中的更新函数,创建Watcher实例
class Complie {
// 省略。。。
// 编译插值
complieText(node) {
  // RegExp.$1是isInterpolation()中/\{\{(.*)\}\}/匹配出来的组内容
  // 相等于{{ count }}中的count
  const exp = String(RegExp.$1).trim()
  // node.textContent = this.$vm[exp]
  this.update(node, exp, 'text')
}
// 处理c-text文本指令
text(node, exp) {
  // node.textContent = this.$vm[exp]
  this.update(node, exp, 'text')
}
// 处理c-html指令
html(node, exp) {
  // node.innerHTML = this.$vm[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(newVal) {
    fn && fn(node, newVal)
  })
}
// 文本更新器
textUpdater(node, value) {
  node.textContent = value
}
// html更新器
htmlUpdater(node, value) {
  node.innerHTML = value
}
}
定义Dep类
  • data的一个属性对应一个Dep实例

  • 管理多个Watcher实例,通知所有Watcher实例更新

// 创建订阅器,每个Dep实例对应data中的一个属性
class Dep {
constructor() {
  this.deps = []
}
// 添加Watcher实例
addDep(dep) {
  this.deps.push(dep)
}
notify() {
  // 通知所有Wather更新视图
  this.deps.forEach(dep => dep.update())
}
}
创建Watcher时触发getter
class Watcher {
// vm vue实例,依赖key,updateFn更新函数(编译阶段传递进来)
constructor(vm, key, updateFn) {
  // 省略
  // 把Wather实例临时挂载在Dep.target上
  Dep.target = this
  // 获取一次属性,触发getter, 从Dep.target上获取Wather实例存放到Dep实例中
  this.$vm[key]
  // 添加后,重置Dep.target
  Dep.target = null
}
}
defineReactive中作依赖收集,创建Dep实例
function defineReactive(obj, key, val) {
// 递归处理,处理val是嵌套对象情况
observe(val)

const dep = new Dep()
Object.defineProperty(obj, key, {
  get() {
    Dep.target && dep.addDep(Dep.target)
    return val
  },
  set(newVal) {
    if(val !== newVal) {
      val = newVal
      // 继续进行响应式处理,处理newVal是对象情况
      observe(val)
      // 更新视图
      dep.notify()
    }
  }
})
}

监听事件指令@xxx

  • 在创建vue实例时,需要缓存methods到vue实例上

  • 编译阶段取出methods挂载到Compile实例上

  • 编译元素时

  • 识别出v-on指令时,进行事件的绑定

  • 识别出@属性时,进行事件绑定

  • 事件绑定:通过指令或者属性获取对应的函数,给元素新增事件监听,使用bind修改监听函数的this指向为组件实例

// 自定义Vue类
class CVue {
constructor(options) {
  this.$methods = options.methods
}
}

// 编译模板中vue语法,初始化视图,更新视图
class Compile {
constructor(vm, el) {
  this.$vm = vm
  this.$el = document.querySelector(el)
  this.$methods = vm.$methods
}

// 编译元素
complieElement(node) {
  // 取出元素上属性
  const attrs = node.attributes
  Array.from(attrs).forEach(attr => {
    // c-text="count"中c-text是attr.name,count是attr.value
    const { name: attrName, value: exp } = attr
    if(this.isDirective(attrName)) {
      // 省略。。。
      if(this.isEventListener(attrName)) {
        // v-on:click, subStr(5)即可截取到click
        const eventType = attrName.substring(5)
        this.bindEvent(eventType, node, exp)
      }
    } else if(this.isEventListener(attrName)) {
      // @click, subStr(1)即可截取到click
      const eventType = attrName.substring(1)
      this.bindEvent(eventType, node, exp)
    }
  })
}
// 是事件监听
isEventListener(attrName) {
  return attrName.startsWith('@') || attrName.startsWith('c-on')
}
// 绑定事件
bindEvent(eventType, node, exp) {
  // 取出表达式对应函数
  const method = this.$methods[exp]
  // 增加监听并修改this指向当前组件实例
  node.addEventListener(eventType, method.bind(this.$vm))
}
}

v-model双向绑定

实现v-model绑定input元素时的双向绑定功能

// 编译模板中vue语法,初始化视图,更新视图
class Compile {
// 省略...
// 处理c-model指令
model(node, exp) {
  // 渲染视图
  this.update(node, exp, 'model')
  // 监听input变化
  node.addEventListener('input', (e) => {
    const { value } = e.target
    // 更新数据,相当于this.username = 'mio'
    this.$vm[exp] = value
  })
}
// model更新器
modelUpdater(node, value) {
  node.value = value
}
}

数组响应式

  • 获取数组原型

  • 数组原型创建对象作为数组拦截器

  • 重写数组的7个方法

// 数组响应式
// 获取数组原型, 后面修改7个方法
const originProto = Array.prototype
// 创建对象做备份,修改响应式都是在备份的上进行,不影响原始数组方法
const arrayProto = Object.create(originProto)
// 拦截数组方法,在变更时发出通知
;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
// 在备份的原型上做修改
arrayProto[method] = function() {
  // 调用原始操作
  originProto[method].apply(this, arguments)
  // 发出变更通知
  console.log(`method:${method} value:${Array.from(arguments)}`)
}
})

class Observe {
constructor(obj) {
  if(Array.isArray(obj)) {
    // 修改数组原型为自定义的
    obj.__proto__ = arrayProto
    this.observeArray(obj)
  } else {
    // 对象
    this.walk(obj)
  }
}
observeArray(items) {
  // 如果数组内部元素时对象,继续做响应化处理
  items.forEach(item => observe(item))
}
}

作者:LastStarDust
来源:https://juejin.cn/post/7036291383153393701

0 个评论

要回复文章请先登录注册