Vue响应式主要包含:
Object.defineProperty实现数据劫持Proxy实现数据劫持{{}}v-bind, v-on, v-model, v-for,v-ifvdom,把vdom渲染为普通dom
数据变化时能自动更新视图,就是数据响应式
Vue2使用Object.defineProperty实现数据变化的检测
new Vue()⾸先执⾏初始化,对data执⾏响应化处理,这个过程发⽣在Observer中data中获取并初始化视图,这个过程发⽣在 Compile中Watcher实例,将来对应数据变化时,Watcher会调⽤更新函数data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个 Watcherdata中数据⼀旦发⽣变化,会⾸先找到对应的Dep,通知所有Watcher执⾏更新函数
CVue:自定义Vue类 Observer:执⾏数据响应化(分辨数据是对象还是数组) Compile:编译模板,初始化视图,收集依赖(更新函数、 watcher创建) Watcher:执⾏更新函数(更新dom) Dep:管理多个Watcher实例,批量更新
参考 前端手写面试题详细解答
observe: 遍历vm.data的所有属性,对其所有属性做响应式,会做简易判断,创建Observer实例进行真正响应式处理
cvue
{{ count }}
data执⾏响应化处理// 自定义Vue类
class CVue {constructor(options) {this.$options = optionsthis.$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.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}})})
}
class CVue {constructor(options) {// 省略。。// 2 代理data上属性到实例上proxy(this)// 3 编译new Compile(this, this.$options.el)}
}// 编译模板中vue语法,初始化视图,更新视图
class Compile {constructor(vm, el) {this.$vm = vmthis.$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)) {// TODOconsole.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 }}中的countconst 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.attributesArray.from(attrs).forEach(attr => {// c-text="count"中c-text是attr.name,count是attr.valueconst { name: attrName, value: exp } = attrif(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统⼀通知。
实现思路
defineReactive中为每个key定义一个Dep实例Watcher实例getter方法,便可以把Watcher实例存储到key对应的Dep实例中Dep实例,Dep实例调用notiy方法通知所有Watcher更新监听器,数据变化更新对应节点视图
// 创建Watcher监听器,负责更新视图
class Watcher {// vm vue实例,依赖key,updateFn更新函数(编译阶段传递进来)constructor(vm, key, updateFn) {this.$vm = vmthis.$key = keythis.$updateFn = updateFn}update() {// 调用更新函数,获取最新值传递进去this.$updateFn.call(this.$vm, this.$vm[this.$key])}
}
class Complie {// 省略。。。// 编译插值complieText(node) {// RegExp.$1是isInterpolation()中/\{\{(.*)\}\}/匹配出来的组内容// 相等于{{ count }}中的countconst 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}
}
Watcher实例,通知所有Watcher实例更新// 创建订阅器,每个Dep实例对应data中的一个属性
class Dep {constructor() {this.deps = []}// 添加Watcher实例addDep(dep) {this.deps.push(dep)}notify() {// 通知所有Wather更新视图this.deps.forEach(dep => dep.update())}
}
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.targetDep.target = null}
}
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()}}})
}
@xxxmethods到vue实例上v-on指令时,进行事件的绑定@属性时,进行事件绑定bind修改监听函数的this指向为组件实例// 自定义Vue类
class CVue {constructor(options) {this.$methods = options.methods}
}// 编译模板中vue语法,初始化视图,更新视图
class Compile {constructor(vm, el) {this.$vm = vmthis.$el = document.querySelector(el)this.$methods = vm.$methods}// 编译元素complieElement(node) {// 取出元素上属性const attrs = node.attributesArray.from(attrs).forEach(attr => {// c-text="count"中c-text是attr.name,count是attr.valueconst { name: attrName, value: exp } = attrif(this.isDirective(attrName)) {// 省略。。。if(this.isEventListener(attrName)) {// v-on:click, subStr(5)即可截取到clickconst eventType = attrName.substring(5)this.bindEvent(eventType, node, exp)}} else if(this.isEventListener(attrName)) {// @click, subStr(1)即可截取到clickconst 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绑定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个方法
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__ = arrayProtothis.observeArray(obj)} else {// 对象this.walk(obj)}}observeArray(items) {// 如果数组内部元素时对象,继续做响应化处理items.forEach(item => observe(item))}
}
上一篇:两首古诗,第二首
下一篇:于美人灵璧县玉集墓下座