Vue2.6双向绑定原理解析(附实现一个简易的new Vue及3.0中proxy的对比)

一、引言

Vue 作为近几年发展最快的JS框架, 其崛起主要原因不单单是因为粉丝的过度追捧,也并不是因为某个大公司的权威推动。
为什么使用Vue,道理很简单,因为Vue好用,并且它同时具备angular和react的优点,轻量级,api简单,文档齐全,简单强大。那么为什么开发者觉得他好用呢?我归纳为以下几点:

1.声明式渲染

2.响应式的数据绑定

3.组件化

4.Virtual DOM

在这个章节,作者通过Vue源码和思维导图的方式深入讲解『响应式的数据绑定』即双向绑定原理(由于售卖前端团队使用的Vue源码版本Vue2.6,因此本文基于Vue2.6源码讲解),后面还会实现一个简易的demo模拟Vue双向绑定原理。

二、深入理解

1.双向绑定

学术圈对Vue双向绑定的答案有很多,但既然是讲原理,我就本着通俗易懂的方式,简单来说就是数据和视图其中一方做出修改,另一方也会随之变动,即视图能够驱动数据,数据也能驱动视图。为了方便理解,我们对比一下数据单项绑定和双向绑定的有什么区别

单向绑定 我们以小程序为例:

wxml:

<view>
  <text>text:</text>
  <text>{{name}}</text>
</view>
<view style="display:flex;">
  <text>input:</text>
  <input type="text" value='{{name}}' bindinput='bindName'/>
</view>

js:

const app = getApp()

Page({
  data: {
    name: '坤坤'
  },
})

此处应有动图展示效果,以后补全

双向绑定我们当然要用Vue举例了:

<label>
    {{keyword}}
    <el-input
        size="small"
        v-model="keyword"
        @keyup.enter.native="handlelist"
        placeholder="请输入节点IP"
        clearable
    />
    <label>
        <el-button
            size="small"
            type="primary"
            @click="handlelist"
        >搜索</el-button>
    </label>
</label>

keyword: string = ''

此处应有动图展示效果,以后补全

小程序需要通过bind和setData的方式才能实现双向绑定

wxml:

<view>
  <text>text:</text>
  <text>{{name}}</text>
</view>
<view style="display:flex;">
  <text>input:</text>
  <input type="text" value='{{name}}' bindinput='bindName'/>
</view>

js:

const app = getApp()

Page({
  data: {
    name: '坤坤'
  },
  bindName: function (event) {
    this.setData({
      name: event.detail.value
    })
  }
})

此处应有动图展示效果,以后补全

由此,我们了解到双向绑定是什么,即:

1.视图驱动数据——>事件绑定

2.数据驱动视图——>数据劫持和订阅发布

本章我们主要讲解第二点,也是Vue响应式原理的核心部分:数据驱动

下面给出一个概括性的思维导图:
1
监听器Observer:vue对数据对象进行遍历,包括子属性对象的属性,利用Object.defineProperty()方法对上述每个属性都加上setter和getter方法,这样,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据的变化。

解析器Compile:解析Vue模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一但数据有变动,收到通知,调用更新函数进行数据更新。

订阅者Watcher:Watcher订阅者是Observer和Compiler之间通信的桥梁,主要的任务是订阅Observer中的属性值变化的消息,当收到属性值变化的消息时,触发解析器Complier中对应的更新函数。

订阅器Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者Watcher,对监听器Observer和订阅者Watcher进行统一管理。

通过这个导图我们看到数据劫持和发布订阅都是通过什么实现的,首先看数据劫持

2.数据劫持

在Vue2.0版本中 数据劫持是通过Object.DefineProperty() 实现的,当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 会遍历此对象所有的 property,并使用 Object.DefineProperty() 把这些 property 全部转为 getter/setter

额外声明:Object.DefineProperty() 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。(es5-shim可以让一些低级的浏览器支持最新的ecmascript5的一些特性)

3.发布订阅

发布订阅是一种设计模式,我们看看Vue源码中是如何实现的:

首先我们要找到处理响应式的入口:

  • 在构造函数中,调用了 src/core/instance/init.js 中的 _init 方法
  • 在 _init 方法中调用了 src/core/instance/state.js中定义的 initState(vm):初始化 Vue 实例的状态
    • 初始化了 data、props、methods等
    • initState(vm) 方法中判断:
      • 如果定义了 data 就调用 initData(vm)把 data 中的成员注入到 Vue 实例,并且把它转换成响应式对象。
      • initData() 方法最后调用了 observe() 方法
    • 如果没有定义 data,就直接调用 observe()
      • 初始化一个空的响应式对象
  • observe 就是响应式处理的入口。

observe() 方法在src\core\observer\index.js中定义。
observer(监听器) 文件夹中的文件,都是和响应式处理相关的。

①监听器
/* @flow */

import Dep from './dep'
import VNode from '../vdom/vnode'
import { arrayMethods } from './array'
import {
  def,
  warn,
  hasOwn,
  hasProto,
  isObject,
  isPlainObject,
  isPrimitive,
  isUndef,
  isValidArrayIndex,
  isServerRendering
} from '../util/index'

const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

export let shouldObserve: boolean = true

export function toggleObserving (value: boolean) {
  shouldObserve = value
}

export class Observer {
  // 用Flow 语法为属性指定类型
  value: any; // 观测对象
  dep: Dep;// 依赖对象
  vmCount: number; // 计数器
  constructor (value: any) {//初始化
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)// 将实例 this 挂载到观察对象 value 的 __ob__ 属性,这也是我们打印属性会看到__ob__的由来
    if (Array.isArray(value)) {// 判断属性值是否是数组
      if (hasProto) {//数组响应式处理
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)// 为数组中的每一个对象元素创建一个 observer
    } else {
      this.walk(value)//对象响应式处理
    }
  }

  walk (obj: Object) {//遍历每一个属性,调用 defineReactive方法将每个属性做响应式处理
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  observeArray (items: Array<any>) {//数组每个属性递归响应式处理
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])//遍历并通过Object.defineProperty 挂载方法
  }
}

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&//某些情况会通过设置false禁用响应,这是逻辑上的优化.例如实例上定义props,没有意义但是不会报错
    !isServerRendering() &&//判断是否是服务器渲染
    (Array.isArray(value) || isPlainObject(value)) &&//判断是否是纯粹的 [object object] 对象
    Object.isExtensible(value) &&
    !value._isVue//判断是否是Vue实例
  ) {
    ob = new Observer(value)//对value进行响应式操作
  }
  if (asRootData && ob) {//如是根数据,计数
    ob.vmCount++
  }
  return ob
}

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()// 创建依赖对象实例,为当前属性收集依赖

  const property = Object.getOwnPropertyDescriptor(obj, key)// 获取属性的描述符对象
  if (property && property.configurable === false) {// 判断属性是否可配置
    return
  }

  // 提供预定义的存取器函数,如果obj是用户定义的对象,可能自定义了get/set,所以获取用户定义的get/set,在其基础上扩展 收集依赖 和 派发更新 的功能
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {// 获取初始值
    val = obj[key]
  }
  
  let childOb = !shallow && observe(val)// 判断是否递归观察子对象(深度监听),则将子对象属性转换成响应式,并返回子观察对象
  Object.defineProperty(obj, key, {// 把属性转换成 getter/setter
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {// 首先判断是否预定义了getter,如果存在则获取并返回getter调用的返回值,如果不存在则直接返回之前获取的 初始值
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {// 依赖收集
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {// 判断值是否发生了变化
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (getter && !setter) return// 判断是否只读属性
      if (setter) {// 存在预定义的setter则调用
        setter.call(obj, newVal)
      } else {//没有则直接赋值
        val = newVal
      }
      childOb = !shallow && observe(newVal)// 如果新值是对象,并且是深层监听,则将这个对象转化为响应式,并返回给 childOb
      dep.notify()//分发更新
    }
  })
}

我们看到,Oberve中实现响应式的核心方法为defineReactive(),它调用了Object.defineProperty(),而在Object.defineProperty()主要进行了两项操作,首先在get中进行了依赖收集,然后在set中进行了分发更新,它们都用到了dep中的方法,那下面我们来看看dep都包含了什么

②订阅器

在src\core\observer\dep.js中

/* @flow */

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

export default class Dep {
  static target: ?Watcher;//一个watcher类型的指针变量 收集依赖的关键性道具
  id: number;// dep的id
  subs: Array<Watcher>;//用来收集watcher对象的数组 收集依赖的容器

  constructor () {//每创建一个Dep实例+1,作为唯一标识
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {//添加一个订阅者
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {//删除一个订阅者
    remove(this.subs, sub)
  }

  depend () {//通过改变指向来建立依赖
    if (Dep.target) {
      Dep.target.addDep(this)// 如果这个指向已经存在,就把订阅器对象添加到订阅者的依赖中,addDep为订阅者的方法,Dep.target相当于正要建立依赖的那个订阅者,也就是watcher
    }
  }

  notify () {//分发更新
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Dep.target = null//全局唯一,同一时间只有一个watcher被使用
const targetStack = []

export function pushTarget (target: ?Watcher) {//入栈
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {//出堆
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

这里我们看到前面observe中调用的两个方法,depend和notify:

  • depend通过调用watcher中的adddep方法建立依赖,设置 Dep.target 是在创建 watcher 对象的位置:src\core\instance\lifecycle.js的mountComponent()方法中new了一个Watcher,然后在Watcher的构造函数中调用了get()方法,get()放开中调用了 pushTarget()方法,内部将自己记录到了Dep.target。
  • notify通过id进行排序,按顺序分发,更新时按照顺序添加到任务队列

既然是队列,那么为什么这里会有进栈出栈的方法呢?
由于Vue2.0以后每一个组件都会调用mountComponent,在其中创建一个watcher对象,如果组件有嵌套的话,当渲染A父件的时候发现,自己还有子组件吗,于是要先去渲染子组件,这时子组件的渲染过程就被挂载起来,父组件所对应的watcher对象也应该被存储到栈中(pushTarget),当子组件渲染完成后,会把自己对应的watcher从栈中弹出(popTarget),然后继续去执行父组件的渲染。
这时我们想到了什么?是不是想到了Vue中父子组件的生命周期?

  • 父beforeCreate->
  • 父created->
  • 父beforeMount->
  • 子beforeCreate->
  • 子created->
  • 子beforeMount->
  • 子mounted->
  • 父mounted
    (默默复习一遍父子组件生命周期,是不是恍然大悟)
③订阅者

在src\core\observer\watcher.js中

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {//是否是渲染Watcher
      vm._watcher = this//把渲染Watcher记录到_watcher
    }
    vm._watchers.push(this)//把渲染 watcher、计算属性、侦听器,记录到_watchers,存储所有的watcher
    
    if (options) {//Watcehr中最终要更新视图
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy//是否延迟更新视图,计算属性的Watcher:lazy为true 因为计算属性要在数据变化后才会更新视图
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid //订阅者唯一标识
    this.active = true//标识当前watcher是否是活动的
    this.dirty = this.lazy//dirty用于缓存计算属性watcher的值,计算属性watcher的lazy为true,这里初始化dirty为true,当计算属性watcher的getter触发时,会判断dirty,如果dirty为false,直接获取它的缓存值:watcher.value,如果dirty为true,则调用 watcher.evaluate -> watcher.get 获取,并把dirty设置false,当计算属性watcher的update触发时会将dirty设置为true
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    
    if (typeof expOrFn === 'function') {//设置watcher的getter
      this.getter = expOrFn 如果 expOrFn 是方法,直接设置为getter
    } else {
      this.getter = parsePath(expOrFn)//监听器的Watcher会传入字符串(侦听的属性),例如'person.name',parsePath('person.name')返回一个函数,用于获取person.name的值
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy//判断是否延迟执行,get中调用了当前组件的getter,并将this指向Vue实例,将get的返回值记录到 this.value中
      ? undefined
      : this.get()
  }

  get () {
    pushTarget(this)// 把当前的 Watcher 对象存入到 targetStack(target栈)中
    let value
    const vm = this.vm//缓存自己
    try {
      value = this.getter.call(vm, vm)//get方法的核心:用getter
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {//深度监听
        traverse(value)
      }
      popTarget()//订阅者出栈
      this.cleanupDeps()//释放依赖
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {//newDepIds newDeps用于存储watcher对象依赖的所有dep,最后会被清空,depIds deps 保留最终的结果,判断是否已经依赖了当前dep对象
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)//调用dep对象的addSub方法把当前watcher对象添加到subs大数组
      }
    }
  }

  cleanupDeps () {//清空newDepIds和newDeps
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  update () {
    /* istanbul ignore else */
    if (this.lazy) {//lazy默认为false
      this.dirty = true
    } else if (this.sync) {//sync默认为false
      this.run()
    } else {
      queueWatcher(this)//把当前watcher放到一个队列中
    }
  }

  run () {
    if (this.active) {
      const value = this.get()//调用watcher的get方法,并记录返回结果,对渲染watcher来说updateComponent是没有返回值的,所以渲染watcher在这里value = undefined
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        const oldValue = this.value//辞旧
        this.value = value//迎新
        if (this.user) { //判断如果当前是用户的watcher
          try {//就调用它的callback回调函数(例如侦听器对应的function),使用try catch 是避免用户定义的回调发生异常
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)//渲染 watcher的callback为空
        }
      }
    }
  }
}

由此我们看到如果数据更新后,监听器会调用 dep 对象的 notify 方法,而notify方法中调用了 watcher 的 update,update又调用了queueWatcher,先把watcher放到队列中,然后遍历这个队列,然后去调用每个watcher的run方法,run 方法中最终:

  • 渲染 watcher:调用watcher的updateComponent()函数,函数内部调用了 _render 和 _update 方法:
    • _render:调用用户传入的 render 或 编译器生成的 render,创建 VNode
    • _update:调用 patch 函数生成真实DOM,渲染到视图
  • 其他 watcher:调用用户定义的回调函数

在响应式实现的过程中还有一个很重要的解析器Compiler,我们会在后面的章节中单独讲解。

三、源码实战

以上从依赖收集到分发更新分析了一遍源码,自己也尝试撸一个demo,加深理解:

首先我根据自己的理解画一张导图:

2

准备一个空文件夹,新建一个html页面,添加init.js(后续章节讲解),observer.js,dep.js,watcher.js

3

结合导图,根据上面的理解,那么首先要对数据进行劫持监听,所以我们首先要设置一个监听器Observer,通过Object.defineProperty()方法来监听所有的属性,get收集依赖,当属性变化时,就需要用set发起通知,通知每个订阅者Watcher,看是否需要更新。因为属性可能是多个,所以可能会有多个订阅者,因此我们需要一个消息订阅器Dep用数组subs来收集这些订阅者,并在监听器Observer和订阅者Watcher之间进行统一的管理。由于在节点元素上可能存在一些指令,所以我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令初始化成一个订阅者Watcher,并替换模板数据并绑定相应的函数,这时候当订阅者Watcher接受到相应属性的变化,就会执行相对应的更新函数,从而更新视图,由于解析器原理比较复杂,本章这里只用watcher的callback函数直接返回更新后的dom,简单略过解析过程,具体的解析器原理将在以后的章节中详细讲解。

首先,写一个最简单的vue动态交互,即通过v-model进行双向绑定。

<!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="name">{{name}}</div>
</body>
    <script src="observe.js"></script>
    <script src="dep.js"></script>
    <script src="watcher.js"></script>
    <script src="init.js"></script>
    <script>
        var el = document.querySelector('#name');
        var Vue = new Vue({
            name:'五一假期'
        }, el, 'name');
    
        setTimeout(function() {//这里通过延时代替事件简化修改data属性值的过程
            Vue.data.name = '离端午不远了';
        },5000);
    </script>

1.实现一个监听器Observer

数据监听器的核心方法就是Object.defineProperty(),通过遍历循环对所有属性值进行监听,并对其进行Object.defineProperty()处理:


    function observe(data) {
        if(!data || typeof data !== 'object') {//判断data是否是对象
            return;
        }
        Object.keys(data).forEach(function(key){//循环调用核心方法给属性添加get和set
            defineReactive(data,key,data[key]);
        });
    }
    //给每个属性添加set和get
    function defineReactive(data,key,val) {//顾名思义 定义一个响应式的对象
        observe(val);//递归监视所有子属性(注意这里和3.0的不同,后边有时间我们对比一下,3.0并不是要递归所有子属性的)
        var dep = new Dep();//声明一个订阅器 由源码可见 每个属性值都对应一个dep
        Object.defineProperty(data, key, {//数据劫持的核心方法:JS原生方法,IE8以下都不行,3.0的proxy是IE都不可以,而且无法shim,即不能通过第几浏览器的方法来实现
            get: function() {//
                if(Dep.target) {   //判断是否需要添加订阅者,
                    dep.addSub(Dep.target);
                }
                return val;
            },
            set: function(newVal) {//监视变化,分发更新
                if (val === newVal) {
                    return;
                }
                val = newVal;
                console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
                dep.notify(); // 如果数据变化,通知所有订阅者
            }
        });
    }

我们将订阅器Dep添加一个订阅者设计在get里面,这是为了让Watcher在初始化时触发,因此判断是否需要需要添加订阅者,至于具体实现的方法,我们在下后面的章节详细讲解。在set方法中,如果函数变化,就会通知所有的订阅者,订阅者们将会执行相对应的更新函数,到目前为止,一个比较完善的Observer已经成型了,下面我们要写订阅者Watcher。

我们已经知道监听器Observer是在get函数中执行了添加订阅者的操作的,所以我们只需要在订阅者Watcher在初始化时触发相对应的get函数来执行添加订阅者的操作即可。那么我们只需要获取对应的属性值,就可以通过Object.defineProperty()触发对应的get了。

function Watcher(vm, exp, cb) {
    this.vm = vm;    //指向Vue的作用域
    this.exp = exp;  //绑定属性的key值
    this.cb = cb;    //闭包
    this.value = this.get();
}
 
Watcher.prototype = {
    update:function() {
        this.run();
    },
    run:function() {
        var value = this.vm.data[this.exp];
        var oldVal = this.value;
        if(value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm,value,oldVal);
        }
    },
    get:function() {
        Dep.target = this;                         // 缓存自己
        var value = this.vm.data[this.exp];  // 强制执行监听器里的get函数
        Dep.target = null;                         // 释放自己
        return value;
    }
}

实现简单的dep,收集watcher,并通知每个订阅者检查更新:

function Dep() {
    this.subs = [];//收集watcher对象的数组,收集依赖的容器
}
 
Dep.prototype = {                        
    addSub:function(sub) {
        this.subs.push(sub);      //将订阅者统一管理
    },
    notify:function() {
        this.subs.forEach(function(sub) {
            sub.update();        //通知每个订阅者检查更新
        })
    }
}
Dep.target = null;

还是缺少点什么,缺少一个init类,
后面我们只需要将订阅者Watcher和监听器Observer关联起来,就可以实现一个简单的双向绑定。因为本章节还没有设计指令解析器,所以对于模板数据我们都进行写死处理,假设模板上有一个节点元素,且id为’name’,并且双向绑定的绑定变量也是’name’,嵌在差值表达式中,如上图html:

<div id="name">{{name}}</div>
  • 上面我们提到在Vue构造函数中,调用了 src/core/instance/init.js 中的 _init 方法,在 _init 方法中调用了 src/core/instance/state.js中定义的 initState(vm):初始化 Vue 实例的状态,初始化了 data、props、methods等,initState(vm) 方法中判断:如果定义了 data 就调用 initData(vm)把 data 中的成员注入到 Vue 实例,并且把它转换成响应式对象。
  • 还提到src/core/instance/lifecycle.js中的mountComponent方法中,组件渲染前会调用它,因此每个组件都会对应new一个watcher(2.0版本)。
    因此我们需要在init中做两件事:
  • 实现监听
  • new一个wathcer
//将Observer和Watcher关联起来,实现简单的init
function littleVue(data, el, exp) {
    this.data = data;//获取data
    observe(data);//监控data
    el.innerHTML = this.data[exp];//给差值表达式赋值,模拟渲染过程
    new Watcher(this, exp, function(value) {//当前作用域,要监视的属性的key值,这里callback相当于从compiler中兜了一圈回来得到真实的修改后的dom
        el.innerHTML = value;//更新dom
    });
}

然后在页面上new一个littleVue,就可以实现双向绑定了:

<body>
    <div id="name">{{name}}</div>
</body>
    <script src="observe.js"></script>
    <script src="dep.js"></script>
    <script src="watcher.js"></script>
    <script src="init.js"></script>
    <script>
        var el = document.querySelector('#name');
        var littleVue= new littleVue({
            name:'五一假期'
        }, el, 'name');
    
        setTimeout(function() {
            alert('五一马上结束了')
            littleVue.data.name = '端午来了';
        },5000);
//我们在为属性赋值的时候形式是: 'littleVue.data.name = 'XXX' ',
//而我们理想的形式是:littleVue.name = 'YYY',那么怎么实现这种形式呢,
//只需要在new littleVue时做一个代理处理,让访问littleVue的属性代理为访
//问littleVue.data的属性,原理还是使用Object.defineProperty()对属性在外
//面再包装一层,具体会在后面的init章节详细讲解。
    </script>

启动页面,页面中显示"五一假期",过5秒后,点击完alert中的确定按钮,你会发现"端午来了",双向绑定成功。

附言:

由于Vue2.6版本相当于是Vue2.0到Vue3.0的一个过度版本,而3.0中响应式原理的核心方法是proxy(),我们看看proxy()到底比2.0中监视对象的方式好在了哪里,这里做一个简单的介绍:

首先我们看上面讲到的defineReactive()方法:

export function defineReactive (
    obj: Object,
    key: string,
    val: any,
    customSetter?: ?Function,
    shallow?: boolean
  ) {
    const dep = new Dep()// 创建依赖对象实例,为当前属性收集依赖
    ...
    let childOb = !shallow && observe(val)// 判断是否递归观察子对象(深度监听),则将子对象属性转换成响应式,并返回子观察对象
    Object.defineProperty(obj, key, {// 把属性转换成 getter/setter
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
	...
      },
      set: function reactiveSetter (newVal) {
        ...
      }
    })
}

该方法在shallow === ture的情况我们会在以后的章节中讲解,
这里针对data属性值,shallow值为false,位深度监听,因此我们看到observe(val)不管我用不用这个子属性val,都直接进行了递归处理。

下面我们再看看3.0中,proxy()一个经典的例子:

let obj={a:1,b:{c:2}};
let handler={
  get:function(obj,prop){
    const v = Reflect.get(obj,prop);
    if(v !== null && typeof v === 'object'){
      return new Proxy(v,handler);//代理内层
    }else{
      return v; // 返回obj[prop]
    }
  },
  set(obj,prop,value){
    return Reflect.set(obj,prop,value);//设置成功返回true
  }
};
let p=new Proxy(obj,handler);

p.a//会触发get方法
p.b.c//会先触发get方法获取p.b,然后触发返回的新代理对象的.c的set。

这里我们看到,默认proxy只代理外层属性,递归是在get方法中实现了,当中判断了子属性,如果子属性存在,且被访问到,才会执行递归操作,因此性能得到了提升。

因此本文作者认为,与其说对比Object.defineProperty()方法和proxy()方法的区别,不如说是实现方式的改进,因为其实本质上都是利用的Object.defineProperty(),只不过proxy()中Object.defineProperty()方法被定义到的Reflect对象中,proxy和reflect都是ES6的内容,由此可见Vue3.0是高度拥抱了ES6,包括TS。只不过是Vue作者将递归操作的时机优化了,区别就是这么简单,而严格意义上讲,我认为proxy的出现实质上是对defineReactive()方法或者说是对observe监听方式的优化,而不是像网上大多数文章讨论的竟然是Object.defineProperty()和proxy()的区别,这多少对读者会有一定的误导性,我认为那样说是不严谨的。

转载请注明出处

下期见!

参考资料:

1.深入Vue - 源码目录及构建过程分析

2.ES6 全套教程 ECMAScript6 (原著:阮一峰)

3.深入响应式原理

4.vue2.0原理的理解

  • 49
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

厉害坤坤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值