前置1:数据响应式核心

为什么要做响应式?

https://www.bilibili.com/video/BV1d4411v7UX 第1-5节响应式原理;
响应式解决开发中的什么问题?
在开发中,如果需要实现一个计算的功能 b = a * 10,b的结果依赖着a的变化,当a为1,b为10。然后修改a的值,此时b并不会发生数据变化。

  1. let a = 1;
  2. let b = a * 10; // b:10
  3. a = 2; // a:2
  4. b; //b的值依旧为10;

响应式数据就是实现一个函数,当a变化,b的值会自动变化,类型excel的表格中的函数,B1 = A1 * 10;通过函数计算就可以实时更新B1表格的值。

劫持对象的getter/setter方法

  1. // 用来判断参数是对象
  2. function isObject (obj) {
  3. return typeof obj === 'object'
  4. && !Array.isArray(obj)
  5. && obj !== null
  6. && obj !== undefined
  7. }
  8. function convert (obj) {
  9. // 如果传入的参数不是对象,直接抛出类型错误
  10. if (!isObject(obj)) {
  11. throw new TypeError()
  12. }
  13. // Object.keys 将对象的key取出,转为数组形式
  14. Object.keys(obj).forEach(key => {
  15. let rawVale = obj[key]; //初始化定义对象属性的原始值
  16. Object.defineProperty(obj, key, {
  17. get () {
  18. console.log(`getting key "${key}": ${rawVale}`);
  19. return rawVale; // 此处必须使用外层定义的rawVale,如果不定义原始值,直接获取值obj[key],则会出现死循环(一直getter下去)。
  20. },
  21. set (value) {
  22. console.log(`setting key "${key}" to: ${rawVale}`);
  23. rawVale = value;
  24. }
  25. })
  26. })
  27. }
  28. let o = {foo: 1};
  29. convert(o);
  30. console.log(o.foo)
  31. o.foo = 99;
  32. console.log(o.foo);

Dep的依赖depend和通知notify

  1. // 创建Dep类
  2. class Dep {
  3. constructor() {
  4. // 创建观察者对象
  5. this.subscribers = new Set()
  6. }
  7. // 收集依赖
  8. depend() {
  9. if (activeUpdate) {
  10. this.subscribers.add(activeUpdate)
  11. }
  12. }
  13. // 触发更新
  14. notify() {
  15. this.subscribers.forEach(subscriber => subscriber())
  16. }
  17. }
  18. // 定义全局中正在更新的函数activeUpdate
  19. let activeUpdate
  20. function effectUpdate(update) {
  21. function wrappedUpdate() {
  22. // wrappedUpdate赋值给activeUpdate,可以把函数添加到观察者的队列中
  23. activeUpdate = wrappedUpdate
  24. update()
  25. activeUpdate = null
  26. }
  27. wrappedUpdate()
  28. }
  29. let dep = new Dep();
  30. effectUpdate(() => {
  31. dep.depend()
  32. console.log('update');
  33. })
  34. // 调用notify时,会触发收集的activeUpdate函数
  35. dep.notify();

结合对象的getter/setter及Dep类,实现mini版响应式

  1. function isObject (obj) {
  2. return typeof obj === 'object'
  3. && !Array.isArray(obj)
  4. && obj !== null
  5. && obj !== undefined
  6. }
  7. function observe (obj) {
  8. if (!isObject(obj)) {
  9. throw new TypeError()
  10. }
  11. Object.keys(obj).forEach(key => {
  12. let internalValue = obj[key]
  13. let dep = new Dep()
  14. Object.defineProperty(obj, key, {
  15. get () {
  16. dep.depend()
  17. return internalValue
  18. },
  19. set (newValue) {
  20. const isChanged = internalValue !== newValue
  21. if (isChanged) {
  22. internalValue = newValue
  23. dep.notify()
  24. }
  25. }
  26. })
  27. })
  28. }
  29. class Dep {
  30. constructor () {
  31. this.subscribers = new Set()
  32. }
  33. depend () {
  34. if (activeUpdate) {
  35. // register the current active update as a subscriber
  36. this.subscribers.add(activeUpdate)
  37. }
  38. }
  39. notify () {
  40. // run all subscriber functions
  41. this.subscribers.forEach(subscriber => subscriber())
  42. }
  43. }
  44. let activeUpdate
  45. function autorun (update) {
  46. function wrappedUpdate () {
  47. activeUpdate = wrappedUpdate
  48. update()
  49. activeUpdate = null
  50. }
  51. wrappedUpdate()
  52. }
  53. let state = {
  54. count : 0,
  55. name: 'raw'
  56. }
  57. observe(state);
  58. // F1,该函数内部收集了两个属性的依赖,当两个属性都发生改变,此函数会执行两次,第一次更新count,第二次更新name
  59. autorun(()=>{
  60. // 触发getter方法
  61. console.log('-----------');
  62. console.log(state.count)
  63. console.log(state.name)
  64. })
  65. // F2,只收集了name的依赖
  66. autorun(()=>{
  67. console.log('///////////');
  68. console.log(state.name)
  69. })
  70. // 触发setter方法中的dep.notify(),会将所有的依赖项进行更新
  71. state.count = 90;
  72. state.name = 'change';

vue2使用defineProperty实现响应式

只处理一个属性

  1. let data = {
  2. msg: "hello world"
  3. };
  4. let vm = {}
  5. // 数据劫持,给vm的msg设置成get/set方法,在设置属性或获取属性时可以加入一些操作
  6. Object.defineProperty(vm, "msg",{
  7. enumerable: true,
  8. configurable: true,
  9. get(){
  10. return data.msg
  11. },
  12. set(newVal){
  13. data.msg = newVal
  14. }
  15. })
  16. console.log(vm.msg)
  17. vm.msg = "change"

经过数据转化getter/setter,访问数据和设置数据可以加入特殊操作,把data的msg属性,代理到了vm对象上。

处理多个属性时

当data数据有多个属性成员时,需要遍历操作。可以把上面转化响应式的方法封装成函数。

  1. let data = {
  2. msg: "hello world",
  3. count: 0,
  4. };
  5. let vm = {}
  6. function proxyData(data) {
  7. Object.keys(data).forEach(item=>{
  8. // 定义响应式对象,必须有一个中介对象vm,否则会进入死循环
  9. Object.defineProperty(vm, item, {
  10. enumerable: true,
  11. configurable: true,
  12. get(){
  13. return data[item];
  14. },
  15. set(newVal){
  16. if(newVal === data[item] ) return;
  17. data[item] = newVal;
  18. // 数据值变化,更新dom的值
  19. document.querySelector("#app").textContent = data[item];
  20. }
  21. })
  22. })
  23. }
  24. proxyData(data);
  25. vm.msg = "change msg"

⚠️注意:在设置getter的返回值时,这个数据不能是Object.defineProperty定义的对象,即不能和第一个参数对象相同,否则会进入死递归循环。

vue3使用Proxy实现响应式

Proxy是把data对象整体设置为响应式,不用对data成员进行遍历,性能很大提升

  • reactive使用的proxy原理
  • ref使用的defineProperty,这也是ref可以给基本类型做响应式数据的原理。但是ref必要加.value ```javascript // 数据响应式,在vue3中使用proxy对象,代理整个data对象,不需要递归 let data = { msg: “hello”, count:0 } let vm = new Proxy(data, { get(target, key) {
    1. return target[key];
    }, set(target, key, value) {
    1. if(value === target[key]) return;
    2. target[key] = value;
    } })

vm.msg = “update”; console.log(vm.msg);

  1. Proxy代理的是data对象本身,而不是data属性成员。
  2. <a name="yDtCg"></a>
  3. # 前置2:发布订阅和观察者模式
  4. <a name="ySSIN"></a>
  5. ## 发布订阅模式
  6. vue$emit,以及EventBus事件处理,都是发布订阅模式。发布订阅包含发布者/订阅者/事件处理中心。
  7. ```javascript
  8. // events = { "update":[f1,f2], "change":[m1,m2]}
  9. class EventBus{
  10. constructor(){
  11. // 观察者和发布者,建立关系的事件中心
  12. this.events = Object.create(null);
  13. }
  14. // 观察者
  15. $on(eventType, handler){
  16. this.events[eventType] = this.events[eventType] || [];
  17. // 添加观察者
  18. this.events[eventType].push(handler);
  19. }
  20. // 发布事件,通知所有观察者
  21. $emit(eventType){
  22. this.events[eventType].forEach(event=>{
  23. event()
  24. })
  25. }
  26. }
  27. let bus = new EventBus();
  28. bus.$on("haha",()=>{
  29. console.log("haha111");
  30. })
  31. bus.$on("haha",()=>{
  32. console.log("haha222");
  33. })
  34. bus.$emit("haha")

观察者模式

vue的响应式数据采用的观察者模式,包含发布者/观察者。

  1. // 观察者模式
  2. // 没有事件处理中心
  3. // 观察者watcher,包含update方法
  4. // 发布者dep,包含addDep和notify方法
  5. class Dep {
  6. constructor(){
  7. this.deps = [];
  8. }
  9. addDep(dep){
  10. if(dep.update){
  11. this.deps.push(dep)
  12. }
  13. }
  14. notify(){
  15. this.deps.forEach(dep => dep.update())
  16. }
  17. }
  18. // 观察者
  19. class Watcher{
  20. update(){
  21. console.log("watch update");
  22. }
  23. }
  24. let w1 = new Watcher();
  25. let w2 = new Watcher();
  26. let dep = new Dep();
  27. dep.addDep(w1)
  28. dep.addDep(w2)
  29. dep.notify();

发布订阅模式和观察者模式区别

vue2源码分析 - 图1

mini版vue2实现框架

vue2的核心模块
vue2源码分析 - 图2
Vue提供的render函数来实现vnode,也就是h函数,可以转换成VNode的函数

  1. render(){
  2. let { h } = Vue;
  3. return h('div',{ title : this.title , message : 'you' },[
  4. h('input',{ value : this.title , onInput : (ev)=>{ this.title = ev.target.value } }),
  5. h('button',{ onClick : this.inc },'点击')])
  6. }

AST和VNode的区别

AST和VNode的职责是不同的,不能进行等价划分;

  • 即AST是compiler中把模板编译成有规律的数据结构,方便转换成render函数所存在的;
  • VNode是优化DOM操作的,减少频繁DOM操作的,提升DOM性能的。可以进行dom diff

转化过程 template > ast > render function > 执行 render function > VNode

vue2源码分析 - 图3
vue的实现主要包括五个功能类:

  • Vue:把data中的成员注入到vue实例,并同时转为getter/setter
  • Observer:能够对数据对象的所有属性进行监听,有变化立刻通知Dep类
  • Compiler:解析编译特殊标签,如:指令/差值表达式
  • Dep:设置发布者,Dep类在数据转为响应式数据时调用,并在getter方法内实现收集依赖。Dep类有addSub和notify方法。
  • Watcher:data成员的每个响应式数据,都是一个watcher对象,在Watcher类内部通过设置Dep.target=this, 将watcher实例和Dep类关联起来。 watcher类有update方法更新视图。

    vue2依赖收集和派发更新

    Watch和Dep的关系。Watcher内 部收集有Dep,Dep内也收集有Watcher,是多对多的关系。
    1. class Watcher{
    2. constructor(vm, exprOrFn, callback, options){
    3. this.deps = []; //watcher中存放着dep
    4. this.depsId = new Set(); //为了避免dep重复,设置为Set数据结构
    5. this.getter = expOrFn; //内部传递过来的回调函数,放在getter属性上。在get方法里执行
    6. }
    7. get() {
    8. pushTarget(this); // 把当前的watcher存起来
    9. let value = this.getter.call(this.vm); //渲染watcher执行
    10. popTarget(); //移除watcher
    11. return value;
    12. }
    13. //【4444444】
    14. // 通过Dep.target.addDep(this)走到这里
    15. addDep(dep){
    16. let id = dep.id;
    17. if(!this.depsId.has(id)){
    18. this.depsId.add(id);
    19. this.deps.push(dep); //在watcher中存放dep的动作
    20. // 下面这句,又回到Dep类的addSub方法中,作用是给dep添加watcher
    21. dep.addSub(this);
    22. }
    23. }
    24. update(){
    25. this.get();
    26. }
    27. }
  1. let id = 0;
  2. class Dep{
  3. constructor(){
  4. this.id = id++;
  5. this.subs = []; //dep中存放watcher
  6. }
  7. depend(){
  8. // 【3333333】
  9. if(Dep.target){ //如果类中存在 Dep.target,说明存在watcher】
  10. // 这一句的调用,说明是watcher的addDep方法,进入到Watcher中
  11. Dep.target.addDep(this);// 让watcher,去存放dep,this表示当前dep
  12. }
  13. }
  14. // 【这里是触发更新的过程】
  15. notify(){
  16. this.subs.forEach(watcher=>watcher.update());
  17. }
  18. // 【5555555】
  19. // addSub是往subs数组中添加watcher
  20. addSub(watcher){
  21. this.subs.push(watcher); //dep存放watcher的动作
  22. }
  23. }
  24. let stack = [];
  25. export function pushTarget(watcher){
  26. // 【1111111】
  27. Dep.target = watcher; // 通过给Dep类设置target属性,并赋值为watcher
  28. stack.push(watcher);
  29. }
  30. export function popTarget(){
  31. stack.pop();
  32. Dep.target = stack[stack.length-1];
  33. }
  34. export default Dep;

响应式数据定义时

  1. let dep = new Dep();
  2. Object.defineProperty(data, key, {
  3. get() {
  4. // 【222222】
  5. if(Dep.target){ // 如果取值时有watcher
  6. dep.depend(); // 让watcher保存dep,并且让dep 保存watcher
  7. }
  8. return value
  9. },
  10. set(newValue) {
  11. if (newValue == value) return;
  12. observe(newValue);
  13. value = newValue;
  14. dep.notify(); // 通知渲染watcher去更新
  15. }
  16. });

图解分析依赖收集和派发更新的过程

vue2源码分析 - 图4
vue2源码分析 - 图5

watch的实现

watch是依赖watcher实现的,通过传入参数user,将渲染watcher设置为用户watcher。渲染的是用户传递的回调函数。
改写watcher,第5行,获取到是用户watcher。第30行,执行用户传入的回调函数

  1. class Watcher {
  2. constructor(vm, exprOrFn, callback, options) {
  3. // ...
  4. // 通过options读取到user属性
  5. this.user = !! options.user
  6. if(typeof exprOrFn === 'function'){
  7. this.getter = exprOrFn;
  8. }else{
  9. this.getter = function (){ // 将表达式转换成函数
  10. let path = exprOrFn.split('.');
  11. let obj = vm;
  12. for(let i = 0; i < path.length;i++){
  13. obj = obj[path[i]];
  14. }
  15. return obj;
  16. }
  17. }
  18. this.value = this.get(); // 将初始值记录到value属性上
  19. }
  20. get() {
  21. pushTarget(this); // 把用户定义的watcher存起来
  22. const value = this.getter.call(this.vm); // 执行函数 (依赖收集)
  23. popTarget(); // 移除watcher
  24. return value;
  25. }
  26. run(){
  27. let value = this.get(); // 获取新值
  28. let oldValue = this.value; // 获取老值
  29. this.value = value;
  30. if(this.user){ // 如果是用户watcher 则调用用户传入的callback
  31. this.callback.call(this.vm,value,oldValue)
  32. }
  33. }
  34. }

computed的实现

computed计算属性的实现也是依赖watcher,通过传入的lazy属性和dirty属性。computed是懒执行的,初次加载并不会执行。

  • lazy属性用来判断是computed的watcher
  • dirty属性用来设置computed的更新时间,
    • 如果dirty是true则更新,说明依赖的属性发生了变化
    • 如果是false,则直接读取缓存,不在进行计算。

computed比watch,有缓存功能。watch可以检测到每次值的变化,能够获取到旧值和新值。

  1. function initComputed(vm, computed) {
  2. // 存放计算属性的watcher
  3. const watchers = vm._computedWatchers = {};
  4. for (const key in computed) {
  5. const userDef = computed[key];
  6. // 获取get方法
  7. const getter = typeof userDef === 'function' ? userDef : userDef.get;
  8. // 通过传入{lazy: true}属性,创建计算属性watcher
  9. watchers[key] = new Watcher(vm, userDef, () => {}, { lazy: true });
  10. defineComputed(vm, key, userDef)
  11. }
  12. }
  1. class Watcher {
  2. constructor(vm, exprOrFn, callback, options) {
  3. this.vm = vm;
  4. this.dirty = this.lazy
  5. // ...
  6. this.value = this.lazy ? undefined : this.get(); // 调用get方法 会让渲染watcher执行
  7. }
  8. }

计算属性需要保存一个dirty属性,用来实现缓存功能

  1. function defineComputed(target, key, userDef) {
  2. if (typeof userDef === 'function') {
  3. sharedPropertyDefinition.get = createComputedGetter(key)
  4. } else {
  5. sharedPropertyDefinition.get = createComputedGetter(userDef.get);
  6. sharedPropertyDefinition.set = userDef.set;
  7. }
  8. // 使用defineProperty定义
  9. Object.defineProperty(target, key, sharedPropertyDefinition)
  10. }
  11. function createComputedGetter(key) {
  12. return function computedGetter() {
  13. const watcher = this._computedWatchers[key];
  14. if (watcher) {
  15. if (watcher.dirty) { // 如果dirty为true
  16. watcher.evaluate();// 计算出新值,并将dirty 更新为false
  17. }
  18. if (Dep.target) { // 计算属性在模板中使用 则存在Dep.target
  19. watcher.depend()
  20. }
  21. // 如果依赖的值不发生变化,则返回上次计算的结果
  22. return watcher.value
  23. }
  24. }
  25. }

修改watcher中的方法evaluate和update

  1. evaluate() {
  2. this.value = this.get()
  3. this.dirty = false
  4. }
  5. update() {
  6. if (this.lazy) {
  7. this.dirty = true;
  8. } else {
  9. queueWatcher(this);
  10. }
  11. }

Vue类

功能

  • 负责接收初始化的参数options
  • 把data中属性成员注入到vue实例,转化成getter/setter
  • 调用observer监听data中所有属性变化
  • 调用compiler解析差值表达式或指令等特殊标签

    结构

    vue2源码分析 - 图6

    代码

    vue.js,创建Vue类 ```javascript class Vue{ constructor(options){

    1. // 设置this.$options,保存选项options的数据
    2. this.$options = options;
    3. this.$data = options.data;
    4. this.$el = typeof options.el === 'string' ? document.querySelector(options.el) :options.el
    5. // 把data中的数转化成getter和setter,注入vue实例中
    6. this._proxyData(this.$data);
    7. // 调用observer对象,监听数据变化
    8. new Observer(this.$data);
    9. // 调用compiler对象,解析指令和差值表达式
    10. new Compiler(this)

    } // 让Vue实例代理data的属性,经过该操作,vue实例对就可以直接访问data中的属性 _proxyData(data){

    1. // 遍历data中所有属性
    2. Object.keys(data).forEach((key)=>{
    3. // 要代理的对象是this即vue实例,把data中的属性都代理到vue实例上
    4. Object.defineProperty(this, key, {
    5. enumerable:true,
    6. configurable: true,
    7. get(){
    8. console.log("proxy");
    9. return data[key];
    10. },
    11. set(newVal){
    12. if(newVal === data[key]) return;
    13. data[key] = newVal;
    14. }
    15. })
    16. })

    } }

  1. <a name="prf2n"></a>
  2. ## Observer类
  3. <a name="apeOb"></a>
  4. ### 功能
  5. 1. 把data中属性成员转化成getter/setter响应式数据
  6. 2. data中的属性的值也是对象,递归把该属性的value也转换成响应式数据
  7. 3. 数据变化发送通知
  8. <a name="mz1DT"></a>
  9. ### 结构
  10. ![](https://cdn.nlark.com/yuque/0/2021/jpeg/737887/1625377184679-bb1d5022-d33e-403e-b910-d3cc8d38eb82.jpeg)
  11. <a name="P3LWT"></a>
  12. ### 代码
  13. ```javascript
  14. // 响应式对象,可以随时观察data属性数据的变化
  15. class Observer{
  16. constructor(data){
  17. // 将data转为响应式
  18. this.walk(data);
  19. }
  20. walk(data){
  21. // 判断data是否存在,判断data的类型为对象
  22. if(!data || typeof data !== 'object') return;
  23. Object.keys(data).forEach(key=>{
  24. this.defineReactive(data, key);
  25. })
  26. }
  27. defineReactive(obj, key){
  28. let _self = this;
  29. // 创建Dep实例对象,建立依赖对象(发布者)
  30. let dep = new Dep()
  31. // 创建临时中间变量value,可以在getter的return中使用
  32. let value = obj[key];
  33. // ⭐️如果data下属性的数据仍是对象,需要把对象下的属性继续执行响应式
  34. this.walk(value)
  35. // 和_proxy方法很大的区别是第一个参数,_proxy第一个参数是vue实例,该处的第一个参数是data对象
  36. Object.defineProperty(obj, key, {
  37. enumerable: true,
  38. configurable: true,
  39. get(){
  40. console.log("observer");
  41. // 收集依赖,Dep.target是watcher的实例对象
  42. Dep.target && dep.addSub(Dep.target)
  43. // 此处return的结果不能是obj[key],因为第一个参数对象是obj
  44. return value;
  45. },
  46. set(newValue){
  47. if(newValue === value) return;
  48. value = newValue;
  49. // ⭐️给data的属性赋值为一个对象,需要将该对象转为响应式,如果赋值的是基本类型,则不是响应式
  50. _self.walk(newValue)
  51. // 监听数据的变化,发送通知
  52. dep.notify()
  53. }
  54. })
  55. }
  56. }

Compiler类

功能:

  1. 编译模板,解析指令/差值表达式
  2. 页面的首次渲染
  3. 数据变化后重新渲染视图

    结构

    vue2源码分析 - 图7

    代码

    ```javascript // 编译特殊语法 class Compiler{ constructor(vm){

    1. this.vm = vm;
    2. this.el = vm.$el;
    3. // 创建实例对象后,立即编译模板
    4. this.compile(this.el)

    } // 编译模版,处理文本节点和元素节点 compile(el){

    1. let childNode = el.childNodes;
    2. Array.from(childNode).forEach(item =>{
    3. // 处理文本节点
    4. if(this.isTextNode(item)){
    5. this.compileText(item);
    6. }
    7. // 处理元素节点
    8. else if(this.isElementNode(item)){
    9. this.compileElement(item);
    10. }
    11. // 判断item是否有子节点,如果有子节点,递归调用compile
    12. if(item.childNodes && item.childNodes.length>0){
    13. this.compile(item)
    14. }
    15. })

    } // 编译元素节点,处理指令 compileElement(node){

    1. // 获取属性节点
    2. // console.log(node.attributes);
    3. // 遍历所有的属性节点
    4. Array.from(node.attributes).forEach((item)=>{
    5. /*
    6. 解析v-text="msg"
    7. attrName : v-text
    8. key : msg
    9. */
    10. // 判断属性是否是指令
    11. let attrName = item.name;
    12. let keyVal = item.value;
    13. if(this.isDirective(attrName)){
    14. // 将指令v-去掉,v-text转为text,v-model转为model
    15. attrName = attrName.substr(2)
    16. this.update(node, keyVal, attrName)
    17. }
    18. })

    }

/ 适配器设计模式 / update(node, key, attrName){ let updateFn = this[attrName + ‘Updater’] updateFn && updateFn.call(this, node, this.vm[key], key) }

  1. // 处理v-text指令
  2. textUpdater(node, value, key){
  3. node.textContent = value
  4. /* ⭐️指令v-text创建watcher对象,当数据改变更新视图 */
  5. new Watcher(this.vm, key, (newValue)=>{
  6. node.textContent = newValue
  7. })
  8. }
  9. // 处理v-model指令
  10. modelUpdater(node, value, key){
  11. node.value = value
  12. /* ⭐️指令v-model创建watcher对象,当数据改变更新视图 */
  13. new Watcher(this.vm, key, (newValue)=>{
  14. node.value = newValue
  15. })
  16. // 改变输入框的值,视图发生变化,双向绑定
  17. node.addEventListener("input", ()=>{
  18. this.vm[key] = node.value
  19. })
  20. }
  21. // 编译文本节点,处理差值表达式{{ msg }}
  22. compileText(node){
  23. // console.dir(node,"compileText");
  24. // ⭐️使用正则表达式处理差值表达式,差值表达式可能有多个括号,并且要把中间的值提取出来
  25. let reg = /\{\{(.+?)\}\}/
  26. // 获取文本节点内容
  27. let value = node.textContent;
  28. if(reg.test(value)){
  29. // 获取差值表达式中的值
  30. let key = RegExp.$1.trim();
  31. // 替换差值表达式的值
  32. node.textContent = value.replace(reg, this.vm[key]);
  33. /* ⭐️差值表达式创建watcher对象,当数据改变更新视图 */
  34. new Watcher(this.vm, key, (newValue)=>{
  35. node.textContent = newValue;
  36. })
  37. }
  38. }
  39. // 判断是否是指令
  40. isDirective(attrName){
  41. // 属性以v-开头则为指令
  42. return attrName.startsWith("v-")
  43. }
  44. // 判断节点是否是文本
  45. isTextNode(node){
  46. return node.nodeType === 3;
  47. }
  48. // 判断节点是否是元素节点
  49. isElementNode(node){
  50. return node.nodeType === 1;
  51. }

}

  1. <a name="f5Dz7"></a>
  2. ## Dep类
  3. <a name="Y8ypa"></a>
  4. ### 功能
  5. - 收集依赖,添加观察者watcher
  6. - 通知所有观察者
  7. <a name="ZWMf3"></a>
  8. ### 结构
  9. ![](https://cdn.nlark.com/yuque/0/2021/jpeg/737887/1625381776474-036b0a05-911b-42f0-9cb3-9514e55e7843.jpeg)
  10. <a name="CXk2V"></a>
  11. ### 代码
  12. ```javascript
  13. class Dep{
  14. constructor(){
  15. this.subs=[]
  16. }
  17. //添加观察者
  18. addSub(sub){
  19. if(sub&&sub.update){
  20. this.subs.push(sub)
  21. }
  22. }
  23. // 给每个观察者发送通知
  24. notify(){
  25. this.subs.forEach(sub=>sub.update())
  26. }
  27. }

Watcher类

功能

  • 当数据发生变化触发依赖,dep通知所有的Watcher实例更新视图
  • 自身实例化的时候,给Dep.target=this,往dep对象中添加自己

    结构

    vue2源码分析 - 图8

代码

  1. // 监测数据变化,并更新视图,在compiler中调用
  2. class Watcher{
  3. constructor(vm, key, cb){
  4. this.vm = vm;
  5. // data中属性名
  6. this.key = key;
  7. // 由于更新视图的形式不同,可以传递cb回调函数处理不同的方法,cb负责更新视图:
  8. // 如:差值表达式更新的是textContent,v-model指令需要更新input元素的value
  9. this.cb = cb;
  10. /* ⭐️⭐️⭐️
  11. watcher和dep建立关系
  12. 把watcher对象记录到Dep类的静态属性target上
  13. */
  14. Dep.target = this;
  15. // 通过访问vm中的属性,就会触发get方法,在get方法中调用addSub,下面的vm[key]正好执行
  16. this.oldValue = vm[key];
  17. // 调用过addSub,把watcher添加到dep的sub之后,需要将watcher置空,以便下次使用
  18. Dep.target = null;
  19. }
  20. // 当数据发生变化时,更新视图
  21. update(){
  22. let newVal = this.vm[this.key]
  23. if(newVal === this.oldValue) return;
  24. this.cb(newVal)
  25. }
  26. }

测试文件index.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>vue-mini demo</title>
  8. </head>
  9. <body>
  10. <div id="app">
  11. <h1>差值表达式</h1>
  12. <h3>{{msg}}</h3>
  13. <h3>{{count}}</h3>
  14. <h1>v-text</h1>
  15. <div v-text="msg"></div>
  16. <h1>v-model</h1>
  17. <input type="text" v-model="msg"><br/>
  18. <input type="text" v-model="count">
  19. </div>
  20. <script src="./js/dep.js"></script>
  21. <script src="./js/watcher.js"></script>
  22. <script src="./js/compiler.js"></script>
  23. <script src="./js/observer.js"></script>
  24. <script src="./js/vue.js"></script>
  25. <script>
  26. let app = new Vue({
  27. el:"#app",
  28. data: {
  29. msg:"vue mini",
  30. count:123,
  31. person: {
  32. name:"John",
  33. age:10
  34. }
  35. }
  36. });
  37. // app.msg = {
  38. // text:"msg"
  39. // }
  40. // console.log(app.$data.msg,"msg");
  41. </script>
  42. </body>
  43. </html>

数据双向绑定

在compiler类中,处理v-model指令时,设置事件处理方法

  1. // 改变输入框的值,视图发生变化,双向绑定
  2. node.addEventListener("input", ()=>{
  3. this.vm[key] = node.value
  4. })

锤子分析vue源码

锤子科技前端工程师:分析vue源码
https://www.zhihu.com/people/zhaoliangliang/posts?page=3