vue 响应式源码实现

222 阅读4分钟

1. 原理

1.1 defineReactive简单版

利用Object.defineProperty() 实现数据的响应,注意需要用一个temp把 值存起来


let temp; 
let obj = {a:1}
defineReactive(obj,"a")
function defineReactive(obj,key) {
    Object.defineProperty(obj,key,{
        get () {
            console.log("调用get方法,temp",temp)
            // console.log("obj[key]",obj[key]) //为什么不直接用 obj[key] 因为又会死循环重复调用get 
            return temp
        },
        set (val) {
            console.log("调用set方法,val",val)
            temp = val
            return 
        }
    })
}

obj.a
obj.a = 2
obj.a

//调用get方法,temp undefined
//调用set方法,val 2
//调用get方法,temp 2

1.2 defineReactive闭包版

方法参数再方法体内被返回,形成闭包

let obj = {}
defineReactive(obj,"a",'1')
function defineReactive(obj,key,val) {
    Object.defineProperty(obj,key,{
        get () {
            console.log("调用get方法,val",val) 
            return val //直接返回 参数val 形成闭包
        },
        set (newVal) {
            console.log("调用set方法,newVal",newVal)
            if(val === newVal) {
                return
            }
            val = newVal
            return 
        }
    })
}

obj.a
obj.a = 2
obj.a

// 调用get方法,val 1
// 调用set方法,newVal 2
// 调用get方法,val 2

1.3. 简单的页面响应

简单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>Document</title>
</head>
<body>
    <div id="app">

    </div>
    
    <script> 
let obj = {}
defineReactive(obj,"a",'1')
function defineReactive(obj,key,val) {
    Object.defineProperty(obj,key,{
        get () {
            console.log("调用get方法,val",val) 
            return val //直接返回 参数val 形成闭包
        },
        set (newVal) {
            if(val === newVal) {
                return
            }
            console.log("调用set方法,newVal",newVal)
            val = newVal
            updateFn()
        }
    })
}

let app = document.getElementById("app")
function updateFn() { 
    app.innerHTML = obj.a
}
setInterval(() => {
    obj.a = new Date().toLocaleTimeString();
}, 1000);
    </script>
</body>
</html>

1.4. 遍历所有属性

//观察方法
function observe(obj) {
    if (typeof obj !== "object" || obj == null) {
        return 
    }
    Object.keys(obj).forEach( k => { 
        defineReactive(obj,key,obj[key])
    })
}

1.5. 解决嵌套对象问题

function defineReactive(obj, key, val) {
    observe(val) //判断值是不是也是对象 递归调用
}

1.6. 插入值是对象处理

set(newVal) {
    if (newVal !== val) {
        observe(newVal) // 赋值的时候也调用一下 
        ...
     }
 }

// vue的set方法就是这个原理
function set(obj, key, val) {
    defineReactive(obj, key, val) 
}

1.7 数组响应式处理


// 1.对象响应化:遍历每个key,定义getter、 setter
// 2.数组响应化:覆盖数组原型⽅法,额外增加通知逻辑
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto);
   ['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(
        method => {
            arrayProto[method] = function () {   //这里修改覆盖 新arrayProto对应的每一个方法
                originalProto[method].apply(this, arguments)//originalProto[method] 调用原来的方法,如Array的push
                notifyUpdate(arguments[0],method) //加入自己想要派发的信息
            }
        }
    )
    /*等价于
    copyProto['push'] = function() {
        orginalProto['push'].call(this,arguments)
        notifyUpdate.log("test..")
    }
    */
  //观察方法 主入口
  function observe(obj) {
    if (typeof obj !== 'object' || obj == null) {
        return
    }
    // 增加数组类型判断,若是数组则覆盖其原型
    if (Array.isArray(obj)) {
        Object.setPrototypeOf(obj, arrayProto)  //等价于 obj.__proto__ = arrayProto  
        //处理数据每个元素响应式
        for (let index = 0; index < obj.length; index++) {
           observe(obj[index]);//继续递归处理
        } 
    } else {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            const key = keys[i]
            defineReactive(obj, key, obj[key])
        }
    }
}
//响应式逻辑
function defineReactive(obj, key, val) {
    observe(val) // 解决嵌套对象问题
    Object.defineProperty(obj, key, {
        get() {
            return val
        },
        set(newVal) {
            if (newVal !== val) {
                observe(newVal) // 新值是对象的情况
                val = newVal
                notifyUpdate(val)
            }
        }
    })
}
function notifyUpdate(val) {
    console.log(val,'⻚⾯更新!')
}  

//测试代码
var testObj = {name:"jason"}
observe(testObj)
testObj.name = "jason.yang"

var testArray = [{name:"jason1"},{name:"jason2"}]
observe(testArray)
testArray.push({name:"jason3"})


2. 实战

2.1官方vue分析

//myVue.html
<meta charset="UTF-8">
<div id="app">
  <p>{{counter}}</p>
  <p j-text="counter"></p>
  <p j-html="desc"></p>
</div>

<script src="myVue.js"></script>

<script>
  const app = new myVue({
    el:'#app',
    data: {
      counter: 1,
      desc: '<span style="color:red">测试</span>'
    },
  })
  setInterval(() => {
    app.counter++
  }, 1000);

</script>
  1. 需要实现data数据响应
  2. 实现{{}} 符号解析
  3. 实现j-text和j-html标签解析 image.png

2.2代码实现

  1. 创建一个MyVue类 接受初始化参数
  2. 实现一个observe的方法完成数据响应 和 Observe类用类处理数据的类型处理,暂时只处理对象情况,数组后面优化
  3. 创建Compile类解析 特殊符号{{}}与j-开头标签
  4. 实现this.$data.a 等价于 this.a的数据代理

2.2.1 observe处理流程

image.png

2.3Compile解析流程

广度遍历,深度优先

  1. 遍历当前根节点下所有dom节点,判断当前节点是否为 需要解析的{{}}或者j-或者 @事件
  2. 如果还是节点继续深度递归解析,如果不是继续执行compile()解析
  3. 用正则匹配执行的类型,如对应的compileText方法,同时调用通用的update方法来创建new Watch和Dep
  4. 由于key 添加了对应Watch和Dep,就可以等待下次key的内容变化时候,触发set方法自动调用dep派发所有watch的notify更新

image.png

//myVue.js

// 实现myyVue构造函数
function defineReactive(obj, key, val) {
  // 如果val是对象,需要递归处理之
  observe(val)

  // 管家创建
  const dep = new Dep()
  
  Object.defineProperty(obj, key, {
    get() {
      console.log('get', key);
      // 依赖收集
      Dep.target && dep.addDep(Dep.target)
      return val
    },
    set(newVal) {
      if (val !== newVal) {
        // 如果newVal是对象,也要做响应式处理
        observe(newVal)
        val = newVal
        console.log('set', key, newVal);

        // 通知更新
        dep.notify()
      }
    }
  })
}

// 遍历指定数据对象每个key,拦截他们
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj
  }

  // 每遇到一个对象,就创建一个Observer实例
  // 创建一个Observer实例去做拦截操作
  new Observer(obj)
}

// proxy代理函数:让用户可以直接访问data中的key
function proxy(vm, key) {
  Object.keys(vm[key]).forEach(k => {
    Object.defineProperty(vm, k, {
      get() {
        return vm[key][k]
      },
      set(v) {
        vm[key][k] = v
      }
    })
  })
}

// 根据传入value类型做不同操作
class Observer {
  constructor(value) {
    this.value = value

    // 判断一下value类型
    // 遍历对象
    this.walk(value)
  }
  //目前只处理对象情况
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }
}

class myVue {
  constructor(options) {
    // 0.保存options
    this.$options = options
    this.$data = options.data
    // 1.将data做响应式处理
    observe(this.$data)
    // 2.为$data做代理
    proxy(this, '$data')
    // 3.编译模板
    new Compile('#app', this)
  }
}

class Compile {
  // el-宿主元素,vm-myyVue实例
  constructor(el, vm) {
    this.$el = document.querySelector(el)
    this.$vm = vm

    // 解析模板
    if (this.$el) {
      // 编译
      this.compile(this.$el)
    }
  }

  compile(el) {
    // el是宿主元素
    // 遍历它,判断当前遍历元素的类型
    el.childNodes.forEach(node => {
      if (node.nodeType === 1) {
        // console.log('编译元素', node.nodeName);
        this.compileElement(node)
      } else if (this.isInter(node)) {
        // 文本, {{xxx}}
        // console.log('编译文本', node.textContent, RegExp.$1);
        this.compileText(node)
      }

      // 递归
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }

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

  // 编译文本
  compileText(node) {
    this.update(node, RegExp.$1, 'text')
  }

  // 编译元素:分析指令、@事件
  compileElement(node) {
    // 获取属性并遍历之
    const nodeAttrs = node.attributes

    Array.from(nodeAttrs).forEach(attr => {
      // 指令:j-xxx="yyy"
      const attrName = attr.name  // j-xxx
      const exp = attr.value // yyy
      if (this.isDirective(attrName)) {
        const dir = attrName.substring(2) // xxx
        // 指令实际操作方法
        this[dir] && this[dir](node, exp)
      }
    })
  }

  isDirective(attr) {
    return attr.indexOf('j-') === 0
  }

  // 执行text指令对应的更新函数
  text(node, exp) {
    this.update(node, exp, 'text')
  }

  // j-text对应操作函数
  textUpdater(node, val) {
    node.textContent = val
  }

  html(node, exp) {
    this.update(node, exp, 'html')
  }

  htmlUpdater(node, val) {
    node.innerHTML = val
  }

  // 提取update,初始化和更新函数创建,该方法是为了每次出发时候,都
  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)
    })
  }

}

// Watcher: 小秘书,跟视图中依赖1:1
// const watchers = []
class Watcher {
  constructor(vm, key, updaterFn) {
    this.vm = vm
    this.key = key
    this.updaterFn = updaterFn
    // 依赖收集触发
    Dep.target = this
    this.vm[this.key] // 触发上面的get
    Dep.target = null
  }
  update() {
    this.updaterFn.call(this.vm, this.vm[this.key])
  }
}

// 管家:和某个key,一一对应,1对多 
class Dep {
  constructor() {
    this.deps = []
  }
  addDep(watcher) {
    this.deps.push(watcher)
  }
  notify() {
    this.deps.forEach(watcher => watcher.update())
  }
}

3.代码优化

3.1数组支持响应式

//拿到 原来的数组原型方法
const orginalProto = Array.prototype
//创建备份
const arrayProto = Object.create(orginalProto)
['push','pop','shift'].forEach( method => {
  //保留原来操作
  orginalProto[method].apply(this,arguments)
  //新增切面操作
  console.log("当前调用了方法:",method)
})

// 根据传入value类型做不同操作
class Observer {
  constructor(value) {
    this.value = value 
    this.walk(value)
  }

  walk(obj) {
    if(Array.isArray(obj)) {//添加数组判断
      //修改原型
      obj.__proto__ = arrayProto
      for (let index = 0; index < obj.length; index++) {
        walk(obj[index]);//继续递归处理
      } 
    }else {
      Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key])
      })
    }
  }
}

3.2新增@事件处理

  // 编译元素:分析指令、@事件
  compileElement(node) {
    // 获取属性并遍历之
    const nodeAttrs = node.attributes

    Array.from(nodeAttrs).forEach(attr => {
      // 指令:k-xxx="yyy"
      const attrName = attr.name  // k-xxx
      const exp = attr.value // yyy
      if (this.isDirective(attrName)) {
        const dir = attrName.substring(2) // xxx
        // 指令实际操作方法
        this[dir] && this[dir](node, exp)
      }else if(this.isEvent(attrName)) { //处理事件
        const dir = attrName.substring(1)
        this.eventHandler(node,exp,dir)
      }
      // 处理事件
    })
  }
  
  //新增事件判断
  isEvent(attr) {
    return attr.indexOf('@') === 0
  }
  eventHandler(node,exp,dir) {
    //这里拿到的 是vm里面的 methods 键值关系对象
    const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp]
    fn.bind(this.$vm) // 解决调用绑定问题
    node.addEventLister(dir,fn)
  }

3.3实现v-model

model(node,exp) {
    this.update(node,exp,'model')
    //增加事件
    node.addEventListener('input', e => {
        this.$vm[exp] = e.target.value
    })
}
//直接节点赋值
modelUpdater(node,value) {
    node.value = value
}

4.源码地址

gitee.com/mjsong/my-v…