手写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的功能。

相关推荐
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。1 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
mosen8682 小时前
Uniapp去除顶部导航栏-小程序、H5、APP适用
vue.js·微信小程序·小程序·uni-app·uniapp
别拿曾经看以后~2 小时前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
我要洋人死3 小时前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人3 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人3 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR3 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香3 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel