手写Vue2响应式原理

首先vue创建实例过程一般如下:

js 复制代码
import Vue from 'vue';//这个Vue是需要实现的构造函数

let vm = new Vue({
    el:'#app', 
    data(){
        return { 
            msg:'hello',
            profile:{name:'zs',age:18},
            arr:[{a:1},2,3]
        }
    },
    computed:{

    },
    watch:{

    }
});

Vue构造函数

js 复制代码
// index.js
import {initState} from './observe'
function Vue(options){ // vue中原始用户传入的数据
    this._init(options); // 初始化vue 并且将用户选项传入
}

Vue.prototype._init = function (options) {
    // vue中初始化  this.$options 表示的是vue中参数
    let vm = this;
    vm.$options = options;

    // 数据初始化,包含有data computed watch
    initState(vm); 

    ...
}
export default Vue

initState数据初始化函数,需要根据不同的数据进行初始化(就是做响应式和一些其他初始化)

js 复制代码
// /observe/index.js
export function initState(vm){ 
    //做不同的初始化工作 
    let opts = vm.$options;
    if(opts.data){
        initData(vm); // 初始化数据
    }
    if(opts.computed){
        initComputed(); // 初始化计算属性
    }
    if(opts.watch){
        initWatch(); // 初始化watch
    }
}
function initData(vm){
    ...
}
function initComputed(){
    ...
}
function initWatch(){
    ...
}

首先开始对initData进行初始化

js 复制代码
// /observe/index.js
import Observer from './observer';

function initData(vm){ // 将用户插入的数据 通过object.defineProperty重新定义
    let data = vm.$options.data; // 用户传入的data
     //根据data是函数形式还是对象形式进行处理,data数据挂载到vm实例的_datas属性
    data = vm._data = typeof data === 'function'?data.call(vm) :data || {}
    for(let key in data){
         proxy(vm,'_data',key); //会将对vm上的取值操作和赋值操作代理给vm._data 属性
    }
    observe(data); // 观察数据,做响应式
}

export function observe(data){
    if(typeof data !== 'object' || data == null) {
        return // 不是对象或者是null 我就不用执行后续逻辑
    }
    return new Observer(data)//Observer里面实现数据劫持
}
function proxy(vm,source,key){ // 代理数据  vm.msg = vm._data.msg
    Object.defineProperty(vm,key,{
        get(){
            return vm[source][key]
        },
        set(newValue){
            vm[source][key] = newValue
        }
    })
}

这里proxy主要是将通过vm的操作代理data中的数据,data中的所有数据都会出现在vm上面,例如data中有msg属性,可以直接通过vm.msg获取,或者修改msg的值,vm.msg='world'。

Observer类的实现

observer类作用:实现数据响应式,只要将需要做响应式的数据传入即可

js 复制代码
// /observe/observer.js
class Observer { 
    constructor(data){ // data 就是最初定义的data
          this.walk(data);
    }
    walk(data){ 
          Object.keys(data).forEach(key=>{
            defineReactive(data,key,data[key]);
        })
    }
}
export default Observer

defineReactive是做数据劫持的一个方法,利用了 Object.defineProperty,对每个属性做劫持

对象数据劫持

js 复制代码
// /observe/observer.js
import { observe } from "./index";
class Observer { 
    constructor(data){ // data 就是最初定义的data
          this.walk(data);
    }
    walk(data){ 
          Object.keys(data).forEach(key=>{
            defineReactive(data,key,data[key]);
        })
    }
}

export function defineReactive(data, key, value) {
  // 定义响应式的数据变化
  
  // 如果value 依旧是一个对象的话 需要深度观察value:{school:{name:'zs',age:18}}
  observe(value); 
  //这里的 observe(value); 是上面proxy函数的上一个observe函数,做观察数据用
  
  Object.defineProperty(data, key, {
    get() {
      console.log("获取数据");
      return value;
    },
    set(newValue) {
      if (newValue === value) return;
      observe(newValue); // 如果你设置的值是一个对象的话 应该在进行监控这个新增的对象
      console.log("设置数据");
      value = newValue;
    },
  });
}
export default Observer

解释上面defineReactive函数中的observe(value)调用:假如当前value是一个对象,比如{school:{name:'zs',age:18}},调用defineReactive的时候就会先进入observe,里面就会判断if(typeof data !== 'object' || data == null),很明显,这个是一个对象,所以会执行下面的return new Observer(data),然后调用walk函数,接着就是遍历school里面的每个key值,并做数据劫持defineReactive。

然后等到observe执行完成之后,就会开始执行observe下面的Object.defineProperty,这个时候是对school这个对象本身的劫持,如果发生vm.school={},赋值为一个新对象,那么就会劫持到school本身的变化了。

对于set函数中设置了新值,也需要进行observe,进行响应式处理。

数组劫持

数组劫持主要拦截对数组本身有改变的方法,例如push shift unshift pop reverse sort splice。

js 复制代码
// /observe/array.js
import { observe } from ".";
//为了避免对数组的原型产生影响,需要拷贝它的原型出来,并指定为你定义的数组的原型
let oldArrayProtoMethods = Array.prototype;

// 拷贝的一个新的对象 可以查找到 老的方法
export let arrayMethods = Object.create(oldArrayProtoMethods);

let methods = [
    'push',
    'shift',
    'pop',
    'unshift',
    'reverse',
    'sort',
    'splice'
];

export function observerArray(inserted){ // 对数组中每一项进行观测,因为数组中有些项可能是对象,例如arr:[{a:1},2,3]
    for(let i = 0 ; i < inserted.length;i++){
        observe(inserted[i]); //这里的observe也是上面观察数据的方法
    }
}

methods.forEach(method=>{   // arr.push(1,2,3)  args=[1,2,3]
    arrayMethods[method] = function (...args) { //args参数是调用数组方法加入的数值,例如push(1,2,3)

        let r = oldArrayProtoMethods[method].apply(this,args);

        let inserted;//定义一个变量存放新增内容,这个内容如果是对象需要做响应式
        //只有下面这三个方法可能存在新增内容,其他没有
        switch (method) { 
            case 'push':
            case 'unshift':
                inserted = args;
                break;
            case 'splice':
                //例如arr.splice(0,1,4)
                inserted = args.slice(2);  // 获取splice 新增的内容
            default:
                break;
        }
        //如果有新增,需要做观察
        if(inserted) observerArray(inserted);
        console.log('调用了数组更新的方法')
        return r;
    }
})

上面对数组方法做了拦截处理,但是还没用起来,所以需要在Observer类中做判断处理

js 复制代码
// /observe/observer.js
class Observer { 
    constructor(data){ // data 就是最初定义的data
    
      if (Array.isArray(data)) {
          // 只能拦截数组的方法 ,数组里的每一项 还需要去观测一下
          data.__proto__ = arrayMethods; //修改数组的原型,让他调用方法时找到的都是经过自己重写的方法
          observerArray(data); // 观测数据中的每一项
    } else {
      this.walk(data);
    }
    }
    walk(data){ 
          Object.keys(data).forEach(key=>{
            defineReactive(data,key,data[key]);
        })
    }
}

此后对于数组使用push,pop等改变数组的方法都是可以检测得到的,而通过数组下标修改时检测不到的。

模板编译compiler

html 复制代码
     <div id="app">
        {{msg}}
        <div>
            <div>
                名字 {{profile.name}}
            </div>
            <div>
                年龄 {{profile.age}}
            </div>
        </div>
   </div>

做完数据响应式之后,需要解析这个模板语法,例如{{msg}},将他替换成data中的数据

js 复制代码
// index.js
Vue.prototype._init = function (options) {
     // vue中初始化  this.$options 表示的是vue中参数
    let vm = this;
    vm.$options = options;

    // 数据初始化,包含有data computed watch
    initState(vm); 
    
    //需要判断el用户是否传入
    if(vm.$options.el){
        vm.$mount();//挂载组件
    }
}

$mount方法实现,主要实现挂载元素和更新视图

js 复制代码
// index.js
import {compiler} from './util'
Vue.prototype.$mount = function () {
    let vm = this;
    let el = vm.$options.el; // 获取元素 #app
    el = vm.$el= query(el); // 获取当前挂载的节点 vm.$el就是要挂载的一个元素

    let updateComponent = ()=>{ // 更新组件 、渲染的逻辑
        vm._update(); // 更新组件
    }
    new Watcher(vm,updateComponent); // 渲染watcher,默认会调用updateComponent这个方法
}

function query(el){
    if(typeof el === 'string'){
        return document.querySelector(el);
    }
    return el
}
// 用用户传入的数据 去更新视图
Vue.prototype._update = function () {
    let vm = this;
    let el = vm.$el;

    // 创建一个文档碎片,将el模板里面的数据循环获取节点
    let node = document.createDocumentFragment();
    let firstChild;
    while(firstChild = el.firstChild){ // 每次拿到第一个元素就将这个元素放入到文档碎片中
        node.appendChild(firstChild);//appendChid会移动这个元素,所以每次循环第一次都会变化
    }
    
    // 对文本进行替换
    compiler(node,vm);
    
    el.appendChild(node);
}

执行$mount的时候,最后会创建一个watcher,这个watcher主要是在数据进行更新后,会通知watcher执行更新操作。一开始组件创建的时候便是更新组件,而计算属性computed和监视属性watch便是他们的定义的回调函数。

compiler函数实现

js 复制代码
//utils.js
//?:表示不捕获分组,+?表示惰性
const defaultRE = /\{\{((?:.|\n)+?)\}\}/g
export const util = {
    getValue(vm,expr){ // [msg]
        let keys = expr.split('.');
        //这里主要是返回匹配到的变量的值,例如匹配到msg,需要返回vm.msg的值,
        //但是这里可能存在 vm.school.name,这种访问形式,所以单纯的vm[expr]是不行的
        return keys.reduce((memo,current)=>{ 
            memo = memo[current]; 
            return memo
        },vm);
    },
    compilerText(node,vm){ // 编译文本 替换{{school.name}}
        node.textContent = node.textContent.replace(defaultRE,function (...args) {
            return util.getValue(vm,args[1].trim());//这里注意要去除空格,因为模板里面可能会是这样 {{      msg }} 
        });
    }
}
export function compiler(node,vm){ // node 上面创建的文档碎片 
    let childNodes = node.childNodes; 

    [...childNodes].forEach(child=>{ 
        if(child.nodeType == 1){ //1 元素  3表示文本
            compiler(child,vm); // 递归,编译当前元素的孩子节点
        }else if(child.nodeType == 3){
            util.compilerText(child,vm);
        }
    });
}

watcher类

watcher作用:数据更新后会通知watcher执行相应的回调进行更新

js 复制代码
// observe/watcher.js
let id = 0;
class Watcher{ // 每次产生一个watcher 都要有一个唯一的标识
    /**
     * @param {*} vm 当前组件的实例 new Vue
     * @param {*} exprOrFn 用户可能传入的是一个表达式 也有可能传入的是一个函数
     * @param {*} cb 用户传入的回调函数 vm.$watch('msg',cb)
     * @param {*} opts // 一些其他参数
     */
    constructor(vm,exprOrFn,cb=()=>{},opts={}){
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        if(typeof exprOrFn === 'function'){
            this.getter = exprOrFn; // getter就是new Watcher传入的第二个函数
        }
        this.cb = cb;
        this.opts = opts;
        this.id = id++;

        this.get(); // 默认创建一个watcher 会调用自身的get方法 
    }
    get(){
        this.getter(); // 让这个当前传入的函数执行
    }
}

export default Watcher

到这里已经实现数据的劫持,并在组件一开始时进行数据渲染了,但是在数据发生变化之后,视图还不会进行更新,这里就需要进行依赖收集。

收集依赖

所谓依赖收集,就是给data中每个属性在被模板访问时,比如{{msg}},此时msg就需要创建一个dep实例,这个实例得依赖数组存储那些访问了他的watcher。例如在一开始组件创建时,就会创建一个组件watcher,然后将这个watcher收集到每个属性得dep得依赖数组中。

dep类

js 复制代码
// observe/dep.js
let id = 0;
class Dep{
    constructor(){
        this.id = id++;
        this.subs = [];
    }
    addSub(watcher){ // 订阅 
        this.subs.push(watcher);
    }
    //dep就是通过notify通知watcher得
    notify(){
        this.subs.forEach(watcher=>watcher.update());
    }
    depend(){
        if(Dep.target){ // 为了防止直接调用depend方法 先判断一下
            // Dep.target是一个渲染watcher
            Dep.target.addDep(this); // 在watcher中互相记忆
        }
    }
}

// 用来保存当前的watcher
let stack = [];
export function pushTarget(watcher){
    Dep.target = watcher;
    stack.push(watcher);
}
export function popTarget(){
    stack.pop();
    Dep.target = stack[stack.length-1];
}
export default Dep;

这里得Dep.target记录着使用了当前属性的watcher。

watcher会做如下修改

js 复制代码
let id = 0;
import {pushTarget,popTarget} from './dep'
class Watcher{ // 每次产生一个watcher 都要有一个唯一的标识
    /**
     * @param {*} vm 当前组件的实例 new Vue
     * @param {*} exprOrFn 用户可能传入的是一个表达式 也有可能传入的是一个函数
     * @param {*} cb 用户传入的回调函数 vm.$watch('msg',cb)
     * @param {*} opts // 一些其他参数
     */
    constructor(vm,exprOrFn,cb=()=>{},opts={}){
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        if(typeof exprOrFn === 'function'){
            this.getter = exprOrFn; // getter就是new Watcher传入的第二个函数
        }
        this.cb = cb;
        this.deps = [];
        this.depsId = new Set()
        this.opts = opts;
        this.id = id++;

        this.get(); // 默认创建一个watcher 会调用自身的get方法 
    }
    get(){
        pushTarget(this); // 渲染watcher Dep.target = watcher  msg变化了 需要让这个watcher重新执行
        // 默认创建watcher 会执行此方法
        this.getter(); // 让这个当前传入的函数执行
        //清除watcher
        popTarget();
    }
    addDep(dep){ // 让watcher记录dep
        let id = dep.id; // msg 的dep,每一个属性创建时都会创建一个Dep,都有唯一id
        if(!this.depsId.has(id)){
            this.depsId.add(id)
            this.deps.push(dep);
            dep.addSub(this);
        }
    }
    update(){
        this.get();
    }
}

export default Watcher

defineReactive也要做出改变,就是在get时候,收集依赖watcher,而set时数据更新,需要通知wathcer调用更新方法。

js 复制代码
import Dep from './dep'
export function defineReactive(data,key,value){ // 定义响应式的数据变化

    observe(value); // 递归观察  
    // 每一个属性都new一个Dep,相同的属性用的是同一个,比如msg,就算模板用了多次也是用的同一个Dep
    let dep = new Dep(); // dep里可以收集依赖 收集的是watcher 每一个属性都增加一个dep实例
    Object.defineProperty(data,key,{
        get(){ //只要对这个属性进行了取值操作 ,就会将当前的watcher 存入进去
            if(Dep.target){ // 这次有值用的是渲染watcher
                //依赖收集,必须在Dep.target有值时
               dep.depend(); 
            }
            return value;
        },
        set(newValue){
            if(newValue === value) return;
            observe(newValue); // 如果你设置的值是一个对象的话 应该在进行监控这个新增的对象
            console.log('设置数据')
            value = newValue;
            dep.notify();
        }
    })
}

到这里还需要修改编译函数compiler。

此时数据设置新值后的更新过程:当数据被更新之后,会调用dep.notify函数,notify里面通知各个watcher(虽然目前只有一个,就是组件的渲染watcher), watcher调用update,这个方法会调用this.getter(),这个getter是开始new Watcher时候传给watcher的回调,也就是updateComponent回调,updateComponent会调用vm._update,从而更新这个视图。

但是里面再遇到compiler编译模板时,会遇到,再匹配到一个文本节点时,本来会根据{{msg}}这种方式将{{msg}}替换成data中的值,但是此时拿到的节点是已经渲染好的值,例如msg的值是hello,此时拿到的文本就是hello,所以按照原本的替换方式会造成替换失败。需要如下修改,记录第一次的文本节点形式({{msg}})。

原本的

js 复制代码
  compilerText(node, vm) {
    // 编译文本 替换{{school.name}}
    node.textContent = node.textContent.replace(defaultRE, function (...args) {
      return util.getValue(vm, args[1]);
    });
  },

修改后

js 复制代码
    compilerText(node,vm){ // 编译文本 替换{{school.name}}
        if(!node.expr){    //记录{{msg}}形式,方便下次更新后做替换
            node.expr = node.textContent; 
        }
        node.textContent = node.expr.replace(defaultRE,function (...args) {
            return util.getValue(vm,args[1]); 
        });
    }

测试

html 复制代码
    <div id="app">
      {{msg}}
      <div>
        <div>名字 {{profile.name}}</div>
        <div>年龄 {{profile.age}}</div>
      </div>
      {{msg}}
    </div>

渲染如下

然后更新一下msg的值

js 复制代码
setTimeout(() => {
  vm.msg = "world";
}, 2000);

便可再两秒后观察到模板更新

但是此时如果设置多次,会发现他会更新多次,就如下面

js 复制代码
setTimeout(() => {
  vm.msg = "world";
  vm.msg = "yes";
}, 2000);

此时就需要用到异步批量更新

异步批量更新

异步批量更新是等到数据的赋值完成再统一执行更新视图功能

需要对watcher做出修改,因为更新就是从watcher的update方法开始的,每设置一个新值,就会调用这个update方法。

js 复制代码
let id = 0;
import {pushTarget,popTarget} from './dep'
import Vue from '..';
class Watcher{ // 每次产生一个watcher 都要有一个唯一的标识
    /**
     * @param {*} vm 当前组件的实例 new Vue
     * @param {*} exprOrFn 用户可能传入的是一个表达式 也有可能传入的是一个函数
     * @param {*} cb 用户传入的回调函数 vm.$watch('msg',cb)
     * @param {*} opts // 一些其他参数
     */
    constructor(vm,exprOrFn,cb=()=>{},opts={}){
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        if(typeof exprOrFn === 'function'){
            this.getter = exprOrFn; 
        }
        this.cb = cb;
        this.deps = [];
        this.depsId = new Set()
        this.opts = opts;
        this.id = id++;

        this.get(); // 默认创建一个watcher 会调用自身的get方法 
    }
    get(){
        pushTarget(this); 
        this.getter();
        popTarget(); // Dep.target = undefined
    }
    addDep(dep){
        let id = dep.id;
        if(!this.depsId.has(id)){
            this.depsId.add(id)
            this.deps.push(dep);
            dep.addSub(this);
        }
    }
    update(){ // 如果立即调用get 会导致页面刷新 异步来更新
        queueWatcher(this);
    }
    //添加一个run方法,由他来调用更新
    run(){
        this.get();
    }
}
let has = {};
let queue = [];
function flushQueue(){
    queue.forEach(watcher=>watcher.run());
    has = {}; // 恢复正常 下一轮更新时继续使用
    queue = [];
}
function queueWatcher(watcher){ // 对相同属性的重复的watcher进行过滤操作
    //比如msg,多次设置msg会重复调用这个函数,但这里拿到的watcher是相同的一个
    let id = watcher.id;
    if(has[id] == null){
        has[id] = true;
        queue.push(watcher);

        // 延迟清空队列
        nextTick(flushQueue);// 异步方法会等待所有同步方法执行完毕后调用此方法
    }
}

nextTick方法实现

js 复制代码
let callbacks = [];//记录传入的会回调函数,等待一起更新
function flushCallbacks(){
    callbacks.forEach(cb=>cb());
}
function nextTick(cb){ // cb就是flushQueue
    callbacks.push(cb);
    
    // 要异步刷新这个callbacks ,获取一个异步的方法 
    //                          微任务                       宏任务
    // 异步是分执行顺序的 会先执行(promise  mutationObserver)  setImmediate  setTimeout 
    //这里通过多重判断,主要考虑兼容性和性能问题
    let timerFunc = ()=>{
        flushCallbacks();
    }
    if(Promise){ // then方法是异步的
        return Promise.resolve().then(timerFunc)
    }
    if(MutationObserver){ 
        //MutationObserver可以对dom树做监听,这里是当节点的文本发生变化时,会调用传入MutationObserver中的回调,也就是timeFunc
        let observe = new MutationObserver(timerFunc); 
        let textNode = document.createTextNode(1);
        observe.observe(textNode,{characterData:true});
        textNode.textContent = 2;
        return
    }
    if(setImmediate){
        return setImmediate(timerFunc)
    }
    setTimeout(timerFunc, 0);
}

此时对数据进行更新就不会触发多次了

watch实现

实现最开始initState中的initWatch函数。

js 复制代码
export function initState(vm) {
  //做不同的初始化工作
  let opts = vm.$options;
  if (opts.data) {
    initData(vm); // 初始化数据
  }
  if (opts.computed) {
    initComputed(); 
  }
  if (opts.watch) {
    initWatch(vm); // 初始化watch,传入vm实例
  }
}
js 复制代码
function initWatch(vm) {
  let watch = vm.$options.watch; // 获取用户传入的watch属性
  //遍历每个监视属性,为每个监视属性开启一个watcher
  for (let key in watch) {
    // msg(newValue,oldValue){...}
    let userDef = watch[key];
    //这里的handler是用户定义的监视属性是对象形式里面的handler处理函数
    let handler = userDef;
    if (userDef.handler) {
      handler = userDef.handler;
    }
    //immediate用于是否立即调用
    createWatcher(vm, key, handler, { immediate: userDef.immediate });
  }
}

function createWatcher(vm, key, handler, opts) {
  // 内部最终也会使用$watch方法
  return vm.$watch(key, handler, opts);
}

createWatcher函数,就是给监视属性创建一个watcher,到时候监视的属性发生了更新,watcher里面调用update

$watch函数实现

js 复制代码
Vue.prototype.$watch = function (expr,handler,opts) {
    let vm = this;
    new Watcher(vm,expr,handler,{user:true,...opts}); // user标记时用户定义的watcher,例如watch和computed的回调
}

watcher类也需要做出相应的改变处理

js 复制代码
class Watcher{ 
    /**
     * @param {*} vm 当前组件的实例 new Vue
     * @param {*} exprOrFn 用户可能传入的是一个表达式 也有可能传入的是一个函数
     * @param {*} cb 用户传入的回调函数 vm.$watch('msg',cb)
     * @param {*} opts // 一些其他参数
     */
  //监视属性new watcher时传入参数例子
          //    vm , msg  ,(newValue,oldValue)=>{} ,{user:true}
    constructor(vm,exprOrFn,cb=()=>{},opts={}){
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        if(typeof exprOrFn === 'function'){
            this.getter = exprOrFn; 
        }else {
            this.getter = function () { // 如果调用此方法 会将vm上对应的exprOrFn属性取出来
                return util.getValue(vm,exprOrFn)
            }
        }
        if(opts.user){ // 标识是用户自己写的watcher
            this.user = true;
        }
        this.cb = cb;
        this.deps = [];
        this.depsId = new Set()
        this.opts = opts;
        this.id = id++;
        this.immediate = opts.immediate
        // 监视属性创建watcher的时候,就会发生调用一次,并且获取一个值(老值)
        this.value = this.get(); //记录老值
        if(this.immediate){ // 如果有immediate 就直接运行用户定义的函数
            this.cb(this.value);
        }
    }
    get(){
        pushTarget(this); 
        let value = this.getter(); // 让这个当前传入的函数执行    
        popTarget(); 
        return value;
    }
    addDep(dep){ 
        let id = dep.id; 
        if(!this.depsId.has(id)){
            this.depsId.add(id)
            this.deps.push(dep);
            dep.addSub(this);
        }
    }
    update(){
        queueWatcher(this);
    }
    run(){
        let value = this.get(); // 这里的调用是发生在监视属性更新时调用的,此时获取的值是新值
        if(this.value !== value){
            this.cb(value,this.value);
        }
    }
}

至此,watch的功能已经实现。watch函数的流程:当初始化每一个watcher属性时,都会创建一个对应的watcher,watcher实例里面调用get()方法获取旧值,如果此时有传入immediate,就会发生调用一次cb,cb是用户定义的watch回调函数,cb的第二个参数没传,所以就是undefined。

然后当属性发生更新时,会调用run方法,此时会再次调用get方法获取最新的值,然后判断新旧值是否一致,进行调用cb回调函数,第二个参数this.value就是之前获取的旧值。

computed实现

js 复制代码
let vm = new Vue({
    el:document.getElementById('app')
    data(){
        return {
            firstName:'s',
            lastName:'z'
        }
    },
    computed:{
        fullName(){
            return this.firstName + this.lastName
        }
    }
});
js 复制代码
export function initState(vm) {
  //做不同的初始化工作
  let opts = vm.$options;
  if (opts.data) {
    initData(vm); // 初始化数据
  }
  if (opts.computed) {
    initComputed(vm, opts.computed); // 初始化计算属性
  }
  if (opts.watch) {
    initWatch(vm); // 初始化watch
  }
}

initComputed函数实现

js 复制代码
function initComputed(vm, computed) {
  // 将计算属性的配置 放到vm上
  let watchers = (vm._watchersComputed = Object.create(null)); // 创建存储计算属性的watcher的对象

  for (let key in computed) {
    // computed:{fullName:()=>this.firstName+this.lastName}
    let userDef = computed[key];
    
    //为每个计算属性创建一个watcher
    watchers[key] = new Watcher(vm, userDef, () => {}, { lazy: true }); //lazy标识这个watcher是计算属性watcher, 计算属性watcher 默认刚开始这个方法不会执行,需要在读取到这个属性才会执行
     
    Object.defineProperty(vm, key, {
      get: createComputedGetter(vm, key),
    }); // 将这个属性 定义到vm上,例如vm.fullName
  }
}

当在解析模板的时候,会读取到计算属性,这个时候就会调用这里的createComputedGetter函数,这个函数经过一些列操作会返回最新的值

createComputedGetter函数

js 复制代码
function createComputedGetter(vm, key) {
    //之前在这个_watchersComputed上面存过计算属性的watcher了,可以直接取出来
  let watcher = vm._watchersComputed[key];
  
  // 用户取值是会执行此方法
  return function () {
    if (watcher) {
      // 如果dirty 是false的话 不需要重新执行计算属性中的方法,直接取到缓存值
      if (watcher.dirty) {
        //dirty为true,标识需要重新计算最新值, 调用watcher的evaluate方法计算最新值,这个方法需要定义
        watcher.evaluate();
      }
      
      //这里在后面会解释
      if (Dep.target) {
        // watcher 就是计算属性watcher dep = [firstName.dep,lastName.Dep]
        watcher.depend();
      }
      return watcher.value;
    }
  };
}

if (Dep.target)这里的判断后面会解释到

watcher的evaluate方法和depend方法的实现

js 复制代码
class Watcher{ 
    /**
     * @param {*} vm 当前组件的实例 new Vue
     * @param {*} exprOrFn 用户可能传入的是一个表达式 也有可能传入的是一个函数
     * @param {*} cb 用户传入的回调函数 vm.$watch('msg',cb)
     * @param {*} opts // 一些其他参数
     */
    //          vm  ()=>this.firstName+this.lastName  ()=>{}  lazy:true
    constructor(vm,exprOrFn,cb=()=>{},opts={}){
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        if(typeof exprOrFn === 'function'){
            this.getter = exprOrFn; 
        }else {
            this.getter = function () { 
                return util.getValue(vm,exprOrFn)
            }
        }
        if(opts.user){ // 标识是用户自己写的watcher
            this.user = true;
        }
        this.lazy = opts.lazy; // 如果这个值为true 说明他是计算属性
        this.dirty = this.lazy;
        this.cb = cb;
        this.deps = [];
        this.depsId = new Set()
        this.opts = opts;
        this.id = id++;
        this.immediate = opts.immediate
        // 如果当前我们是计算属性的话 不会默认调用get方法,等到模板解析到计算属性才会调用
        this.value = this.lazy? undefined : this.get(); // 默认创建一个watcher 会调用自身的get方法;
        if(this.immediate){ 
            this.cb(this.value);
        }
    }
    get(){
        pushTarget(this); 
        // dep = [watcher]                    dep =[watcher]
        // fullName(){return this.firstName + this.lastName}
        // 这个函数调用时就会将当前计算属性watcher 存起来
        let value = this.getter.call(this.vm); 
        
        popTarget();
       
        return value;
    }
    evaluate(){
        this.value = this.get();
        this.dirty = false; // 值求过了,标识为false,下次直接拿缓存值
    }
    addDep(dep){ 
        let id = dep.id; 
        if(!this.depsId.has(id)){
            this.depsId.add(id)
            this.deps.push(dep); // 就让watcher 记住了当前的dep
            dep.addSub(this);
        }
    }
    depend(){
        let i = this.deps.length;
        while(i--){
            this.deps[i].depend();
        }
    }
    update(){ 
        if(this.lazy){ // 如果是计算属性 
            this.dirty = true; // 计算属性依赖的值变化了 ,标识为true,后面取值需要重新计算结果
        }else {
            queueWatcher(this);
        }
    }
    run(){
        let value = this.get(); 
        if(this.value !== value){
            this.cb(value,this.value);
        }
    }
}

computed的实现过程:首先在开始初始化计算属性时,也就是调用initComputed函数时,会为每个计算属性创建一个watcher,而在new Watcher时,什么都不会调用,因为传递的lazy是true,里面通过this.value = this.lazy? undefined : this.get(),所以不会执行get函数。

计算属性只有在读取到的时候才会调用get方法,计算他的值。

接着将每个计算属性定义到vm实例上面,通过vm.fullname1获取,并使用defineProperty做了get拦截。initState初始化完成之后,就开始执行挂载方法$mount,这个方法也会new Watcher,这个watcher称他为渲染watcher,创建watcher时,内部会执行get方法,这个时候会通过pushTarget(this)将这个渲染watcher推入数组,然后执行getter方法(开始模板解析)。

在进行模板解析时,例如遇到{{fullname}},此时就会调用这个createComputedGetter方法,然后通过fullname这个key拿到对应的watcher实例,这个watcher称他为计算属性watcher,接着调用evaluate开始计算它的值。

evaluate内部直接调用get方法,此时又会pushTarget(this),这里的this指向的是计算属性的watcher,将当前这个计算属性的watcher推入数组中,这个时候数组内部有两个watcher,分别是【渲染watcher,计算属性watcher】, 然后调用getter方法,计算fullname的结果,这个getter就是当初创建watcher传入内部的计算属性的函数,例如fullName(){return this.firstName + this.lastName}

调用这个getter函数时,内部会访问this.firstName和this.lastName,就会触发这两个变量的get拦截。这两个拦截就会将Dep.target(指向数组中的最后一个,计算属性watcher)的deps加入这个两个属性的dep实例,同样的,这个两个属性的dep实例的subs数组(用于收集watcher依赖)也会收入这个计算属性watcher。因为前面使用了相互收集依赖

形式如下:

js 复制代码
firstName的依赖数组=[fullname计算属性watcher]
lastName的依赖数组=[fullname计算属性watcher]

计算属性watcher的依赖数组=[firstName的dep,lastName的dep]

求完值之后,赋值给value,调用popTarget(),将【渲染watcher,计算属性watcher】中的计算属性watcher推出数组,然后value返回,并记录在这个计算属性watcher的this.value上面,并标记disty为false,下次可以直接走缓存。至此evaluate执行完成了。

接下来就进入判断Dep.target,这个才是在修改完firstname或lastname后,触发更新视图的关键。此时Dep.target是渲染watcher,调用计算属性watcher的depend方法,这个方法内部拿到deps数组,也就是------计算属性watcher的依赖数组=[firstName的dep,lastName的dep],循环里面的每个属性的dep,并调用depend方法,将Dep.target指向的watcher推入firstname和lastname的依赖数组里面,firstname和lastname的依赖数组就会多了一个渲染watcher

js 复制代码
firstName的依赖数组=[fullname计算属性watcher,渲染watcher]
lastName的依赖数组=[fullname计算属性watcher,渲染watcher]

然后在修改了计算属性fullname的任何一个依赖(例如firstname)时,就会被依赖属性的set方法拦截到,set里面调用notify方法,notify调用firstname的依赖数组中每个watcher的update进行更新,就是上面的[fullname计算属性watcher,渲染watcher],fullname计算属性watcher会计算最新值,渲染watcher负责更新视图,此时就实现了computed的功能。

相关推荐
念念不忘 必有回响3 小时前
viepress:vue组件展示和源码功能
前端·javascript·vue.js
C澒3 小时前
多场景多角色前端架构方案:基于页面协议化与模块标准化的通用能力沉淀
前端·架构·系统架构·前端框架
崔庆才丨静觅3 小时前
稳定好用的 ADSL 拨号代理,就这家了!
前端
江湖有缘3 小时前
Docker部署music-tag-web音乐标签编辑器
前端·docker·编辑器
恋猫de小郭4 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅11 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606111 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了12 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅12 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅12 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端