Vue双向数据绑定原理

需求

  • 使用Vue的思想实现一个简易版的Vue,实现一个简单的双向数据绑定
<div id="app"> <h2>{{person.name}}--{{person.age}}</h2> <h3>{{person.name}}</h3> <h3>{{msg}}</h3> <div v-text="msg"></div> <div v-text="person.name"></div> <div v-html="htmlStr"></div> <input type="text" v-model="msg"> <button v-on:click="handleClick">按钮v-on</button> <button @click="handleClick">按钮@</button> </div> <script> var vm = new myVue({ el: '#app', data() { return { person:{ name: 'wmm', age: 20, }, msg: 'vue test', htmlStr: '<h3>我们都是中国人</h3>' } }, methods: { handleClick() { this.person.name += '6' } } }) </script>

基础

数据劫持

  • Vue2.x使用的是Object.defineProperty实现的

Object.defineProperty描述符的属性

  • value:属性的值
  • get:给属性提供getter的方法,如果没有getter则为undefined。该方法返回值被用作属性值。默认为undefined
  • set:给属性提供setter的方法,如果没有setter则为undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认值为undefined
  • writable:属性是否可写
  • enumerable:属性是否会出现在for in或者Object.keys()的遍历中
  • configurable:属性是否配置,以及可否删除
<div id="app"> <input type="text" id="test" /> <p id="test1"></p> </div> <script> var obj = {} // Object.defineProperty 定义 get 和 set 方法 // 在读取 obj.hello 的时候,会走 get 方法 // 在设置的时候 obj.hello = 123 ,会走 set 方法 Object.defineProperty(obj, 'hello', { get: function() { console.log("get方法被调用") }, set: function(newVal) { console.log("set方法被调用", newVal) document.getElementById('test').value = newVal document.getElementById('test1').innerHTML = newVal } }) document.getElementById('test').addEventListener('input', function(e) { obj.hello = e.target.value }) </script>
  • Vue3.x使用的是es6proxy实现的

let proxy = new Proxy(target, handler);

  • target:要代理的目标对象。(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler:定义拦截行为的配置对象(也是一个对象,其内部的属性均为执行操作的函数)。
    • handler.getPrototypeOf():在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy)
    • handler.setPrototypeOf():在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null)
    • handler.isExtensible():在判断一个代理对象是否是可扩展时触发该操作,比如在执行 Object.isExtensible(proxy)
    • handler.preventExtensions():在让一个代理对象不可扩展时触发该操作,比如在执行 Object.preventExtensions(proxy)
    • handler.getOwnPropertyDescriptor():在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo")
    • handler.defineProperty():在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, "foo", {})
    • handler.has():在判断代理对象是否拥有某个属性时触发该操作,比如在执行 "foo" in proxy
    • handler.get():在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo
    • handler.set():在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1
    • handler.deleteProperty():在删除代理对象的某个属性时触发该操作,比如在执行 delete proxy.foo
    • handler.ownKeys():在获取代理对象的所有属性键时触发该操作,比如在执行 Object.getOwnPropertyNames(proxy)
    • handler.apply():在调用一个目标对象为函数的代理对象时触发该操作,比如在执行 proxy()
    • handler.construct():在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行new proxy()
<div id="app"> <input type="text" id="test" /> <p id="test1"></p> </div> <script> var _obj = { hello: '' } var obj = new Proxy(_obj, { get: function(target, key) { console.log("get方法被调用") return target[key] }, set: function(target, key, value) { console.log("set方法被调用", value) target[key] = value document.getElementById('test').value = value document.getElementById('test1').innerHTML = value } }) document.getElementById('test').addEventListener('input', function(e) { obj.hello = e.target.value }) </script>

DocumentFragment

  • DocumentFragment(文档片段)可以看作节点容器,它可以包含多个子节点

  • 当我们将它插入到 DOM 中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。

  • 使用 DocumentFragment 处理节点,速度和性能远远优于直接操作 DOM

  • Vue 进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持,通过 append 方法,DOM 中的节点会被自动删除)到 DocumentFragment 中,经过一番处理后,再将 DocumentFragment 整体返回插入挂载目标。

