Vue响应式设计思路

Vue响应式次要蕴含:

  • 数据响应式
  • 监听数据变动,并在视图中更新
  • Vue2应用Object.defineProperty实现数据劫持
  • Vu3应用Proxy实现数据劫持
  • 模板引擎
  • 提供形容视图的模板语法
  • 插值表达式{{}}
  • 指令 v-bind, v-on, v-model, v-for,v-if
  • 渲染
  • 将模板转换为html
  • 解析模板,生成vdom,把vdom渲染为一般dom

数据响应式原理

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

原理解析

  • new Vue()⾸先执⾏初始化,对data执⾏响应化解决,这个过程发⽣在Observer
  • 同时对模板执⾏编译,找到其中动静绑定的数据,从data中获取并初始化视图,这个过程发⽣在 Compile
  • 同时定义⼀个更新函数和Watcher实例,未来对应数据变动时,Watcher会调⽤更新函数
  • 因为data的某个key在⼀个视图中可能呈现屡次,所以每个key都须要⼀个管家Dep来治理多个 Watcher
  • 未来data中数据⼀旦发⽣变动,会⾸先找到对应的Dep,告诉所有Watcher执⾏更新函数

一些要害类阐明

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,setterfunction 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.countvm.$data.count = 233

代理之后后,能够应用如下形式

vm.countvm.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      }    })  })}

参考 前端手写面试题具体解答

编译

初始化视图

依据节点类型进行编译
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-')  }  // 解决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统⼀告诉。

  • 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))  }}