<div id="app"> <input type="text" id="a" /> <span id="b"></span> </div> <script> function nodeToFragment(node) { var flagment = document.createDocumentFragment() var child while (child = node.firstChild) { // 劫持node的所有子节点 flagment.append(child) // flagment.appendChild(child) } return flagment } var dom = nodeToFragment(document.getElementById('app')) // 返回到app中 document.getElementById('app').appendChild(dom) </script>

订阅-发布者模式

  • 定义一个主题对象Dep,用来管理所有的订阅者
  • 发布者发布信息,主题对象执行notify方法
  • 主题对象触发所有订阅者执行update方法
// 一个发布者publisher var pub = { publish: function() { dep.notify() } } // 三个订阅者subscribers var sub1 = { update: function() { console.log(1) } } var sub2 = { update: function() { console.log(2) } } var sub3 = { update: function() { console.log(3) } } // 一个主题对象 function Dep() { this.subs = [sub1, sub2, sub3] } Dep.prototype.notify = function() { this.subs.forEach(function(sub) { sub.update() }) } // 发布者发布信息,主题对象执行notify方法,进而触发订阅者执行update方法 var dep = new Dep() pub.publish()

实现

myVue

class myVue { constructor(options) { this.$el = options.el this.$data = typeof options.data === 'function' ? options.data() : options.data this.$methods = options.methods this.init() } init() { if (!this.$el) return // 数据劫持,实现响应式 new Observer(this.$data) // 编译模板 new Compile(this.$el, this) // 劫持一下数据,使得可以通过 this.person.name 这种形式访问和设置数据 this.proxyData(this.$data) } proxyData(data) { Object.keys(data).forEach(key => { Object.defineProperty(this, key, { enumerable: false, // 不可枚举 configurable: true, // 可以修改 get() { return data[key] }, set(val) { data[key] = val } }) }) } }

Observer

  • 通过循环和递归使用Object.defineProperty劫持数据
class Observer { constructor(data) { this.observe(data) } observe(data) { if (!data || typeof data !== 'object') return // 循环调用,将所有的属性都配置 get / set Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]) }) } defineReactive(data, key, value) { // value 也有可能是对象,递归调用 this.observe(value) Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { return value }, set(newVal) { if (newVal === value) return value = newVal } }) } }
  • 数组的话,需要重写可以改变数组的方法。有新增的项,需要递归劫持
class Observer { constructor(data) { this.observe(data) } observe(data) { if (!data || typeof data !== 'object') return if (Array.isArray(data)) { Object.setPrototypeOf(data, this.arrMethods()) this.observeArr(data) } else { Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]) }) } } defineReactive(data, key, value) { this.observe(value) Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { return value }, set(newVal) { if (newVal === value) return value = newVal } }) } observeArr(arr) { arr.forEach(item => { this.observe(item) }) } arrMethods() { const METHODS = [ 'push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice' ] const originArrMethods = Array.prototype const arrMethods = Object.create(originArrMethods) const that = this METHODS.forEach(m => { arrMethods[m] = function() { const args = Array.prototype.slice.call(arguments) const res = originArrMethods[m].apply(this, arguments) let newArr switch (m) { case 'push': case 'unshift': newArr = args break case 'splice': newArr = args.slice(2) break default: break } newArr && that.observeArr(newArr) return res } }) return arrMethods } }

Watcher

Vue使用的是 订阅-发布者模式 + 观察者模式

  • 定义Watcher
class Watcher { constructor(vm, expr, cb) { this.vm = vm this.expr = expr this.cb = cb this.value = null } update() {} }
  • 定义Dep
class Dep { constructor() { this.subs = [] } add(sub) { this.subs.push(sub) } notify() { this.subs.forEach(sub => sub.update()) } }

定义一个静态的Dep.target = null
new 一个 Watcher 的对象的时候,先赋值一个全局变量Dep.target = this,用完后清空

  • 修改Watcher
class Watcher { constructor(vm, expr, cb) { this.vm = vm this.expr = expr this.cb = cb this.init() } init() { Dep.target = this // 为了触发 defineProperty 方法中的 get 方法 this.value = this.vm.$data[this.expr] Dep.target = null } update() {} }
  • 修改Observer
const dep = new Dep() class Observer { // ... defineReactive(data, key, value) { this.observe(value) Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { // 第一次 new 对象 Watcher 的时候,初始化数据的时候,往订阅者对象里面添加对象 // 第二次后,就不需要再添加了 if (Dep.target) dep.add(Dep.target) return value }, set(newVal) { if (newVal === value) return value = newVal // 触发更新:发出通知notify() → 触发订阅者的update()方法 dep.notify() } }) } // ... }

Compile

class Compile { constructor(el, vm) { this.el = this.isElementNode(el) ? el : document.querySelector(el) this.vm = vm this.init() } init() { if (this.el) { // 将挂载目标的所有子节点劫持;使用 DocumentFragment 处理 const fragment = this.nodeToFragment(this.el) // 编译模板 this.compile(fragment) // DocumentFragment 处理完成后整体返回插入挂载目标 this.el.appendChild(fragment) } else { console.error('没有容器组件') } } nodeToFragment(el) { // 是一个虚拟节点的容器树,可以存放我们的虚拟节点 const fragment = document.createDocumentFragment() let firstChild // 把node的firstChild赋值成while的条件,可以看做是遍历所有的dom节点 // 一旦遍历到底了,node的firstChild就会未定义成undefined就跳出while while (firstChild = el.firstChild) { fragment.appendChild(firstChild) } return fragment } compile(node) { const childNodes = node.childNodes Array.prototype.slice.call(childNodes).forEach(child => { if (this.isElementNode(child)) { this.compileElement(child) } else if (this.isTextNode(child)) { this.compileText(child) } // 如果子节点存在子节点,递归编译 if (child.childNodes && child.childNodes.length) { this.compile(child) } }) } compileElement(node) {} compileText(node) {} isElementNode(node) { return node.nodeType === 1 } isTextNode(node) { return node.nodeType === 3 } }

Vue有很多指令,比如:v-textv-htmlv-modelv-bindv-on:click@click

这里我们使用 策略模式 统一处理这些指令

var compileUtil = { getVal(vm, expr) { // expr 有这种类似的形式 "person.name" // 这个时候我们要取出来的是 vm.$data['person']['name'] return expr.split('.').reduce((data, val) => { return data[val] }, vm.$data) }, setVal(vm, expr, value) { // expr 有这种类似的形式 "person.name" // 这个时候我们要做的是 vm.$data['person']['name'] = value const arr = expr.split('.') const last = arr.pop() arr.reduce((data, val) => { return data[val] }, vm.$data)[last] = value }, text: function(vm, node, expr) {}, html: function(vm, node, expr) {}, model: function(vm, node, expr) {}, on: function(vm, node, expr, eventName) {}, bind: function(vm, node, expr, attrName) {} }
  • 修改Compile,处理elementtext节点
class Compile { // ... compileElement(node) { const attrs = node.attributes Array.prototype.slice.call(attrs).forEach(attr => { // v-text="person.name" ==> name: "v-text" value: "person.name" // v-on:click="handleClick" ==> name: "v-on:click" value: "handleClick" // @click="handleClick" ==> name: "@click" value: "handleClick" const { name, value } = attr if (this.isDirective(name)) { // "v-text" ==> directive: "text" // "v-on:click" ==> directive: "on:click" const directive = name.slice(2) // "text" ==> dirName: "text" eventName: undefined // "on:click" ==> dirName: "on" eventName: "click" const [dirName, eventName] = directive.split(':') compileUtil[dirName](this.vm, node, value, eventName) } else if (this.isEventName(name)) { // "@click" ==> eventName: "click" const eventName = name.slice(1) compileUtil['on'](this.vm, node, value, eventName) } }) } compileText(node) { const content = node.textContent if (/\{\{(.+?)\}\}/.test(content)) { compileUtil['text'](this.vm, node, content) } } // ... isDirective(attrName) { return attrName.indexOf('v-') === 0 } isEventName(attrName) { return attrName.indexOf('@') === 0 } }
  • 各指令具体方法实现
var compileUtil = { // ... getContent(vm, expr) { return expr.replace(/\{\{(.+?)\}\}/g, (...args) => { return this.getVal(vm, args[1]) }) }, text: function(vm, node, expr) { let value // expr 有可能是这种形式 {{person.name}}--{{person.age}} if (expr.indexOf('{{') !== -1) { value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => { new Watcher(vm, args[1], () => { this.updater.textUpdater(node, this.getContent(vm, expr)) }) return this.getVal(vm, args[1]) }) } else { value = this.getVal(vm, expr) new Watcher(vm, expr, newVal => { this.updater.textUpdater(node, newVal) }) } this.updater.textUpdater(node, value) }, html: function(vm, node, expr) { const value = this.getVal(vm, expr) // 声明成观察者模式 new Watcher(vm, expr, newVal => { this.updater.htmlUpdater(node, newVal) }) this.updater.htmlUpdater(node, value) }, model: function(vm, node, expr) { let value = this.getVal(vm, expr) new Watcher(vm, expr, newVal => { this.updater.modelUpdater(node, newVal) }) this.updater.modelUpdater(node, value) // 双向数据绑定,实现 视图更新 → 数据更新 node.addEventListener('input', e => { const newVal = e.target.value if (newVal === value) return this.setVal(vm, expr, newVal) value = newVal }, false) }, on: function(vm, node, expr, eventName) { const cb = vm.$methods && vm.$methods[expr] if (eventName && cb) { node.addEventListener(eventName, cb.bind(vm), false) } }, bind: function(vm, node, expr, attrName) { const value = this.getVal(vm, expr) new Watcher(vm, expr, newVal => { this.updater.attrUpdater(node, attrName, newVal) }) this.updater.attrUpdater(node, attrName, value) }, updater: { textUpdater(node, value) { node.textContent = value }, htmlUpdater(node, value) { node.innerHTML = value }, modelUpdater(node, value) { node.value = value }, attrUpdater(node, name, value) { node.setAttribute(name, value) } } }
  • 实现Watcherupdate方法,实现视图更新
class Watcher { constructor(vm, expr, cb) { this.vm = vm this.expr = expr this.cb = cb this.init() } init() { Dep.target = this this.value = compileUtil.getVal(this.vm, this.expr) Dep.target = null } // 在这个 update 方法中更新视图 update() { const newVal = compileUtil.getVal(this.vm, this.expr) if (newVal === this.value) return this.cb.call(this.vm, newVal, this.value) this.value = newVal } }

完整代码


创作不易,若本文对你有帮助,欢迎打赏支持作者!

 分享给好友: